65.9K
CodeProject 正在变化。 阅读更多。
Home

Silverlight 5 中的 MultiBinding

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (23投票s)

2011年11月18日

CPOL

12分钟阅读

viewsIcon

158266

downloadIcon

3235

Silverlight 5 的增强型 MultiBinding 标记扩展实现,支持可绑定的 Converter、ConverterParameter 和 StringFormat

Screenshot from Silverlight Multibinding Demo App

引言

本文介绍了一个适用于 Silverlight 5 的 MultiBinding 实现,它能够将多个源的值聚合到一个目标依赖属性。与 WPF 不同,Silverlight 开箱即用地只支持单值绑定,但得益于 Silverlight 5 中引入的自定义标记扩展支持,可以编写一个具有与 WPF 版本 MultiBinding 相似语法和功能的 MultiBinding 实现。

MultiBinding 标记扩展支持所有类型的源绑定,包括数据上下文敏感型绑定以及带有命名元素和 RelativeSource 的绑定。它还支持双向绑定,即从单个目标值转换回多个源值。它还可以通过样式应用于多个元素。在某些方面,此 Silverlight 版本扩展了 WPF 的 MultiBinding

  1. 可以通过 XAML 标记属性语法 ({z:MultiBinding}) 声明源绑定,作为在集合中使用元素语法 (<z:MultiBinding>) 指定源绑定的补充。
  2. MultiBinding ConverterConverterParameter StringFormat 属性可以使用 Bindings 动态绑定到任意源。
  3. 源不限于 BindingsString 常量、XAML 对象、StaticResource 和其他标记扩展可以用作单个源。
  4. 在双向绑定中,当用户提供的 IMultiValueConverter.ConvertBack 实现抛出异常时,可以使用普通的 Silverlight 验证机制。
  5. 实现单值 IValueConverter 的转换器也可以与此 MultiBinding 一起使用,使其在运行时 ConverterConverterParameter StringFormat 发生变化时,对单源绑定也很有用。

背景

与普通的 Binding 不同,MultiBinding 允许将多个源绑定到单个依赖属性。一个常见的用法是呈现一个包含来自多个源的值的自定义文本。对于这种情况,MultiBinding 提供了一个 StringFormat 属性来定义带有源值占位符的格式。但其用途不限于文本。可以使用自定义的 IMultiValueConverter 来指定如何组合源值。

在 WPF 中,必须在 XAML 元素语法中定义 MultiBinding,如下所示:

<TextBlock>
    <TextBlock.Text>
        <MultiBinding StringFormat="{}{0} {1}" >
            <Binding Path="FirstName" />
            <Binding Path="LastName" />
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

对于 Silverlight 3 和 4,Colin Eberhardt 提出了一种基于附加属性的 MultiBinding 解决方案(请参阅 此链接此链接)。相比之下,这里提出的 Silverlight 5 解决方案支持标记扩展语法,可绑定的 ConverterConverterParameter StringFormat 属性,并完全支持带有 ElementName RelativeSources 的源绑定。此外,可绑定的属性不限于 System.Windows.Control 命名空间中的属性。另一方面,由于 MultiBinding 使用了 Silverlight 特有的 IMarkupValue<T> 接口,因此不能直接在 WPF 中使用下面介绍的 MultiBinding 解决方案。

在撰写本文时,ntg123 发布了另一个 Silverlight MultiBinding 解决方案。有关更多信息,请参阅文章 Silverlight Multi-Binding

在使用 Model-View-ViewModel 模式 (MVVM) 时,MultiBinding 通常不是必需的。多个(模型)源的聚合可以在 ViewModel 属性中完成。但必须注意,每当源值发生更改时,都要确保为聚合属性进行更改通知。尽管 MVVM 也产生了更易于单元测试的代码,但有时如果使用绑定到视图或直接到模型的方式可以更简单地达到相同的结果,则不希望或不需要使用此模式。我鼓励您思考下面 MultiBinding 用例的替代解决方案,例如使用 MVVM 模式。

Using the Code

具有文本格式的简单模型绑定

如果我们有一个模型或 ViewModel 对象,其 FirstNameLastName 属性作为数据上下文,我们可以使用 MultiBinding 以这种方式显示全名:

<TextBlock Text="{z:MultiBinding Source1={Binding FirstName}, 
    Source2={Binding LastName}, StringFormat='%1, %0' }" />    

每当 FirstNameLastName 被修改时,文本都会更新,前提是模型对象支持更改通知。MultiBinding 支持在属性中指定最多 5 个源,因此只要源的数量不大于此,就可以使用简单的 XAML 属性语法。当源的数量增加或某些属性无法在属性语法中指定时,可以使用 XAML 元素语法,如下一个示例所示,我们在列表框数据模板中使用 MultiBinding 显示 Persons 的全名。用户可以在组合框中选择名称格式。

<ComboBox Name="cboNameFormat" Width="180" SelectedValuePath="Tag" >
     <ComboBoxItem Tag="%0 %1" IsSelected="True">First and last name</ComboBoxItem>
     <ComboBoxItem Tag="%1, %0">Last name, first name</ComboBoxItem>
</ComboBox>

<ListBox ItemsSource="{Binding Persons}" >
   <ListBox.ItemTemplate>
      <DataTemplate>
        <TextBlock>
           <TextBlock.Text>
              <local:MultiBinding StringFormat=
        "{Binding SelectedValue, ElementName=cboNameFormat}" >
                <local:BindingCollection>
                   <Binding Path="FirstName" />
                   <Binding Path="LastName" />
                </local:BindingCollection>
            </local:MultiBinding>
          </TextBlock.Text>
        </TextBlock>
     </DataTemplate>
   </ListBox.ItemTemplate>
</ListBox>

如上所示,此 MultiBinding 支持在 StringFormat 中使用 %n 作为值占位符,而不是普通的 {n} 语法,因为后者在 XAML 中使用时需要大量转义。与 WPF 版本相比,另一个重要区别是 StringFormat 也可以作为绑定进行指定。在上面的代码片段中,我们将 StringFormat 直接绑定到 combobox 选中的值。更改选择将影响 listbox 中所有名称的格式,如文章顶部的截图所示。

与 WPF 版本不同,StringFormat 始终应用,无论目标属性类型如何。原始 WPF MultiBinding 仅在目标属性为 String 类型时应用 StringFormat,这意味着它不能直接用于 ContentControl 派生的内容。

使用自定义 IMultiValueConverter

除了简单的 string 格式化,Silverlight MultiBinding 还允许您提供一个自定义转换器,该转换器负责将源值转换为单个目标值。自定义转换器必须实现 IMultiValueConverter 接口,这与 WPF 版本相同。

在下面的示例中,我们使用 MultiBinding 在选中两个复选框时启用一个按钮。

<CheckBox Name="chkLicenceAccepted" >I have accepted the Licence</CheckBox>
<CheckBox Name="chkConditionsAccepted" >I have accepted the Conditions</CheckBox>
<Button Content="Continue" >

    <Button.IsEnabled>
       <local:MultiBinding
          Source1="{Binding IsChecked, ElementName=chkLicenceAccepted}"
          Source2="{Binding IsChecked, ElementName=chkConditionsAccepted}"
          Converter="{local:MinNumberOfSetConverter}"
          ConverterParameter="2"
       />
    </    </Button.IsEnabled>
</Button>

如上所示,MultiBinding 支持带有 ElementNames 的源绑定。RelativeSource 也受到支持,允许通过使用 Self 或视觉树中的祖先(Silverlight 5 新增功能)来引用目标元素(使用 AncestorType)。

下面显示了 berOfSetConverter 的实现。我们这里只需要一个逻辑 AND 转换器,但为了在许多情况下有用,我将其写得更通用,支持指定需要返回 true 的“已设置”(“true”)源值的数量。

public classpublic class MinNumberOfSetConverter : MarkupExtension, IMultiValueConverter
{
   public object Convert(object[] values, Type targetType,
       object parameter, System.Globalization.CultureInfo culture)
   {
      int minNumberOfSet;
      // The parameter is minimum number of values to be set.
      // If not set all input values must be set to return true.
      if( !int.TryParse(parameter as String, out minNumberOfSet) ) 
                minNumberOfSet = values.Length;
      int numberOfSet = 0;
      for (int i = 0; i < values.Length; i++)
      {
         bool? boolValue = values[i] as bool?;
         if (boolValue.GetValueOrDefault()) numberOfSet++;
         if (numberOfSet >= minNumberOfSet) return true;
      }
      return numberOfSet >= minNumberOfSet;
   }

   public object[] ConvertBack(object value, Type[] targetTypes, 
        object parameter, System.Globalization.CultureInfo culture)
   {
      throw new NotSupportedException();
   }

   public override Object ProvideValue(IServiceProvider serviceProvider)
   {
      return this;
   }
}

为了支持方便的标记扩展语法,转换器从 MarkupExtension 派生,但这并非转换器的要求。为了在多个位置重用相同的转换器,可以将其定义为资源并使用 StaticResource 进行引用。

从目标到源值的转换

此 Silverlight MultiBinding 还支持双向绑定,通过 ConverterConvertBack 方法从单个值到多个值进行转换,类似于 WPF 的对应项。在下面的示例中,我们在文本中显示长度和单位,允许用户在同一个字段中修改两者。当用户修改文本时,自定义转换器会将数量和单位拆分到其原始字段中。

<TextBlock Text="Length: " />
<TextBox Width="100" BindingValidationError="TextBox_BindingValidationError" >
  <TextBox.Text>
     <local:MultiBinding Mode="TwoWay"
                         NotifyOnValidationError="True"
                         ValidatesOnExceptions="true"
                         Converter="{local:LengthConverter}"
                         Source1="{Binding Length, Mode=TwoWay, 
                ValidatesOnExceptions=True}"
                         Source2="{Binding LengthUnit, Mode=TwoWay}"  />
  </TextBox.Text>
</TextBox>

要启用转换回,Mode 必须设置为 TwoWay,并应用于 MultiBinding 源绑定。转换器还必须实现 IMultiValueConverter.ConvertBack 方法。在上面的示例中,LengthConverter 的实现如下所示:

public object[] ConvertBack(object value, Type[] targetTypes,
    object parameter, System.Globalization.CultureInfo culture)
{
    string lengthWithUnit = value as string;
    if (lengthWithUnit != null)
    {
        object[] result = new object[2];
        string[] parts = lengthWithUnit.Split(new Char[] { ' ' }, 
            StringSplitOptions.RemoveEmptyEntries);
        result[0] = Double.Parse(parts[0].ToString());
        if (parts.Length > 1)
        {
            result[1] = parts[1].Trim();
        }
        return result;
    }
    return null;
}

ConvertBack 预计返回一个 Object[] 数组,其中包含每个源的值。输入参数 targetType 指示类型(基于当前值)。如果 ConvertBack 返回 null,则不会更新任何源。如果结果数组中的某个项设置为 DependencyProperty.UnsetValue,则不会更新相应的源。如果返回的数组比源的数量短,则仅更新第一个源。

如果 ConvertBack 方法抛出异常,它将在目标元素上设置验证错误,但前提是 ValidatesOnExceptions 属性设置为 true。就像普通的 Bindings 一样,您也可以将 NotifyOnValidationError 属性设置为 true,以便在验证错误状态更改时引发冒泡的 BindingValidBindingValidationError 事件。为了在设置单个源时获得验证错误,您也可以在各个源绑定上设置 ValidatesOnExceptions 或其他验证相关属性。

工作原理

要使用 MultiBinding,您不必阅读本节,但如果您对实现细节和我在实现过程中遇到的挑战感兴趣,请继续阅读。

自定义绑定标记扩展

当标记扩展应用于属性时,会调用其 ProvideValue 方法。由于 BindingBaseBinding 具有密封的 ProvideValue 方法,因此无法通过简单继承来扩展绑定机制。相反,我们可以创建一个新的 MarkupExtension 派生类,并让其 ProvideValue 返回一个内部、以编程方式创建的 Binding 实例的结果。对于大多数目标属性,BindingProvideValue 返回 BindingExpression,但对于样式设置器值,它返回 Binding 本身。

对于 MultiBinding ,每次应用 MultiBinding 时都会创建一个新的内部 Binding 实例,即调用 ProvideValue。此绑定的源是 MultiBindingExpression 实例的 SourceValues 属性。与 BindingExpression 一样,为每个绑定目标实例创建一个 MultiBindingExpression

public class MultiBinding : DependencyObject, IMarkupExtension<Object>
{
...
    public object ProvideValue(IServiceProvider serviceProvider)
    {
        // Some error checking code not shown in article text.
        IProvideValueTarget pvt = serviceProvider.GetService
        (typeof(IProvideValueTarget)) as IProvideValueTarget;

        DependencyObject target = pvt.TargetObject as DependencyObject;

        Binding resultBinding = ApplyToTarget(target);

        return resultBinding.ProvideValue(serviceProvider);
    }

    private Binding ApplyToTarget(DependencyObject target)
    {
        Seal();

        // Create new MultiBindingExpression to hold information about this multibinding
        MultiBindingExpression newExpression = new MultiBindingExpression(target, this);

        // Create new binding to expressions's SourceValues property
        PropertyPath path = new PropertyPath("SourceValues");
        Binding resultBinding = new Binding();
        resultBinding.Path = path;
        resultBinding.Source = newExpression;
        resultBinding.Converter = newExpression;
        resultBinding.Mode = Mode;
        resultBinding.UpdateSourceTrigger = UpdateSourceTrigger;
        resultBinding.TargetNullValue = TargetNullValue;
        resultBinding.ValidatesOnExceptions = ValidatesOnExceptions;
        resultBinding.NotifyOnValidationError = NotifyOnValidationError;
        resultBinding.ConverterCulture = ConverterCulture;
        resultBinding.FallbackValue = FallbackValue;
        return resultBinding;
    }
}

MultiBindingExpression.SourceValues 属性保存来自参与绑定的未转换源值。将源值聚合到最终目标值的过程涉及调用任何用户提供的 IMultiValueConverter 实现,并根据 StringFormat 设置进行格式化。这在 MultiBindingExpression.Convert 方法中完成,该方法实现了标准的单值 IValueConverter 接口。如上面的代码片段所示,MultiBindingExpression newExpression 被设置为内部绑定的 Converter,而不仅仅是 Source,以实现此设置。在第一个实现中,聚合是在设置 SourceValues 属性之前完成的,但为了能够使用绑定文化和目标类型感知,我认为让 MultiBindingExpression 实现 IValueConverter 并将其用作源的转换器更好。

隐藏的附加属性以支持相对于目标的绑定

另一个挑战是支持所有类型的相对于目标的源绑定,包括数据上下文、命名元素和 RelativeSource。在第一个实现中,这些绑定被传输并由 MultiBindingExpression 实例管理,每次应用 MultiBinding 时都会创建一个。MultiBindingExpression 然后派生自 FrameworkElement 以支持绑定和 DataContext。通过将 MultiBindingExpressionDataContext 绑定到目标的 DataContext,可以支持相对于目标数据上下文的 binding。这类似于 Josh Smith 称为“虚拟树”的方法。但是,使用此方法支持 ElementNameRelativeSource 要困难得多,因为 MultiBindingExpression 不是视觉树的真正一部分。Colin Ebenhardt 已展示了一种支持 ElementName 的方法,但我选择了另一种基于附加属性的解决方案,该属性附加到 MultiBinding 目标对象。这些附加数据属性归 MultiBindingExpression 所有,并在创建并应用于目标元素的 MultiBindingExpression 实例时设置。然后,MultiBinding 中的所有源绑定都会重新应用于附加数据属性,将它们置于正确的上下文中以使用相对于 DataContext、命名元素和 RelativeSource 的绑定。如果 XAML 代码在应用 MultiBinding 后保存,我们将看到数据属性,如下面的代码片段所示,其中 D0 D1 是数据属性的名称,MBE 是 MultiBindingExpression 的缩写。

<TextBlock Text="{z:MultiBinding Source1={Binding FirstName} 
    Source2={Binding LastName} StringFormat='%0 %1'}"
           MBE.MultiBindingExpression="..." MBE.D0="{Binding FirstName}" 
    MBE.D1="{Binding LastName}" /> 

为了支持对同一目标的任意数量的 MultiBindings 以及单个 MultiBinding 上的任意数量的源绑定,附加数据属性会根据需要动态创建。它们对设计器隐藏,因为它们没有任何 getter 或 setter。为了跟踪附加数据属性和 multibinding 之间的映射,在目标上设置了一个额外的隐藏 MultiBindingExpressions 附加属性。它设置为应用于目标元素的 MultiBindingExpressions 列表。每个 MultiBindingExpression 包含 private 成员字段,用于跟踪哪个数据属性对应于哪个源属性。数据属性也用于存储对 ConverterConverterParameter StringFormat 的任何绑定,允许它们也可以相对于目标元素进行指定。每当源绑定值发生更改时,就会调用为所有数据属性注册的公共属性更改回调方法。此回调方法将启动一个请求来更新 SourceProperty。此更新将异步发生,方法是将请求发布到目标的调度程序,以允许在聚合结果之前更改多个绑定值,例如在数据上下文更改时。

一个派生自 DependencyObject 的标记扩展以支持绑定

为了支持为 MultiBinding 标记扩展属性指定绑定,MultiBinding 派生自 DependencyObject。有趣的是,这在 Silverlight 中受支持,但在 WPF 中不受支持,因为在 Silverlight 中,标记扩展可以简单地实现 IMarkupExtension<T> 接口,而它们必须在 WPF 中从直接派生自 Object 的 MarkupExtension 派生。MultiBinding 应用于目标时,来自 MultiBinding 实例的任何绑定都会重新应用于目标上的附加数据属性。通过 DependencyObject.ReadLocalValue 检测绑定,该方法在源标记属性存在绑定时返回 BindingExpression 。从这个 BindingExpression,我们可以获取 ParentBinding 并使用 BindingOperation.SetBinding 将其重新绑定到附加数据属性。

// localValue is the value from DependencyObject.ReadLocalValue(<a MultiBinding property>)

DependencyProperty destProperty = GetDataProperty(dataPropertyIndex);

BindingExpression bindingExpression = localValue as BindingExpression;

if (bindingExpression != null)
{
    Binding propertyBinding = bindingExpression.ParentBinding;
    BindingOperations.SetBinding(Target, destProperty, propertyBinding);
}
else
{
    Target.SetValue(destProperty, localValue);
}

在样式中支持 MultiBindings

另一个挑战是支持在样式中将 MultiBindings 指定为值,就像普通绑定可以在样式中用于在多个地方重用一样:

<Style x:Key="myStyle1" TargetType="TextBox">
    <Setter Property="Text"
            Value="{z:MultiBinding Mode=TwoWay, Source1={Binding Title, Mode=TwoWay}, 
            Converter={StaticResource stringFormatConverter}}" />
    <Setter Property="Tag"
            Value="{z:MultiBinding Source1={Binding Title}, 
            Converter={StaticResource stringFormatConverter}}" />
</Style>

如果没有对样式设置器进行特殊处理,我们将把附加数据属性应用于样式设置器,并创建一个内部绑定到一个 MultiBindingExpression,该表达式将在使用该样式的所有目标元素之间共享,从而导致意外行为。我们想要的是为应用样式的每个元素创建一个新的 MultiBindingExpression。为此,我们必须检测何时将 MultiBinding 应用于设置器值属性。

private static readonly PropertyInfo SetterValueProperty = 
        typeof(Setter).GetProperty("Value");

public object ProvideValue(IServiceProvider serviceProvider)
{

   IProvideValueTarget pvt = serviceProvider.GetService
        (typeof(IProvideValueTarget)) as IProvideValueTarget;

   if (pvt.TargetObject is Setter && pvt.TargetProperty == SetterValueProperty)
   {
      Setter setter = (Setter)pvt.TargetObject;
      ApplyToStyle(setter);
      return this;
   }
      …
}
private void ApplyToStyle(Setter setter)
{
    // Save the original Setter property for later use...
    m_styleProperty = setter.Property;

    // ... and replace it with an internal attached property.
    setter.Property = styleMultBindingProperties[currentStyleMultiBindingIndex];
    currentStyleMultiBindingIndex = (currentStyleMultiBindingIndex + 1) 
                    % MaxStyleMultiBindingsCount;
    Seal();
}

我们在上面的 ApplyToStyle 中所做的是用内部附加属性替换设置器的原始 Property 属性。原始设置器属性存储在 m_styleProperty 中。当应用样式并设置附加属性时,为附加属性注册的属性更改回调会将 multibinding 应用于样式设置器中最初设置的属性。

// Called when one of the styleMultiBindingProperties are changed
private static void OnStyleMultiBindingChanged
    (DependencyObject d, DependencyPropertyChangedEventArgs args)
{
   MultiBinding mb = (MultiBinding)args.NewValue;
   if (mb != null)
   {
     // Only apply multibinding from Style if no local value has been set.
     object existingValue = d.ReadLocalValue(mb.m_styleProperty);
     if (existingValue == DependencyProperty.UnsetValue)
     {
       // Ap       // Apply binding to target by creating
       // MultiBindingExpression and setting attached data properties.
        Binding resultBinding = mb.ApplyToTarget(d);
        // Set binding on the property originally defined by the style setter.
        BindingOperations.SetBinding(d, mb.m_styleProperty, resultBinding);
     }
  }
}

为了在同一样式中支持多个 MultiBindings 用于不同属性,我们需要为每个属性使用不同的样式 multibinding 附加属性。这就是为什么我们有一个属性数组(在 static 类构造函数中创建,未在上文显示)。为了避免为每个设置器创建新的附加属性,我们以轮循方式重用它们。因此,同一 Style 中具有 MultiBindings 的设置器有一个最大数量,由常量 MaxStyleMultiBindingsCount 定义,当前设置为 10。在同一个样式中应用更多 MultiBindings 的可能性非常小,但如果需要,可以增加此设置。

历史

  • 2011 年 11 月 16 日
    • 使用 Silverlight 5 RC 在 Visual Studio 2010 中开发和测试的初始版本
  • 2014 年 5 月 8 日
    • 版本 1.1,升级到 VS2013 并主要进行修复以避免设计时错误
© . All rights reserved.