Behavior,Trigger,TriggerAction屬於元素的附加屬性,並不在可視化樹中,當我們在綁定的時候需要使用ElementName指定數據源的時候,ElementName是不起作用的。因爲使用ElementName屬性本身的元素需要在可視化樹中。
在FrameworkElement中有一個FindName函數,通過這個函數我們可以找到當前 XAML 命名空間中指定的對象。而Binding中的ElementName屬性實現也是間接的調用到這個函數。 Behavior,Trigger,TriggerAction中都保存有被附加的對象,通過這個對象和FindName再加上反射,我們就可以實現ElementName。
Behavior,Trigger,TriggerAction中並沒有ElementName函數,但可以通過調用附加對象的ElementName來實現這個功能。我選擇使用接口來擴展這樣的功能。
首先定義通用接口:
interface IElementBinding
{
/// <summary>
/// 附加對象改變時觸發的事件
/// </summary>
event EventHandler AssociatedObjectChanged;//附加對象改變時觸發的事件
/// <summary>
/// 附加對象是否在可視化樹中
/// </summary>
/// <returns></returns>
bool IsLoaded();
/// <summary>
/// 查找元素
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
object FindName(string name);
/// <summary>
/// 獲取繼承接口的對象
/// </summary>
/// <returns></returns>
DependencyObject GetDependencyObject();
}
我說明一下
IsLoaded函數,其他的函數看註釋。FindName是有使用條件的,對象本身必須處於可是化樹中才可通過這個函數找到指定的對象,IsLoaded就是用來判斷這個的,具體如何判斷看我後面的代碼。
定義完接口然後需要一個結構來指定邦定中需要用到的屬性:
public class BindingProperties
{
/// <summary>
/// 被附加源中需要綁定的屬性名稱
/// </summary>
public string SourceProperty { get; set; }
/// <summary>
/// 需要綁定控件的名稱
/// </summary>
public string ElementName { get; set; }
/// <summary>
/// 需要綁定控件中的目標屬性
/// </summary>
public string TargetProperty { get; set; }
/// <summary>
/// 轉換器
/// </summary>
public IValueConverter Converter { get; set; }
}
下面是施行邦定的關鍵代碼:
public class ElementBinding
{
public static BindingProperties GetBinding(DependencyObject obj)
{
if (obj == null)
return null;
return obj.GetValue(BindingProperty) as BindingProperties;
}
public static void SetBinding(DependencyObject obj, BindingProperties value)
{
obj.SetValue(BindingProperty, value);
}
public static readonly DependencyProperty BindingProperty =
DependencyProperty.RegisterAttached("Binding", typeof(BindingProperties), typeof(ElementBinding),
new PropertyMetadata(null, new PropertyChangedCallback(ElementBinding.OnBinding)));
private static void OnBinding(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
var Interactivity = depObj as IElementBinding;
if (Interactivity == null)
return;
if (Interactivity.IsLoaded())
{ CreateRelayBinding(Interactivity, GetBinding(Interactivity.GetDependencyObject())); }
else
{
Interactivity.AssociatedObjectChanged += Interactivity_AssociatedObjectChanged;
}
}
static void Interactivity_AssociatedObjectChanged(object sender, EventArgs e)
{
IElementBinding sourceElement = sender as IElementBinding;
BindingProperties bindingProperties = GetBinding(sourceElement.GetDependencyObject());
CreateRelayBinding(sourceElement, bindingProperties);
}
private static void CreateRelayBinding(IElementBinding sourceElement, BindingProperties bindingProperties)
{
if (sourceElement == null || bindingProperties == null)
return;
//獲取被綁定源
var source=sourceElement.GetDependencyObject();
if (source == null)
return;
//查找目標對象
DependencyObject targetElement = sourceElement.FindName(bindingProperties.ElementName) as DependencyObject;
if (targetElement == null)
return;
//通過反射找到被綁定源中需要被綁定的附加屬性
var sourceDependencyPropertyField = source.GetType().GetField(bindingProperties.SourceProperty + "Property", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
if (sourceDependencyPropertyField == null)
return;
DependencyProperty sourceDependencyProperty = sourceDependencyPropertyField.GetValue(null) as DependencyProperty;
Binding binding = new Binding();
binding.Source = targetElement;
binding.Path = new PropertyPath(bindingProperties.TargetProperty);
binding.Converter = bindingProperties.Converter;
//綁定
BindingOperations.SetBinding(source, sourceDependencyProperty, binding);
}
public static bool IsElementLoaded(FrameworkElement element)
{
if (element == null)
return false;
UIElement rootVisual = Application.Current.RootVisual;
DependencyObject parent = element.Parent;
if (parent == null)
{
parent = System.Windows.Media.VisualTreeHelper.GetParent(element);
}
return parent != null || (rootVisual != null && element == rootVisual);
}
}
邦定的過程在CreateRelayBinding函數中。
邦定流程好了以後可以開始對Behavior,Trigger,TriggerAction進行改造了。邦定中TriggerAction用的比較多,我就
以這個爲例子,Behavior和Trigger自行擴展。
首先自己定義一個TriggerAction基類,所有自定義的TriggerAction都從這個基類繼承,然後再裏面實現IElementBinding的所有接口。注意:不實現IElementBinding接口是無法使用ElementBinding類的。
public abstract class CustomTriggerActionBase : TriggerAction<FrameworkElement>, IElementBinding
{
public event EventHandler AssociatedObjectChanged;
protected override void OnAttached()
{
base.OnAttached();
if (ElementBinding.IsElementLoaded(this.AssociatedObject) && AssociatedObjectChanged != null)
AssociatedObjectChanged(this, new EventArgs());
else if (this.AssociatedObject != null)
this.AssociatedObject.Loaded += AssociatedObject_Loaded;
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.Loaded -= AssociatedObject_Loaded;
}
void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
if (AssociatedObjectChanged != null)
AssociatedObjectChanged(this, new EventArgs());
}
public bool IsLoaded()
{
return ElementBinding.IsElementLoaded(this.AssociatedObject);
}
public object FindName(string name)
{
return this.AssociatedObject == null ? null : this.AssociatedObject.FindName(name);
}
public DependencyObject GetDependencyObject()
{
return this;
}
}
使用起來很簡單,比如我們有一個NavigateAction,用來導航,裏面有一個Parameter,用來向下一個頁面傳遞參數,
使用時這樣寫:
<Button Content="To Page1" Width="200" Height="80" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<local:NavigateAction NavigateName="/Page1.xaml">
<local:ElementBinding.Binding>
<local:BindingProperties SourceProperty="Parameter" ElementName="ParameterText" TargetProperty="Text"/>
</local:ElementBinding.Binding>
</local:NavigateAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
導航前界面:
導航後的界面: