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

可绑定转换器参数

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (8投票s)

2012 年 9 月 10 日

CPOL

7分钟阅读

viewsIcon

124909

downloadIcon

4599

一种在WPF的XAML中实现可绑定转换器参数的简单技术。

 

引言

绑定是WPF引入的最强大的功能之一,它允许我们使用最少的声明式代码(XAML)来实现复杂的基于数据的UI,同时依赖于一个“内置的”、即插即用的机制,该机制以直接的方式或通过转换器将数据连接到UI控件。

绑定转换器本质上是封装的函数,它们接收绑定值,以特定的方式对其进行操作,并返回操作后的结果。在许多情况下,转换器需要另一份信息来源才能“完成其工作”,这份来源被称为“ConverterParameter”,它是转换器的ConvertConvertBack方法参数之一。

WPF的开箱即用的绑定将ConverterParameter限制为只能从XAML接收静态值或资源值,而传递绑定表达式是被禁止的。

//
// this line of xaml produce the following error:
//
<TextBox Text="{Binding Path=FirstName,Converter={StaticResource TestValueConverter},ConverterParameter={Binding Path=ConcatSign}}" />

类型为“Binding”的“ConverterParameter”属性上不能设置“Binding”。“Binding”只能设置在DependencyObject的DependencyProperty上。

在许多情况下,这种功能(可绑定的ConverterParameter)正是人们所需要的。也就是说,ConverterParameter的值(一个绑定表达式,而不是一个静态预定义值)将在每次触发绑定评估时进行评估,并作为ConverterParameter参数传递给转换器。

在本文中,我将展示一种相对简单明了的技术来实现这一点。实现绑定ConverterParameter的XAML部分将尽可能接近上面产生错误的行,但它会起作用…… 

可绑定转换器参数 Take-3  

更新

  • 29/6/13
    - 添加功能:元素名称绑定 
    - Bug修复:内存泄漏 

这是我第三次尝试来实现一个健壮、简单的解决方案来解决这个非常普遍的障碍。

主要目标

将先前尝试中描述的概念锻造成真实世界、健壮的解决方案。

支持的功能和优点

  1. 可绑定的转换器参数(包括绑定、静态值、枚举*)
    *
    有更好的方法来实现基于枚举的绑定,在BcpBinding Markup-Extension类中添加对ConverterParameter枚举的支持,是为了展示其处理新的、不同类型数据的能力,同时只进行少量的代码修改。
  2. 多重绑定支持
  3. 双向绑定支持
  4. 可读、易于理解的XAML
    <!--THIS SAMPLE SHOWS SINGLE-BINDING WITH BINDED/MIXED ConverterParameters SYNTAX-->
    <TextBox >
    
        <TextBox.Text >
            <local:BcpBinding Path="FirstName" 
               ConverterParameters="Binding Path=ConcatSign,Binding Path=ConcatSign2" 
               Converter="{StaticResource TestValueConverterWithTwoObjectsParameter}" 
               Mode="TwoWay"/>
        </TextBox.Text>
        <TextBox.Tag >
            <local:BcpBinding Path="FirstName" 
              ConverterParameters="Binding Path=ConcatSign,Binding Path=ConcatSign2,[some not binded value]" 
              Converter="{StaticResource TestValueConverterWithThreeObjectsParameter}" />
        </TextBox.Tag>
    </TextBox>
    <!--THIS SAMPLE SHOWS MULTI-BINDING WITH BINDED ConverterParameters SYNTAX-->
    <TextBox >
        <TextBox.Text >
            <local:BcpBinding 
                   Converter="{StaticResource MultiValueMultiConverterParameterConverter}" 
                   Mode="TwoWay" >
                Binding Path=FirstName
                Binding Path=LastName
                <local:BcpBinding.ConverterParameters>
    
                    Binding Path=ConcatSign
                    Binding Path=ConcatSign2
                    Binding Path=AllConcatSigns
                </local:BcpBinding.ConverterParameters>
            </local:BcpBinding>
        </TextBox.Text>
    </TextBox>
    
    <!--THIS SAMPLE SHOWS MULTI-BINDING WITH MIXED ConverterParameters SYNTAX-->
    <TextBox >
    
        <TextBox.Text >
            <local:BcpBinding Converter="{StaticResource MultiValueMultiMixedConverterParameterConverter}" 
                  Mode="TwoWay" 
                  ConverterParameters="Binding Path=ConcatSign,Binding 
                    Path=ConcatSign2,[some not binded value],Binding Path=AllConcatSigns">
                Binding Path=FirstName
                Binding Path=LastName
            </local:BcpBinding>
        </TextBox.Text>
    </TextBox>
  5. 绑定的ConverterParameter更改不会触发绑定的重新评估(包括转换器)。
    依我看,绑定的ConverterParameter更改不应该触发其所属的绑定。
    如果我们想要这种行为,我们会将其作为另一个绑定放在一个已经存在的、开箱即用的MultiBinding中。
    在我看来,ConverterParameter只是转换器执行其工作所需的额外数据(无论是静态值,还是从绑定表达式计算出的值)。
    我所知道的唯一一种场景是,我们希望一个绑定触发一个“父绑定”并且也作为ConverterParameter,那就是在MultiValueConverter的ConvertBack情况下。在这种情况下,我们期望从单个值中提取多个值,并且可能需要原始值,而ConvertBack的值就由这些值组成。
    在这种情况下,我将使用如下语法
    <TextBox >
        <TextBox.Text >
            <local:BcpBinding Converter="{StaticResource some-converter}" Mode="TwoWay" 
                ConverterParameters="Binding Path=FirstName,
                  Binding Path=LastName">
    
                Binding Path=FirstName
                Binding Path=LastName 
            </local:BcpBinding>
        </TextBox.Text>
    </TextBox>  
  6. 绑定到索引器(例如,SomeIndexer[])支持。在绑定和转换器参数绑定中都支持。

注释

  • 由于此解决方案基于之前的解决方案,因此我在此部分(TAKE3)省略了说明。我在代码中添加了注释。
  • 绑定语法是弱类型的,因为绑定和转换器参数作为字符串传递给Markup-Extension。
  • 我省略了代码中的错误验证,因为它们会增加额外的代码,使实现解决方案思路的相关代码变得模糊。

# TAKE3 结束 

可绑定转换器参数 Take-2 

这是我第二次尝试来实现一个健壮、简单的解决方案来解决这个非常普遍的障碍。

主要目标: 

  1. XAML代码应尽可能接近预期代码。
  2. 简单且简洁。
  3. 对现有代码(XAML和整体项目)的影响最小。
  4. 支持每个元素无限数量的可绑定转换器参数绑定。
  5. 支持双向绑定(针对具有可绑定转换器参数的绑定)。 

解决方案原则

  1. 允许用户以尽可能接近所需(不可用形式)的形式编写XAML。我选择了这种方式:
  2. <TextBox Text="{Binding Path=FirstName,Converter={StaticResource 
      TestValueConverter},ConverterParameter='Binding Path=ConcatSign',Mode=TwoWay}" />
  3. 创建一个外部机制,在“幕后”进行拼接,以实现所需的功能。

解决方案本身

  1. 像往常一样编写XAML,每当需要应用“可绑定转换器参数”时,请按上面代码所示编写。
  2. 对于具有这些特殊绑定类型的元素,请在代码隐藏中注册_Initialized事件(对于那些MVVM狂热者,即在任何情况下都放弃使用代码隐藏,或者在没有代码隐藏的情况下,建议使用Behavior)。
  3. 在_Initialized事件处理程序中执行以下操作:
  • 获取Initialized-Element范围内的所有DependencyObjects。
  • 对于每个DependencyObject,获取所有具有这种特殊绑定语法的DependencyProperties。
  • 对于每个DependencyProperty,执行以下操作(请参阅代码注释):
  • // 1. get original binding 
    Binding bindingOrig = BindingOperations.GetBinding(item, dp);
    
    //create set of attached properties to replase this ConverterParameter Binding 
    //give unique name by using dp's name:
    DependencyProperty apBindingSource=null;
    
    //(5.) another attached prop for two-way binding operations
    DependencyProperty apIsSourceChanged = 
       DependencyProperty.RegisterAttached(dp.Name + "IsSourceChanged", typeof(bool), 
       item.GetType(), new PropertyMetadata(false));
       
    // 1. attached-prop for the ConverterParameter-Binding
    
    DependencyProperty apConverterParameterBindingSource = 
       DependencyProperty.RegisterAttached(dp.Name + "ConverterParameterBindingSource", 
       typeof(object), item.GetType(), new PropertyMetadata(null)); 
    Binding bindingConverterParameterBindingSource = new Binding(sConverterParameterBindingPath);
    BindingOperations.SetBinding(item, apConverterParameterBindingSource, 
                                 bindingConverterParameterBindingSource);
    
    // 2. attached-prop to hold the Converter Object
    DependencyProperty apConverter = DependencyProperty.RegisterAttached(dp.Name + 
      "Converter", typeof(IValueConverter), item.GetType(), new PropertyMetadata(null));
    (item).SetValue(apConverter, bindingOrig.Converter);
    
    //3. attached-prop to hold the evaluate result - will be binded to the original Binded dp
    
    DependencyProperty apEvaluatedResult = DependencyProperty.RegisterAttached(dp.Name + 
      "EvaluatedResult", typeof(object), item.GetType(), new PropertyMetadata(null, (s, edp) =>
    {
        if (!(bool)s.GetValue(apIsSourceChanged))
        {
            // change didn't come from source - target got changed in two-way binding
            // change source via convert back
            object ret= (s.GetValue(apConverter) as IValueConverter).
            ConvertBack(edp.NewValue, null, s.GetValue(apConverterParameterBindingSource), null);
            s.SetValue(apBindingSource, ret);
        }
    }));
    
    // 4. attached-prop to replace the source binding - bind apBindingSource
    // to the original source (instead of the original dp)
    apBindingSource = DependencyProperty.RegisterAttached(dp.Name + 
      "BindingSource", typeof(object), item.GetType(), new PropertyMetadata(null, (s, edp) =>
    {
      s.SetValue(apIsSourceChanged, true);
      s.SetValue(apEvaluatedResult, (s.GetValue(apConverter) as IValueConverter).Convert(
       edp.NewValue, null, s.GetValue(apConverterParameterBindingSource), null));
      s.SetValue(apIsSourceChanged, false);
    }));
    Binding NewBindingToSource = new Binding(bindingOrig.Path.Path); 
    NewBindingToSource.Mode =bindingOrig.Mode;
    // reroute source to apBindingSource
    BindingOperations.SetBinding(item, apBindingSource, NewBindingToSource);

基本上,我们正在用一组唯一命名的附加属性替换“特殊”绑定。然后将原始绑定的路由重定向到这些附加属性。以下草图将尝试对此进行说明(ap=AttachedProperty):

目标 <---绑定---> **源** 使用(转换器参数 <---绑定---> 转换器参数源)通过转换器

变为

目标 <---绑定---> Result_ap <---更新--- (Source_ap <---绑定---> **源**) 使用(ConverterParameter_ap <---绑定---> 转换器参数源)通过Converter_ap 

# TAKE2 结束 

注意事项

以下部分将描述我针对手头问题的解决方案。由于此解决方案相当简单,我选择不使其比实际需要更复杂。因此,例如,我选择在Multi-Binding解决方案中使用预定义的绑定集,而不是添加一个复杂的机制来模糊我希望传达的主要思想。此解决方案是我在文章《Silverlight Multi-Binding》中已经展示的一种技术的变体,并进行了一些WPF和上下文相关的修改和调整。

使用代码

已知事实

  • 绑定“自然地”“生活”在框架内,它们会响应相关的PropertyChanged事件来重新评估自身。
  • 附加属性可以具有绑定。
  • 附加属性依赖属性的子集,因此它们内置了属性更改处理机制。
  • 自定义Markup Extension可以模仿(模拟)其他类型的Markup Extension,称为“Binding”。

所需材料

对于此解决方案,我们将需要以下类:

  • CustomBindingUtil.cs:此类将包含一组附加属性,这些属性将被附加到我们希望具有绑定的Framework元素上。此外,它将在响应绑定/更改时执行实际的值生成。我将此类分为三个区域:Single-Binding、Multi-Binding、Shared。
  • #region Single Binding Attached-Properties
    
    public static object GetSingleBinding(DependencyObject obj)
    {
        return (object)obj.GetValue(SingleBindingProperty);
    }
    
    public static void SetSingleBinding(DependencyObject obj, object value)
    {
        obj.SetValue(SingleBindingProperty, value);
    }
    
    // Using a DependencyProperty as the backing store for SingleBinding.
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SingleBindingProperty =
        DependencyProperty.RegisterAttached("SingleBinding", typeof(object), 
        typeof(CustomBindingUtil), new PropertyMetadata(null, SingleBindingChanged));
    
    private static void SingleBindingChanged(DependencyObject obj, 
            DependencyPropertyChangedEventArgs e)
    {
        try
        {
            object convparam = obj.GetValue(ConverterParameterProperty);
            object binding = obj.GetValue(SingleBindingProperty);
            obj.SetValue(BResultProperty, (obj.GetValue(ConverterProperty) 
                as IValueConverter).Convert(binding, null, convparam, null));
        }
        catch (Exception ex)
        {
            throw;
        }
    }
    
    public static object GetBResult(DependencyObject obj)
    {
        return (object)obj.GetValue(BResultProperty);
    }
    
    public static void SetBResult(DependencyObject obj, object value)
    {
        obj.SetValue(BResultProperty, value);
    }
    
    // Using a DependencyProperty as the backing store for BResult.
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty BResultProperty =
        DependencyProperty.RegisterAttached("BResult", typeof(object), 
        typeof(CustomBindingUtil), new UIPropertyMetadata(null));
    
    
    #endregion
    
    #region Multi Binding Attached-Properties
    #region Predefined Bindings
    
    
    public static Object GetBinding1(DependencyObject obj)
    {
        return (Object)obj.GetValue(Binding1Property);
    }
    public static void SetBinding1(DependencyObject obj, Object value)
    {
        obj.SetValue(Binding1Property, value);
    }
    // Using a DependencyProperty as the backing store for Binding1.
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty Binding1Property =
        DependencyProperty.RegisterAttached("Binding1", typeof(Object), 
        typeof(CustomBindingUtil), new PropertyMetadata(null, BindingChanged));
    
    public static Object GetBinding2(DependencyObject obj)
    {
        return (Object)obj.GetValue(Binding2Property);
    }
    public static void SetBinding2(DependencyObject obj, Object value)
    {
        obj.SetValue(Binding2Property, value);
    }
    // Using a DependencyProperty as the backing store for Binding2.
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty Binding2Property =
        DependencyProperty.RegisterAttached("Binding2", typeof(Object), 
        typeof(CustomBindingUtil), new PropertyMetadata(null, BindingChanged));
    
    
    public static Object GetBinding3(DependencyObject obj)
    {
        return (Object)obj.GetValue(Binding3Property);
    }
    public static void SetBinding3(DependencyObject obj, Object value)
    {
        obj.SetValue(Binding3Property, value);
    }
    // Using a DependencyProperty as the backing store for Binding3.
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty Binding3Property =
        DependencyProperty.RegisterAttached("Binding3", typeof(Object), 
        typeof(CustomBindingUtil), new PropertyMetadata(null, BindingChanged));
    
    #endregion
    
    /// <summary>
    /// update result property with converted (& Weighted) all available sub-binding results
    /// </summary>
    /// <param name="obj"></param>
    /// <param name="e"></param>
    
    private static void BindingChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        object[] Values = new Object[] { obj.GetValue(Binding1Property), 
                 obj.GetValue(Binding2Property), obj.GetValue(Binding3Property) };
        try
        {
            object convparam = obj.GetValue(ConverterParameterProperty);
            obj.SetValue(MBResultProperty, (obj.GetValue(MultiConverterProperty) 
                as IMultiValueConverter).Convert(Values, null, convparam, null));
        }
        catch (Exception ex)
        {
            throw;
        }
    }
    
    public static IMultiValueConverter GetMultiConverter(DependencyObject obj)
    {
        return (IMultiValueConverter)obj.GetValue(MultiConverterProperty);
    }
    
    public static void SetMultiConverter(DependencyObject obj, IMultiValueConverter value)
    {
        obj.SetValue(MultiConverterProperty, value);
    }
    
    // Using a DependencyProperty as the backing store for MultiConverter. 
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty MultiConverterProperty =
        DependencyProperty.RegisterAttached("MultiConverter", 
        typeof(IMultiValueConverter), typeof(CustomBindingUtil), new UIPropertyMetadata(null));
    
    public static Object GetMBResult(DependencyObject obj)
    {
        return (Object)obj.GetValue(MBResultProperty);
    }
    public static void SetMBResult(DependencyObject obj, Object value)
    {
        obj.SetValue(MBResultProperty, value);
    }
    // Using a DependencyProperty as the backing store for MBResult.
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty MBResultProperty =
        DependencyProperty.RegisterAttached("MBResult", typeof(Object), 
        typeof(CustomBindingUtil), new PropertyMetadata(null));
    
    #endregion
    
    #region Shared Attached-Properties
    
    public static IValueConverter GetConverter(DependencyObject obj)
    {
        return (IValueConverter)obj.GetValue(ConverterProperty);
    }
    public static void SetConverter(DependencyObject obj, IValueConverter value)
    {
        obj.SetValue(ConverterProperty, value);
    }
    // Using a DependencyProperty as the backing store for Converter.
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ConverterProperty =
        DependencyProperty.RegisterAttached("Converter", 
        typeof(IValueConverter), typeof(CustomBindingUtil), new UIPropertyMetadata(null));
    
    public static object GetConverterParameter(DependencyObject obj)
    {
        return (object)obj.GetValue(ConverterParameterProperty);
    }
    
    public static void SetConverterParameter(DependencyObject obj, object value)
    {
        obj.SetValue(ConverterParameterProperty, value);
    }
    
    // Using a DependencyProperty as the backing store for ConverterParameter.
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ConverterParameterProperty =
        DependencyProperty.RegisterAttached("ConverterParameter", 
        typeof(object), typeof(CustomBindingUtil), new PropertyMetadata(null));
    
    #endregion
  • MyBindedParameterBinding.cs - 这是一个自定义Markup-Extension类。其主要目标是模仿已知的{Binding...} Markup-Extension,只是增加了可绑定转换器参数的功能。为此,它在Markup-Extension的ProvideValue覆盖方法中有三个匹配的属性:Binding(类型为Binding)、Converter(类型为IValueConverter)和ConverterParameter(类型为Binding)。
  • 绑定(和属性)被设置到元素匹配的附加属性上,返回值被设置为一个新的绑定的(引用元素CustomBindingUtil.BResultProperty的附加属性)-即ProvideValue方法。

    public Binding Binding { get; set; }
    public IValueConverter Converter { get; set; }
    public Binding ConverterParameter { get; set; }
    
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        try
        {
            IProvideValueTarget pvt = serviceProvider.GetService(
              typeof(IProvideValueTarget)) as IProvideValueTarget;
            DependencyObject TargetObject = pvt.TargetObject as DependencyObject;
    
            BindingOperations.SetBinding(TargetObject, 
              CustomBindingUtil.SingleBindingProperty, Binding);
            BindingOperations.SetBinding(TargetObject, 
              CustomBindingUtil.ConverterParameterProperty, ConverterParameter);
            TargetObject.SetValue(CustomBindingUtil.ConverterProperty, this.Converter);
            Binding b = new Binding();
            b.Source = TargetObject;
            b.Path = new PropertyPath(CustomBindingUtil.BResultProperty);
            return b.ProvideValue(serviceProvider);
        }
        catch (Exception)
        {
    
            throw;
        }
    }
  • MyBindedParameterMultiBinding.cs - 原理与MyBindedParameterBinding.cs相同,并进行了一些额外的调整以支持MultiBinding。处理多个绑定的一个更好(更优雅)的方法是推荐的(请参阅上面的注释)。

机制(对于单次绑定)

从XAML开始

代码非常接近已知的绑定语法。

<TextBox >
    <TextBox.Text >
        <local:MyBindedParameterBinding Converter="{StaticResource TestValueConverter}" 
           Binding="{Binding Path=FirstName}" 
           ConverterParameter="{Binding Path=ConcatSign}"/>

    </TextBox.Text>
</TextBox>

FirstName属性更改时,元素的Binding附加属性会更改(因为它绑定到FirstName)。这将触发(CustomBindingUtil.cs中的)SingleBindingChanged方法,其中BResultProperty(另一个附加属性)被设置为计算值(传递到转换器的Convert方法的值的结果,以及当前计算的ConverterParameter参数)。

private static void SingleBindingChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
    try
    {
        object convparam = obj.GetValue(ConverterParameterProperty);
        object binding = obj.GetValue(SingleBindingProperty);
        obj.SetValue(BResultProperty, (obj.GetValue(ConverterProperty) 
          as IValueConverter).Convert(binding, null, convparam, null));
    }
    catch (Exception ex)
    {
        throw;
    }
}

BResultProperty是我们的自定义Markup-Extension通过绑定返回的内容。

public override object ProvideValue(IServiceProvider serviceProvider)
        ...

        Binding b = new Binding();
        b.Source = TargetObject;
        b.Path = new PropertyPath(CustomBindingUtil.BResultProperty);
        return b.ProvideValue(serviceProvider);
    }
    catch (Exception)
    {

        throw;
    }
}

结论

此处描述的技术可以被任何面临可绑定转换器参数需求的人使用。只需将CustomBindingUtil.csMyBindedParameterBinding.cs或/和MyBindedParameterMultiBinding.cs添加到您的项目中即可。这里展示的示例并不是旨在成为一个健壮、面向生产的代码产品,因为它有很多未解决的问题(例如,当尝试使用多个可绑定转换器参数时),尽管如此,它可以得到扩展以成为一个生产级的代码。

© . All rights reserved.