Silverlight MultiBindings,如何将多个绑定附加到单个属性






4.92/5 (17投票s)
这篇博文描述了一种在 Silverlight 应用程序中将多个绑定与单个依赖属性关联的技术。WPF 已经通过 MultiBindings 的形式提供了此功能,本文中的代码模拟了此功能。
这篇博文描述了一种在 Silverlight 应用程序中将多个绑定与单个依赖属性关联的技术。WPF 已经通过 MultiBindings 的形式提供了此功能,本文中的代码模拟了此功能。
更新:这篇博文提供了 Silverlight 3 的代码。对于 Silverlight 4 用户,请参阅我的博客以获取更新的代码。
下面的简单应用程序演示了这项技术,其中有三个数据录入文本框绑定到一个简单的 Person
对象的各个属性,而标题文本块则同时绑定到 Forename
和 Surname
属性。尝试编辑姓氏或名字字段,观察标题是如何更新的。
[CodeProject 不支持 Silverlight 小程序,请在我的博客上查看正在运行的应用程序。]
此应用程序的 XAML 大致如下(为清晰起见,移除了多余的属性/元素)
<TextBlock Foreground="White" FontSize="13">
<local:BindingUtil.MultiBinding>
<local:MultiBinding TargetProperty="Text"
Converter="{StaticResource TitleConverter}">
<Binding Path="Surname"/>
<Binding Path="Forename"/>
</local:MultiBinding>
</local:BindingUtil.MultiBinding>
</TextBlock>
<TextBlock Text="Surname:"/>
<TextBox Text="{Binding Path=Surname, Mode=TwoWay}"/>
<TextBlock Text="Forename:"/>
<TextBox Text="{Binding Path=Forename, Mode=TwoWay}"/>
<TextBlock Text="Age:"/>
<TextBox Text="{Binding Path=Age, Mode=TwoWay}"/>
解决方案
我解决多重绑定的问题的方法是引入一个名为 MultiBinding
的类,该类通过 BindingUtil.MultiBinding
附加属性与具有多重绑定目标属性的元素关联。下图详细介绍了我的想法
Forename
和 Surname
绑定到 MultiBinding
的属性(具体是哪些属性,我们稍后会详细介绍)。MultiBinding
拥有一个类型为 IMultiValueConverter 的关联 Converter
,这个由客户端提供的类实现了转换过程,如下所示
public class TitleConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
string forename = values[0] as string;
string surname = values[1] as string;
return string.Format("{0}, {1}", surname, forename);
}
}
IMultiValueConverter
接口与 IValueConverter
非常相似,只是在这种情况下,一个对象数组被传递给转换器,每个对象按顺序包含我们每个绑定的当前绑定值。
有了这个值转换器,MultiBinding
类就可以检测到两个绑定的变化,然后重新计算绑定到我们 TextBlock
目标属性的 ConvertedValue
。这是我最初的想法,它听起来确实很简单,但实际上并没有初看起来那么容易!
劫持 DataContext
通常,在定义绑定时,我们会省略 Source
属性,例如 {Binding Path=Forename
}。当此表达式代表的 Binding
与一个元素相关联时,绑定源将是目标元素的(可能是继承的)DataContext
。因此,为了允许在 MultiBinding
类上进行绑定,它必须是一个 FrameworkElement
,这为我们提供了 DataContext
属性和 SetBinding()
方法。
然而,存在一个问题;每个元素的 DataContext
都从其在视觉树中的父元素继承。我们的 MultiBinding
不在视觉树中,我们也不希望它在,因此它不会参与 DataContext
继承。我们需要做的是确保当 MultiBinding
所关联的元素上的 DataContext
发生变化时,我们将此 DataContext
“推”到 MultiBinding
上。在 WPF 中,这很容易,FrameWorkElement
暴露了一个 DataContextChanged
事件(对于不公开事件的 DP,总有 DependencyPropertyDescriptor)。然而,在 Silverlight 中,这两种选项都不可用。
我这里的解决方案是创建一个新的附加属性并将其附加到目标元素(在本例中为我们的 TextBlock
),该属性可以“搭便车” DataContext
。下面的代码来自 BindingUtil
类。当 MultiBinding
类作为附加属性与目标元素关联时,我们还绑定其附加的 DataContextPiggyBack
属性。我们定义了一个 static
方法,该方法会在目标元素的 DatatContext
更改时调用,并在其中将新的 DataContext
“推”到 MultiBinding
类。
/// <summary>
/// Invoked when the MultiBinding property is set on a framework element
/// </summary>
private static void OnMultiBindingChanged(DependencyObject depObj,
DependencyPropertyChangedEventArgs e)
{
FrameworkElement targetElement = depObj as FrameworkElement;
// bind the target elements DataContext, to our DataContextPiggyBack property
// this allows us to get property changed events when the targetElement
// DataContext changes
targetElement.SetBinding(BindingUtil.DataContextPiggyBackProperty, new Binding());
}
public static readonly DependencyProperty DataContextPiggyBackProperty =
DependencyProperty.RegisterAttached("DataContextPiggyBack",
typeof(object), typeof(BindingUtil), new PropertyMetadata(null,
new PropertyChangedCallback(OnDataContextPiggyBackChanged)));
public static object GetDataContextPiggyBack(DependencyObject d)
{
return (object)d.GetValue(DataContextPiggyBackProperty);
}
public static void SetDataContextPiggyBack(DependencyObject d, object value)
{
d.SetValue(DataContextPiggyBackProperty, value);
}
/// <summary>
/// Handles changes to the DataContextPiggyBack property.
/// </summary>
private static void OnDataContextPiggyBackChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
FrameworkElement targetElement = d as FrameworkElement;
// whenever the targeElement DataContext is changed, copy the updated property
// value to our MultiBinding.
MultiBinding relay = GetMultiBinding(targetElement);
relay.DataContext = targetElement.DataContext;
}
创建绑定的目标
MultiBinding
类需要一个类型为 Binding
的集合属性。
[ContentProperty("Bindings")]
public class MultiBinding : Panel, INotifyPropertyChanged
{
...
/// <summary>
/// The bindings, the result of which are supplied to the converter.
/// </summary>
public ObservableCollection<Binding> Bindings { get; set; }
...
}
(请注意 ContentProperty
属性的使用,这意味着我们不必在 XAML 中使用属性元素语法显式指定 Binding
集合)。问题是,为了评估这些绑定,它们需要绑定到一个目标属性。我们可以向 MultiBinding
添加一些“虚拟”属性,例如 PropertyOne
、PropertyTwo
等,并将它们绑定到这些属性,但是这种方法很麻烦且受限。
这里的解决方案是使 MultiBinding
成为一个 Panel
,这样它就可以有子元素,每个子元素都会继承其 DataContext
。当 MultiBinding
类在附加时初始化时,会调用 Initialize
方法。此方法会为每个绑定创建一个 BindingSlave
实例,BindingSlave
是一个简单的 FrameworkElement
子类,具有一个 Value
属性,当此属性更改时会引发 PropertyChanged
事件。
/// <summary>
/// Creates a BindingSlave for each Binding and binds the Value
/// accordingly.
/// </summary>
internal void Initialize()
{
foreach (Binding binding in Bindings)
{
BindingSlave slave = new BindingSlave();
slave.SetBinding(BindingSlave.ValueProperty, binding);
slave.PropertyChanged += new PropertyChangedEventHandler(Slave_PropertyChanged);
Children.Add(slave);
}
}
当一个 slave 属性更改时,MultiBinding
事件处理程序会获取当前的绑定值,并使用它们重新评估转换器。
/// <summary>
/// Invoked when any of the BindingSlave's Value property changes.
/// </summary>
private void Slave_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
List<object> values = new List<object>();
foreach (BindingSlave slave in Children)
{
values.Add(slave.Value);
}
ConvertedValue = Converter.Convert(values.ToArray(), typeof(object), ConverterParameter,
CultureInfo.CurrentCulture);
}
ConvertedValue
属性绑定到目标元素的属性,并且会更新以反映此更改。
这种方法与 Josh Smith 在他关于 Virtual Branches 概念的 CodeProject 文章中描述的方法非常相似。MultiBinding
和 BindingSlave
实例可以被视为我们视觉树的一个虚拟分支。
下载源代码
您可以在此处下载此项目的完整源代码。
关于 MVVM 的最后说明
MVVM 模式在 Silverlight 和 WPF 应用程序开发中非常流行。使用此模式,视图的 DataContext
会绑定到您的视图模型。在这种模式下,可以消除对多重绑定的需求(Josh Smith 甚至提出了移除值转换器的概念)。在我们的示例中,PersonViewModel
类将简单地公开一个 Title
属性,该属性执行与 TitleConverter
相同的功能。那么,这是否使我的技术完全过时了?
我不这么认为。虽然 MVVM 是一个很好的模式,但有时添加应用程序的另一个层可能是不可取的,尤其是在您的主要目标是简单性的时候。此外,我不喜欢仅仅因为框架本身缺乏功能就被迫使用特定的模式。MVVM 模式非常适合构建可皮肤化的应用程序并允许 UI 单元测试,但是,如果我不需要这些功能,我宁愿不使用 MVVM。
此致,
Colin E.