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>
导航前界面:
导航后的界面: