在 WPF 中绑定数据时使用路径参数





5.00/5 (5投票s)
通过支持运行时路径参数的绑定扩展 WPF 框架
引言
WPF 绑定有一个很好的特性。除了属性路径之外,它允许在PathProperties
中指定参数,这些参数将在绑定期间传递给Item[]
索引器。不幸的是,Binding
扩展不允许在XAML中指定这些路径参数,迫使我们使用在Path
属性中硬编码的参数,例如:{Binding Path=property1.property2[10]}
。
允许GUI设计器在XAML中指定这些参数岂不是很好?考虑一下这样的情况:{Binding Property1.Property2[(0)], {Binding Path=Index}}
。在这个表达式中,Property2.Item[]
索引器应该接收当前数据上下文Index
属性的值。拥有此功能可以降低视图模型组件的复杂性,允许XAML设计器将数据模型的部分用作视图模型。
背景
WPF 绑定功能允许业务逻辑和用户界面松散耦合。当GUI设计器可以使用XAML开发用户界面而程序员开发业务逻辑组件时,这是非常棒的。在现代MVVM模式中,设计者和程序员都同意视图模型组件的内容,该内容扩展了业务逻辑(数据模型),并具有XAML正确绑定所需的功能。
众所周知,程序员很懒,因此同时支持两个模型(M和VM)并保持同步状态可能会让他们头疼。为XAML设计器提供扩展的绑定功能可以降低视图模型的复杂性。提出的功能之一是像上面描述的那样在绑定路径中指定参数的可能性。这是一个小的例子。
使用代码
考虑以下数据模型和视图模型
// data model
public class Sensor
{
public string Name { get; set; }
}
// view model
public partial class MainWindow : Window
{
ObservableCollection<Sensor> _sensors = new ObservableCollection<Sensor>();
public MainWindow()
{
InitializeComponent();
_sensors.Add(new Sensor() { Name = "Sensor1" });
_sensors.Add(new Sensor() { Name = "Sensor2" });
_sensors.Add(new Sensor() { Name = "Sensor3" });
_sensors.Add(new Sensor() { Name = "Sensor4" });
}
public IList<Sensor> Sensors
{
get { return _sensors; }
}
public int this[Sensor sensor]
{
get { return _sensors.IndexOf(sensor); }
}
}
现在我们想将传感器列表绑定到XAML中的ListBox,并显示集合中每个传感器的索引。请查看以下XAML
<Window x:Class="Sources.MainWindow" x:Name="_window"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:emg="clr-namespace:Emightgen"
Title="Binding using custom parameters" Height="350" Width="525">
<Grid>
<ListBox
ItemsSource="{Binding ElementName=_window, Path=Sensors}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center">Name:</TextBlock>
<TextBlock VerticalAlignment="Center" Margin="5" Text="{Binding Name}" />
<TextBlock VerticalAlignment="Center">Index:</TextBlock>
<TextBlock VerticalAlignment="Center" Margin="5" Text="{emg:Binding '[(0)]', {Binding}, ElementName=_window}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
正如你在上图中看到的,列表框显示每个传感器的索引,在模板数据上下文更改时动态计算参数。
实现
扩展WPF控件非常困难(几乎不可能)。几乎所有类都是密封的或内部的,因此无法继承它们。对于Binding
扩展类也是如此,该类应该为我们的目的而扩展。我们需要继承自Binding
有两个原因。第一个是,我们应该允许在XAML中指定参数;第二个是当数据上下文更改或模板重新应用时计算(绑定)它们。即使我们提供自己的标记扩展来创建包含参数的PropertyPath
,它们也不会在绑定过程中被计算,而是作为Binding
的实例而不是实际值传递给索引器。
幸运的是(在.NET调试和源代码审查之后),我找到了一个我们可以使用的解决方案。我们可以编写自己的标记扩展来模拟WPF绑定,并在内部使用MultiBinding
来提供结果值。使用WPF的MultiBinding
解决了以下四个问题
- 用户指定的参数将在绑定期间自动计算。
- 我们的扩展可以在
DataTrigger
中使用,在DataTrigger
中不允许使用自定义标记扩展。 - 支持双向绑定。
- 在数据上下文更改或应用模板时接收通知。
让我们看一下我们标记扩展的简化版本。它包含模拟WPF绑定的属性和构造函数参数,允许用户指定参数。此标记扩展实现了IMultiValueConverter
接口,该接口用于内部MultiBinding
以提供最终值,并实现IValueConverter
以记住每个计算的参数。
public class BindingExtension : MarkupExtension, IMultiValueConverter, IValueConverter, INotifyPropertyChanged
{
string _path = null;
string _elementName = null;
object _source = null;
Collection<object> _parameters = null;
BindingMode _mode = BindingMode.Default;
public BindingExtension(string path, object arg1, object arg2)
{
_path = path;
_parameters = new Collection<object>();
_parameters.Add(arg1);
_parameters.Add(arg2);
}
[DefaultValue(null), ConstructorArgument("arg1"), EditorBrowsable(EditorBrowsableState.Never)]
public object Arg1
{
get { return _parameters[0]; }
set { _parameters[0] = value; }
}
}
内部MultiBinding
包含用户指定的每个参数的绑定,用于获取数据上下文的特殊绑定以及用于获取目标依赖项对象的特殊绑定。这两个在DataTrigger
中是必需的,DataTrigger
也是DependencyObject
,但它使用自己的逻辑来计算提供的绑定表达式。另一个特殊绑定将用于从代码更新目标属性值。我们的扩展将用作转换器,并将最终值作为转换结果提供。
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
return this;
}
IProvideValueTarget provideValueTarget = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
if (provideValueTarget == null)
{
return this;
}
_targetObject = provideValueTarget.TargetObject as DependencyObject;
if (_targetObject == null)
{
return this;
}
_targetProperty = provideValueTarget.TargetProperty as DependencyProperty;
// create wpf binding
MultiBinding mbinding = new MultiBinding();
mbinding.Mode = _mode;
mbinding.Converter = this;
// binding to evaluate data context
Binding binding1 = new Binding();
binding1.Mode = BindingMode.OneWay;
mbinding.Bindings.Add(binding1);
// binding to evaluate target element
Binding binding2 = new Binding();
binding2.Mode = BindingMode.OneWay;
binding2.RelativeSource = new RelativeSource(RelativeSourceMode.Self);
mbinding.Bindings.Add(binding2);
// binding to private property, that will reevaluate final value when this property changes
Binding binding3 = new Binding();
binding3.Mode = BindingMode.OneWay;
binding3.Source = this;
binding3.Path = new PropertyPath("EffectiveValueChanged");
mbinding.Bindings.Add(binding3);
// this will hold evaluated parameters
_evaluatedParameters = new Collection<object>();
// for every binding parameter apply our internal converter
for (int i = 0; i < _parameters.Count; i++)
{
object pvalue = _parameters[i];
if (pvalue is Binding)
{
Binding pbinding = pvalue as Binding;
// apply only once
if (!(pbinding.ConverterParameter is ParameterConverterArgs))
{
pbinding.ConverterParameter = new ParameterConverterArgs()
{
OriginalConverter = pbinding.Converter,
OriginalParameter = pbinding.ConverterParameter,
ParameterIndex = i
};
pbinding.Converter = this;
pbinding.Mode = BindingMode.OneWay;
}
mbinding.Bindings.Add(pbinding);
}
_evaluatedParameters.Add(pvalue);
}
object value = mbinding.ProvideValue(serviceProvider);
_multiBindingExpression = value as MultiBindingExpression;
return value;
}
主要工作是在IMultiValueConverter.Convert
方法中完成。当必须接收最终值时,它由MultiValueExpression
调用。在这里,我们可以使用计算的参数创建另一个Binding
,使用这些参数设置PathParameters
,并使用目标对象上的BindingOperations
类获取最终值。为了计算最终值,我们可以使用我们自己的附加属性,该属性将接受绑定并返回该值。我们在这里应该小心,因为对于同一个目标对象,多个依赖属性可能使用我们的扩展绑定(在这种情况下,我们应该支持多个附加属性)。
object IMultiValueConverter.Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DependencyObject targetObject = values[1] as DependencyObject;
if (targetObject == null)
{
return null;
}
// to allow several dependency properties to be bound to single target object
// use several attached properties, here just find the next available property to use
// available, means no binding is set to this property
if (_evaluationProperty == null)
{
_evaluationProperty = PathEvaluationProperties.GetFreeEvaluationProperty(targetObject, this);
}
// create path evaluation binding
PathEvaluationBinding binding = BindingOperations.GetBindingBase(targetObject, _evaluationProperty) as PathEvaluationBinding;
if (binding == null)
{
binding = new PathEvaluationBinding(this, targetObject);
binding.Path = new PropertyPath(_path, _parameters.ToArray());
// set binding mode according to the specified in extension by user or in property metadata
if (_multiBindingExpression != null)
{
binding.Mode = _multiBindingExpression.ParentMultiBinding.Mode;
}
if (binding.Mode == BindingMode.Default)
{
if (_targetProperty != null)
{
FrameworkPropertyMetadata mt = _targetProperty.GetMetadata(_targetObject) as FrameworkPropertyMetadata;
if (mt != null && mt.BindsTwoWayByDefault)
{
binding.Mode = BindingMode.TwoWay;
}
}
}
if (string.IsNullOrEmpty(_elementName))
{
binding.Source = _source;
}
else
{
binding.ElementName = _elementName;
}
}
if (_parametersChanged)
{
_parametersChanged = false;
for (int i = 0; i < _evaluatedParameters.Count; i++)
{
binding.Path.PathParameters[i] = _evaluatedParameters[i];
}
}
try
{
// notifications are sent when source value is changed and binding is TwoWay or OneWay
// when we set binding here, notification is sent also, so disable it to prevent an infinite loop
_disableNotification = true;
BindingOperations.SetBinding(targetObject, _evaluationProperty, binding);
}
finally
{
_disableNotification = false;
}
object value = binding.EffectiveValue;
// now we have to convert the value
if (value != null)
{
if (!targetType.IsAssignableFrom(value.GetType()))
{
TypeConverter tc = TypeDescriptor.GetConverter(value);
value = tc.ConvertTo(value, targetType);
}
}
return value;
}
在这里,我描述了实现的简化版本。可以使用上面的链接下载实际的实现,其中包含对单向和双向绑定的支持。
历史
2012年3月28日 - 首次版本