可绑定转换器参数
一种在WPF的XAML中实现可绑定转换器参数的简单技术。
- 下载 BindableConverterParameter.zip (Take1-部分)
- 下载 BindableConverterParameterTake2.zip(Take2-部分)
- 下载 BindableConverterParameterTake3.zip
引言
绑定是WPF引入的最强大的功能之一,它允许我们使用最少的声明式代码(XAML)来实现复杂的基于数据的UI,同时依赖于一个“内置的”、即插即用的机制,该机制以直接的方式或通过转换器将数据连接到UI控件。
绑定转换器本质上是封装的函数,它们接收绑定值,以特定的方式对其进行操作,并返回操作后的结果。在许多情况下,转换器需要另一份信息来源才能“完成其工作”,这份来源被称为“ConverterParameter
”,它是转换器的Convert
和ConvertBack
方法参数之一。
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修复:内存泄漏
- 添加功能:元素名称绑定
- Bug修复:内存泄漏
这是我第三次尝试来实现一个健壮、简单的解决方案来解决这个非常普遍的障碍。
主要目标
将先前尝试中描述的概念锻造成真实世界、健壮的解决方案。
支持的功能和优点
- 可绑定的转换器参数(包括绑定、静态值、枚举*)
* 有更好的方法来实现基于枚举的绑定,在BcpBinding Markup-Extension类中添加对ConverterParameter枚举的支持,是为了展示其处理新的、不同类型数据的能力,同时只进行少量的代码修改。 - 多重绑定支持
- 双向绑定支持
- 可读、易于理解的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>
- 绑定的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>
- 绑定到索引器(例如,SomeIndexer[])支持。在绑定和转换器参数绑定中都支持。
注释
- 由于此解决方案基于之前的解决方案,因此我在此部分(TAKE3)省略了说明。我在代码中添加了注释。
- 绑定语法是弱类型的,因为绑定和转换器参数作为字符串传递给Markup-Extension。
- 我省略了代码中的错误验证,因为它们会增加额外的代码,使实现解决方案思路的相关代码变得模糊。
# TAKE3 结束
可绑定转换器参数 Take-2
这是我第二次尝试来实现一个健壮、简单的解决方案来解决这个非常普遍的障碍。
主要目标:
- XAML代码应尽可能接近预期代码。
- 简单且简洁。
- 对现有代码(XAML和整体项目)的影响最小。
- 支持每个元素无限数量的可绑定转换器参数绑定。
- 支持双向绑定(针对具有可绑定转换器参数的绑定)。
解决方案原则
- 允许用户以尽可能接近所需(不可用形式)的形式编写XAML。我选择了这种方式:
- 创建一个外部机制,在“幕后”进行拼接,以实现所需的功能。
<TextBox Text="{Binding Path=FirstName,Converter={StaticResource
TestValueConverter},ConverterParameter='Binding Path=ConcatSign',Mode=TwoWay}" />
解决方案本身
- 像往常一样编写XAML,每当需要应用“可绑定转换器参数”时,请按上面代码所示编写。
- 对于具有这些特殊绑定类型的元素,请在代码隐藏中注册_Initialized事件(对于那些MVVM狂热者,即在任何情况下都放弃使用代码隐藏,或者在没有代码隐藏的情况下,建议使用Behavior)。
- 在_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
{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;
}
}
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.cs和MyBindedParameterBinding.cs或/和MyBindedParameterMultiBinding.cs添加到您的项目中即可。这里展示的示例并不是旨在成为一个健壮、面向生产的代码产品,因为它有很多未解决的问题(例如,当尝试使用多个可绑定转换器参数时),尽管如此,它可以得到扩展以成为一个生产级的代码。