在WPF和SL5中都有RelativeSource是FindAncestor绑定,这是个很实用的绑定功能,特别是在配置模板的时候。可惜SL4里还没有,让我们来通过几个辅助类简单的实现一下。
主要思路还是通过VisualTreeHelper获取Parent来实现绑定。
新建解决方案,添加个类叫做AncestorBinding提供一下绑定需要的基本属性,如下:
public class AncestorBinding
{
public string AncestorPath { get; set; }
public string AncestorTypeName { get; set; }
public string TargetPath { get; set; }
public IValueConverter Converter { get; set; }
}
再添加类BindingHelper用以提供绑定用的附加属性,如下:
public class BindingHelper
{
public static AncestorBinding GetAncestorBinding(DependencyObject target)
{
if (target == null)
throw new ArgumentNullException("target");
return (AncestorBinding)target.GetValue(AncestorBindingProperty);
}
public static void SetAncestorBinding(DependencyObject target, AncestorBinding value)
{
if (target == null)
throw new ArgumentNullException("target");
target.SetValue(AncestorBindingProperty, value);
}
public static readonly DependencyProperty AncestorBindingProperty =
DependencyProperty.RegisterAttached("AncestorBinding", typeof(AncestorBinding), typeof(FrameworkElement),
new PropertyMetadata(OnAncestorChanged));
private static void OnAncestorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ancestorBinding = e.NewValue as AncestorBinding;
if (ancestorBinding == null)
throw new ArgumentNullException("AncestorBinding");
var element = d as FrameworkElement;
if (element == null)
throw new ArgumentNullException("target");
ancestorBinding.SetBinding(element);
}
}
可见在AncestorBinding需要提供个SetBinding的方法,实现如下:
public class AncestorBinding
{
internal void SetBinding(FrameworkElement element)
{
if (string.IsNullOrWhiteSpace(this.AncestorTypeName))
throw new ArgumentNullException("AncestorTypeName");
if (string.IsNullOrWhiteSpace(this.TargetPath))
throw new ArgumentNullException("TargetPath");
if (this.TryBinding(element) == false)
{
element.Loaded += ElementLoaded;
}
}
private void ElementLoaded(object sender, RoutedEventArgs e)
{
var element = sender as FrameworkElement;
element.Loaded -= ElementLoaded;
this.TryBinding(element);
}
private bool TryBinding(FrameworkElement element)
{
var dp = element.GetDependencyProperty(this.TargetPath);
if (dp == null)
return false;
var ancestor = element.FindAncestor(this.AncestorTypeName);
if (ancestor == null)
return false;
var binding = new Binding
{
Source = ancestor,
Converter = this.Converter
};
if (!string.IsNullOrEmpty(this.AncestorPath))
{
binding.Path = new PropertyPath(this.AncestorPath);
}
element.SetBinding(dp, binding);
return true;
}
}
因为设置绑定的时候控件可能还没加载,这样就会溯源失败,所以如果失败就需要在Load之后再尝试绑定一次。
在TryBinding中获取需要绑定的目标依赖属性和溯源控件,如果都成功获取就设置绑定。
再看一下TryBinding用到的两个扩展方法,添加静态类FrameworkElementExtensions,如下:
public static class FrameworkElementExtensions
{
internal static DependencyProperty GetDependencyProperty(this FrameworkElement element, string propertyName)
{
if (element == null || string.IsNullOrEmpty(propertyName))
return null;
var dpInfo = element.GetType().GetField(propertyName + "Property", BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy);
if (dpInfo == null)
return null;
return dpInfo.GetValue(element) as DependencyProperty;
}
internal static object FindAncestor(this FrameworkElement element, string ancestorTypeName)
{
if (string.IsNullOrEmpty(ancestorTypeName))
return null;
while (element != null)
{
if (IsTypeMatch(element, ancestorTypeName))
return element;
if (IsTypeMatch(element.DataContext, ancestorTypeName))
return element.DataContext;
element = VisualTreeHelper.GetParent(element) as FrameworkElement;
}
return null;
}
private static bool IsTypeMatch(object obj, string typeName)
{
if (obj == null || string.IsNullOrEmpty(typeName))
return false;
var type = obj.GetType();
var interfaces = type.GetInterfaces();
if (interfaces != null && interfaces.Any(p => p.Name == typeName))
return true;
while (type != null)
{
if (type.Name == typeName)
return true;
type = type.BaseType;
}
return false;
}
}
获取附加属性的方法GetDependencyProperty是通过简单的反射来实现。
溯源的方法FindAncestor是通过VisualTreeHelper来实现,为了更好的溯源,此处对DataContenxt也进行了匹配搜索。
代码就是这么多,挺简单,看看就明白。简单测试一下吧。
<Button Margin="1,2,3,4" Height="30" Width="150">
<TextBlock >
<local:BindingHelper.AncestorBinding>
<local:AncestorBinding TargetPath="Text" AncestorPath="Margin" AncestorTypeName="Button"/>
</local:BindingHelper.AncestorBinding>
</TextBlock>
</Button>
这个示例比较简单,只是把Text属性绑定到Button的Margin上。实际应用中可能在设置模板的时候用到的更多。
效果:
RelativeSource