通过附加行为在 Silverlight 中进行 ElementName 绑定






4.80/5 (4投票s)
这篇技术博文展示了如何在 Silverlight 2.0 中纯粹通过 XAML 实现ElementName绑定。
作为 Silverlight 的相对新手,我在开始开发时,惊喜地感受到了熟悉带来的温暖。从 WPF 过渡到 Silverlight 开发非常容易,因为大部分核心概念都是相同的。然而,你会开始怀念 WPF 框架中的一些部分。其中之一就是 ElementName
绑定。
对于不熟悉这个概念的人,我将给出一个非常简短的概述。当你在 XAML 中绑定视觉元素的属性时,这个绑定的源将是与元素 DataContext
关联的对象。这对于将业务对象绑定到公开其属性的 UI 非常有用。然而,在 WPF 中,绑定为你提供的不仅仅是公开业务数据的机制,它还允许你在视觉元素之间绑定属性。这是一个强大的概念,它允许你创建比框架提供的面板(有关示例,请参阅我关于创建 Bullet Graph 的文章)更复杂的布局。为了实现这一点,WPF 提供了 ElementName
和 RelativeSource
绑定,为你提供了一个强大的机制来定位视觉树中的其他元素进行绑定。下面给出了一个简单的示例,其中一个矩形的宽度绑定到一个命名的滑块。
<Rectangle Width="{Binding Path=Value, ElementName=MySlider}" Height="20" Fill="Green"/>
<Slider x:Name="MySlider" Value="25" Minimum="0" Maximum="300"/>
不幸的是,Silverlight 没有这个能力。
我的第一个想法是简单地将目标元素的 DataContext
指向源元素,以便在它们之间进行属性绑定。然而,令我非常惊讶的是,Silverlight 的依赖属性 不支持属性更改通知。
解决这个问题的常用方法是采用中继对象(Relay Object),如 许多博文中所述。一个具有单个属性 Value
并实现 INotifyPropertyChanged
的对象被绑定到两个视觉元素。下面是一个简单的示例:
<UserControl.Resources>
<local:RelayObject x:Key="Relay">
<local:RelayObject.Value>
<system:Double>20</system:Double>
</local:RelayObject.Value>
</local:RelayObject>
</UserControl.Resources>
<StackPanel x:Name="LayoutRoot" Background="White">
<Rectangle Width="{Binding Path=Value, Source={StaticResource Relay},
Mode=TwoWay" Height="20" Fill="Green"/>
<Slider Value="{Binding Path=Value, Source={StaticResource Relay},
Mode=TwoWay}" Minimum="0" Maximum="300"/>
</StackPanel>
这里,我们的 RelayObject
实例同时绑定到 Rectangle
的 Width
和 Slider
的 Value
,从而有效地将这两个属性绑定在一起。这效果很好,但它并没有真正像 WPF 的 ElementName
绑定,而且,你必须为每个绑定添加一个新的 RelayObject
实例。
我的解决方案利用了在 WPF 和 Silverlight 中越来越受欢迎的 附加行为模式。首先,我们定义一个附加属性,它使用一种类型来包含我们创建绑定所需的信息,即源属性和目标属性,以及我们想要绑定的元素的名称。
public class BindingProperties
{
public string SourceProperty { get; set; }
public string ElementName { get; set; }
public string TargetProperty { get; set; }
}
public static class BindingHelper
{
public static BindingProperties GetBinding(DependencyObject obj)
{
return (BindingProperties)obj.GetValue(BindingProperty);
}
public static void SetBinding(DependencyObject obj, BindingProperties value)
{
obj.SetValue(BindingProperty, value);
}
public static readonly DependencyProperty BindingProperty =
DependencyProperty.RegisterAttached
("Binding", typeof(BindingProperties), typeof(BindingHelper),
new PropertyMetadata(null, OnBinding));
/// <summary>
/// property change event handler for SelectAllButtonTemplate
/// </summary>
private static void OnBinding(
DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
FrameworkElement targetElement = depObj as FrameworkElement;
targetElement.Loaded += new RoutedEventHandler(TargetElement_Loaded);
}
private static void TargetElement_Loaded(object sender, RoutedEventArgs e)
{
FrameworkElement targetElement = sender as FrameworkElement;
// get the value of our attached property
BindingProperties bindingProperties = GetBinding(targetElement);
// perform our 'ElementName' lookup
FrameworkElement sourceElement =
targetElement.FindName(bindingProperties.ElementName) as FrameworkElement;
// bind them
CreateRelayBinding(targetElement, sourceElement, bindingProperties);
}
}
上面代码的作用是定义类型为 BindingProperties
的附加属性。每当此属性附加到依赖对象时,就会调用 OnBinding
方法。在此事件处理程序中,我们将一个处理程序添加到元素的 Loaded
事件。当元素在视觉树中布局并准备好操作时,就会发生此事件,此时我们就可以执行 ElementName
查找。
在 TargetElement_Loaded
事件处理程序中,我们使用 FrameworkElement.FindName 来查找我们要绑定的命名源元素。此方法会在与它位于同一个 XAML 命名空间内的具有给定名称的任何元素。有趣的是,这与 Visual Studio 用于从 XAML 中的命名元素在代码隐藏文件中创建成员变量的同一个方法相同。一旦找到命名元素,就会在它们之间构造一个中继绑定,如下所示:
private static readonly BindingFlags dpFlags =
BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy;
private static void CreateRelayBinding
(FrameworkElement targetElement, FrameworkElement sourceElement,
string targetProperty, string sourceProperty, IValueConverter converter)
{
// create a relay binding between the two elements
ValueObject relayObject = new ValueObject();
// find the source dependency property
FieldInfo[] sourceFields = sourceElement.GetType().GetFields(dpFlags);
FieldInfo sourceDependencyPropertyField =
sourceFields.First(i => i.Name == sourceProperty + "Property");
DependencyProperty sourceDependencyProperty =
sourceDependencyPropertyField.GetValue(null) as DependencyProperty;
// initialise the relay object with the source dependency property value
relayObject.Value = sourceElement.GetValue(sourceDependencyProperty);
// create the binding for our target element to the relay object, this binding will
// include the value converter
Binding targetToRelay = new Binding();
targetToRelay.Source = relayObject;
targetToRelay.Path = new PropertyPath("Value");
targetToRelay.Mode = BindingMode.TwoWay;
targetToRelay.Converter = converter;
// find the target dependency property
FieldInfo[] targetFields = targetElement.GetType().GetFields(dpFlags);
FieldInfo targetDependencyPropertyField =
targetFields.First(i => i.Name == targetProperty + "Property");
DependencyProperty targetDependencyProperty =
targetDependencyPropertyField.GetValue(null) as DependencyProperty;
// set the binding on our target element
targetElement.SetBinding(targetDependencyProperty, targetToRelay);
// create the binding for our source element to the relay object
Binding sourceToRelay = new Binding();
sourceToRelay.Source = relayObject;
sourceToRelay.Path = new PropertyPath("Value");
sourceToRelay.Mode = BindingMode.TwoWay;
// set the binding on our source element
sourceElement.SetBinding(sourceDependencyProperty, sourceToRelay);
}
上面的代码只是创建了我们的中继对象,用源元素的值对其进行初始化,然后构造从中继到源和从中继到目标的绑定。上面代码中唯一“棘手”的部分是使用反射来查找每个元素的静态依赖属性。
使用上面的附加属性(行为),可以如下构造一个ElementName绑定:
<Rectangle Height="20" Fill="Green">
<local:BindingHelper.Binding>
<local:BindingProperties ElementName="Slider"
SourceProperty="Value" TargetProperty="Width"/>
</local:BindingHelper.Binding>
</Rectangle>
<Slider x:Name="Slider" Value="20" Minimum="0" Maximum="300"/>
这种方法有两个优点:首先,我们不必为每个 ElementName
绑定显式创建一个中继对象;其次,源属性的值用于直接初始化中继对象。将上述方法扩展到在绑定中添加 ValueConverters
也是一个简单的练习。
但是,如果我们想将两个不同的属性绑定到我们的滑块呢?例如,如果我们想绑定两个矩形的宽度,我们需要确保创建一个由 Slider
和两个 Rectangles
共享的单个中继对象。解决这个问题的一个简单方法是维护一个从源绑定到其关联中继对象的字典。如果我们有一个以上的目标属性绑定到特定的源属性,则会重用中继对象。任何值转换器都指定在目标绑定上,因此我们可以将多个属性与不同的转换器绑定到源。
以下示例显示了几个绑定到我们的滑块的 Rectangles
,它们的 Width
通过不同的因子进行缩放:
<Rectangle Height="20" Fill="Green">
<local:BindingHelper.Binding>
<local:BindingProperties ElementName="Slider" SourceProperty="Value"
TargetProperty="Width" Converter="{StaticResource ScalingConverter}"
ConverterParameter="2"/>
</local:BindingHelper.Binding>
</Rectangle>
<Rectangle Height="20" Fill="Green">
<local:BindingHelper.Binding>
<local:BindingProperties ElementName="Slider" SourceProperty="Value"
TargetProperty="Width" Converter="{StaticResource ScalingConverter}"
ConverterParameter="4"/>
</local:BindingHelper.Binding>
</Rectangle>
<Slider x:Name="Slider" Value="20" Minimum="0" Maximum="300"/>
这里是手动使用中继对象和使用此附加行为实现上述功能的演示:
有关上述内容的详细信息,请参阅附件的 项目下载。
这种方法确实有一些缺点——不幸的是,语法有点冗长。另外,在目前的形式下,每个视觉元素只能创建一个ElementName绑定。然而,这项技术的有趣之处在于,它可以很容易地改编为以其他方式搜索源元素,例如,模仿 WPF 的相对源绑定。也许下周我会尝试一下……