问题描述
有关在 XAML PropertyPaths 中使用索引器的 Microsoft 文档说要使用:
<Binding Path="[key]" ... />
然而,key
必须进行硬编码才能使用这种类型的索引。
我知道“灵活”索引的解决方案需要 MultiBinding
的形式:
<MultiBinding Converter="{StaticResource BasicIndexerConverter}">
<Binding Path="Indexable"/>
<Binding Path="Key"/>
</MultiBinding>
与
public class BasicIndexerConverter : IMultiValueConverter
{
public object Convert(object[] values,Type targettype,object parameter,CultureInfo culture)
{
string[] indexable = (string[])values[0]; //Assuming indexable is type string[]
int key = (int)values[1];
return indexable[key];
}
public object[] ConvertBack(object values,Type[] targettype,CultureInfo culture)
{
throw new NotImplementedException();
}
}
如果您注意到我的评论,这仅适用于 string[]
对象的绑定。但是,可以为类创建自定义索引运算符,我想对此进行概括。我不能对每个索引器运算符键类型和返回类型进行硬编码,对吗?至少没有 dynamic
。
public class DynamicIndexerConverter : IMultiValueConverter
{
public object Convert(object[] values,CultureInfo culture)
{
dynamic indexable = (dynamic)values[0];
dynamic index = (dynamic)values[1];
return indexable[index];
}
public object[] ConvertBack(object values,CultureInfo culture)
{
throw new NotImplementedException();
}
}
这非常简单,考虑到绑定在运行时已解决,我认为 dynamic
的用途并不可怕。但是我想知道是否有一个不太复杂的静态实现,或者是否可以提供具有更好性能的相同功能。这是动态的罕见用例之一吗?
解决方法
示例(一般没有小细节):
public class DynamicIndexerConverter : IMultiValueConverter
{
public object Convert(object[] values,Type targetType,object parameter,CultureInfo culture)
{
if (values[0] is IList list)
return list[int.Parse(values[1].ToString())];
if (values[0] is IDictionary dict)
return dict[values[1]];
dynamic indexable = (dynamic)values[0];
dynamic index = (dynamic)values[1];
return indexable[index];
}
public object[] ConvertBack(object values,Type[] targetType,CultureInfo culture)
{
throw new NotImplementedException();
}
}
,
这个问题很有教育意义,因为我没有一堆 ViewModel,但我想知道如果我有很多的话,是否有更好的方法。
为了澄清问题的教育性质,我添加了第二个更广泛地涵盖该主题的答案。 在答案的最后,会有一个结果尽可能接近于使用索引绑定的解决方案。
1) 索引类型。
尽管文档指出可以显式指定密钥的类型,但实际上它并不能那样工作。
The type of each parameter can be specified with parentheses
绑定只接受一个索引作为字符串的一部分。
应用此索引时,将确定索引的类型并尝试将字符串转换为该类型。
WPF 中所有常用的类型都声明了自己的 TypeConverter。
因此,这对他们来说不是问题。
但是在创建自定义类型时,很少实现这样的转换器。
而且您将无法为这种类型的键设置字符串索引。
如果索引的类型可以采用字符串可以转换成的几种类型,那么也存在歧义。
假设索引类型是对象,并且存在索引(字符串)“0”、(int)0和(double)0的元素。
几乎不可能预测使用 Path = "0" 的结果会是什么。
最有可能是“0”,但并非总是如此。
2) 索引实例对比。
所以要获取索引,会从一个字符串中创建一个实例,那么默认情况下它不会等于在集合中创建索引时使用的实例。
要获取索引,您需要实现按值比较。
这已经为默认值类型实现了。
但是对于自定义的,需要另外实现。
这可以通过覆盖 Equals 和 GetHashCode 在类型中直接实现,也可以在实现 IEqualityComparer
3) 集合类型。
由于我们已经得出了索引必须提供按值比较的结论,这意味着集合必须具有唯一键。
因此我们有两种类型的集合。
第一个是index是元素的序号:Array、List
第二个是字典,即 IDictionary 的一个实现。
当然,您可以不实现接口,但仍然必须满足它们所需的实现原则。
4) 索引可变性。
如果索引(以下称为键)是值类型,按值进行比较,则没有问题。
由于不可能隐式更改存储在集合中的内部密钥。
要更改它,您需要调用集合方法,集合将执行与此相关的所有必要更改。
但是这里是如何处理引用类型...
Equals 方法直接比较两个实例的值。
但是在找到集合中的一系列键之前,会计算 HashCode。
如果key被放入range后,它的Hash会发生变化,那么在搜索的时候,可能会得到一个不存在的错误结果。
有两种方法可以解决这个问题。
第一种是在保存之前创建密钥的副本。
克隆后,对原始实例的更改不会导致对保存的克隆的更改。
这样的实现适用于Binding。
但是在 Sharp 中使用它时,它可能会显示出程序员意想不到的行为。
第二个(最常用的)是所有用于计算HashCode的key值必须是不可变的(即只读字段和属性)。
5) 通过绑定自动更新
对于属性,自动更新由 INotifyPropertyChanged 接口提供。
对于索引集合,INotifyCollectionChanged 接口。
但是对于字典,没有默认接口来通知它的变化。
因此,如果您将绑定设置为字典,然后与此键关联的值将在此字典中更改,则不会发生自动更新。
各种转换器/多转换器也不能解决这个问题。
ObservableDictionary 的实现有很多,但都非常糟糕,是不得已而为之。
即使是 MS 本身的实现 «internal ObservableDictionary»,在发生任何更改时,都会导致更新到此字典的所有绑定,从而创建一个带有空参数的 PropertyChanged。
6) 我的发现。
应该以各种可能的方式避免在绑定中使用索引。
它们只能用于在其他情况下肯定无法解决的情况。
在实施它时,必须考虑所有上述细微差别。
或许,除了他们,还有一些我现在没有想到的。
7) 具有可变路径的通用绑定。
作为一个这样的“世界末日解决方案”,我创建了一个代理。
它的工作原理是字符串插值。
有一行带有插值表达式。
有一组参数可以插入它。
插值表达式可以包含索引和某种复合路径。
该解决方案包括几个类:一个主代理、一个简单的辅助代理、一个(或多个)值到数组转换器、标记扩展以简化参数数组的绑定。
所有这些类都可以单独应用。
using System;
using System.Windows;
using System.Windows.Data;
namespace Proxy
{
/// <summary> Provides a <see cref="DependencyObject"/> proxy with
/// one property and an event notifying about its change. </summary>
public class ProxyDO : DependencyObject
{
/// <summary> Property for setting external bindings. </summary>
public object Value
{
get { return (object)GetValue(ValueProperty); }
set { SetValue(ValueProperty,value); }
}
// Using a DependencyProperty as the backing store for Value. This enables animation,styling,binding,etc...
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value),typeof(object),typeof(ProxyDO),new PropertyMetadata(null));
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
ValueChanged?.Invoke(this,e);
}
/// <summary> An event that occurs when the value of any
/// <see cref="DependencyProperty"/> of this object changes.</summary>
public event EventHandler<DependencyPropertyChangedEventArgs> ValueChanged;
/// <summary> Returns <see langword="true"/> if the property value <see cref="Value"/> is not set.</summary>
public bool IsUnsetValue => Equals(ReadLocalValue(ValueProperty),DependencyProperty.UnsetValue);
/// <summary> Clears all <see cref="DependencyProperty"/> this <see cref="ProxyDO"/>.</summary>
public void Reset()
{
LocalValueEnumerator locallySetProperties = GetLocalValueEnumerator();
while (locallySetProperties.MoveNext())
{
DependencyProperty propertyToClear = locallySetProperties.Current.Property;
if (!propertyToClear.ReadOnly)
{
ClearValue(propertyToClear);
}
}
}
/// <summary> <see langword="true"/> if the property <see cref="Value"/> has Binding.</summary>
public bool IsValueBinding => BindingOperations.GetBindingExpressionBase(this,ValueProperty) != null;
/// <summary> <see langword="true"/> if the property <see cref="Value"/> has a binding
/// and it is in the state <see cref="BindingStatus.Active"/>.</summary>
public bool IsActiveValueBinding
{
get
{
var exp = BindingOperations.GetBindingExpressionBase(this,ValueProperty);
if (exp == null)
return false;
var status = exp.Status;
return status == BindingStatus.Active;
}
}
/// <summary>Setting the Binding to the Property <see cref="Value"/>.</summary>
/// <param name="binding">The binding to be assigned to the property.</param>
public void SetValueBinding(BindingBase binding)
=> BindingOperations.SetBinding(this,ValueProperty,binding);
}
}
using System;
using System.Windows;
using System.Windows.Data;
namespace Proxy
{
public class PathBindingProxy : Freezable
{
/// <summary>
/// The value obtained from the binding with the interpolated path.
/// </summary>
public object Value
{
get { return (object)GetValue(ValueProperty); }
private set { SetValue(ValuePropertyKey,value); }
}
private static readonly DependencyPropertyKey ValuePropertyKey =
DependencyProperty.RegisterReadOnly(nameof(Value),typeof(PathBindingProxy),new PropertyMetadata(null));
/// <summary><see cref="DependencyProperty"/> for property <see cref="Value"/>.</summary>
public static readonly DependencyProperty ValueProperty = ValuePropertyKey.DependencyProperty;
/// <summary>
/// The source to which the interpolated path is applied.
/// The default is an empty Binding.
/// When used in Resources,a UI element gets his DataContext.
/// </summary>
public object DataContext
{
get { return (object)GetValue(DataContextProperty); }
set { SetValue(DataContextProperty,value); }
}
/// <summary><see cref="DependencyProperty"/> для свойства <see cref="DataContext"/>.</summary>
public static readonly DependencyProperty DataContextProperty =
DependencyProperty.Register(nameof(DataContext),new PropertyMetadata(null));
/// <summary>
/// String to interpolate the path.
/// </summary>
public string InterpolatedPath
{
get { return (string)GetValue(InterpolatedPathProperty); }
set { SetValue(InterpolatedPathProperty,value); }
}
/// <summary><see cref="DependencyProperty"/> для свойства <see cref="InterpolatedPath"/>.</summary>
public static readonly DependencyProperty InterpolatedPathProperty =
DependencyProperty.Register(nameof(InterpolatedPath),typeof(string),new PropertyMetadata(null,(d,e) => ((PathBindingProxy)d).ChangeBinding()));
/// <summary>
/// Array of interpolation arguments
/// </summary>
public object[] Arguments
{
get { return (object[])GetValue(ArgumentsProperty); }
set { SetValue(ArgumentsProperty,value); }
}
/// <summary><see cref="DependencyProperty"/> для свойства <see cref="Arguments"/>.</summary>
public static readonly DependencyProperty ArgumentsProperty =
DependencyProperty.Register(nameof(Arguments),typeof(object[]),e) => ((PathBindingProxy)d).ChangeBinding()));
private void ChangeBinding()
{
string path = InterpolatedPath;
string stringPath;
if (string.IsNullOrWhiteSpace(path))
{
stringPath = string.Empty;
}
else
{
object[] args = Arguments;
if (args == null || args.Length == 0)
{
stringPath = path;
}
else
{
stringPath = string.Format(path,args);
}
}
if (this.stringPath != stringPath)
{
this.stringPath = stringPath;
Path = stringPath;
proxy.SetValueBinding(new Binding($"DataContext.{stringPath}") { Source = this });
}
}
/// <summary>
/// Path obtained by string interpolation.
/// </summary>
public string Path
{
get { return (string)GetValue(PathProperty); }
private set { SetValue(PathPropertyKey,value); }
}
private static readonly DependencyPropertyKey PathPropertyKey =
DependencyProperty.RegisterReadOnly(nameof(Path),new PropertyMetadata(null));
/// <summary><see cref="DependencyProperty"/> for property <see cref="Path"/>.</summary>
public static readonly DependencyProperty PathProperty = PathPropertyKey.DependencyProperty;
private readonly ProxyDO proxy = new ProxyDO();
private static readonly Binding bindingEmpty = new Binding();
private string stringPath;
public PathBindingProxy()
{
proxy.ValueChanged += OnValueChanged;
BindingOperations.SetBinding(this,DataContextProperty,bindingEmpty);
InterpolatedPath = string.Empty;
}
private void OnValueChanged(object sender,DependencyPropertyChangedEventArgs e)
{
Value = e.NewValue;
}
protected override Freezable CreateInstanceCore()
{
throw new NotImplementedException();
}
}
}
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Markup;
namespace Converters
{
[ValueConversion(typeof(object),typeof(object[]))]
public class ToArrayConverter : IValueConverter,IMultiValueConverter
{
public object Convert(object value,CultureInfo culture)
=> new object[] { value };
public object Convert(object[] values,CultureInfo culture)
{
return values.Clone();
}
public object ConvertBack(object value,CultureInfo culture)
{
throw new NotImplementedException();
}
public object[] ConvertBack(object value,Type[] targetTypes,CultureInfo culture)
{
throw new NotImplementedException();
}
public static ToArrayConverter Instance { get; } = new ToArrayConverter();
}
public class ToArrayConverterExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
=> ToArrayConverter.Instance;
}
}
using Converters;
using System.Windows.Data;
namespace Proxy
{
public class ArrayBinding : Binding
{
public ArrayBinding()
: base()
{
Converter = ToArrayConverter.Instance;
}
public ArrayBinding(string path)
: base(path)
{
Converter = ToArrayConverter.Instance;
}
}
}
using Converters;
using System.Windows.Data;
namespace Proxy
{
public class ArrayMultiBinding : MultiBinding
{
public ArrayMultiBinding()
: base()
{
Converter = ToArrayConverter.Instance;
}
}
}
使用示例。
using System.Windows;
namespace InterpolationPathTest
{
public class TestViewModel
{
public int[][] MultiArray { get; } = new int[10][];
public Point Point { get; } = new Point(123.4,567.8);
public TestViewModel()
{
for (int i = 0; i < 10; i++)
{
MultiArray[i] = new int[10];
for (int j = 0; j < 10; j++)
{
MultiArray[i][j] = i * j;
}
}
}
}
}
<Window x:Class="InterpolationPathTest.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:InterpolationPathTest" xmlns:proxy="clr-namespace:Proxy;assembly=Common"
mc:Ignorable="d"
Title="TestWindow" Height="450" Width="800">
<Window.DataContext>
<local:TestViewModel/>
</Window.DataContext>
<UniformGrid Columns="1">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<StackPanel.Resources>
<proxy:PathBindingProxy x:Key="multiProxy" InterpolatedPath="MultiArray[{0}][{1}]">
<proxy:PathBindingProxy.Arguments>
<proxy:ArrayMultiBinding>
<Binding Path="Text" ElementName="left"/>
<Binding Path="Text" ElementName="right"/>
</proxy:ArrayMultiBinding>
</proxy:PathBindingProxy.Arguments>
</proxy:PathBindingProxy>
</StackPanel.Resources>
<TextBox x:Name="left" Text="5"
Width="{Binding ActualHeight,Mode=OneWay,RelativeSource={RelativeSource Self}}"
HorizontalContentAlignment="Center"/>
<TextBlock Text=" * "/>
<TextBox x:Name="right" Text="7"
Width="{Binding ActualHeight,RelativeSource={RelativeSource Self}}"
HorizontalContentAlignment="Center"/>
<TextBlock Text=" = "/>
<TextBlock Text="{Binding Value,Source={StaticResource multiProxy}}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<StackPanel.Resources>
<proxy:PathBindingProxy x:Key="pointProxy"
InterpolatedPath="Point.{0}"
Arguments="{proxy:ArrayBinding Text,ElementName=enterXY}"/>
</StackPanel.Resources>
<TextBlock Text="{Binding Point}" Margin="0,50,0"/>
<TextBlock Text="Enter X or Y: "/>
<TextBox x:Name="enterXY"
Width="{Binding ActualHeight,RelativeSource={RelativeSource Self}}"
HorizontalContentAlignment="Center"/>
<TextBlock>
<Run Text=" ="/>
<Run Text="{Binding Value,Source={StaticResource pointProxy},Mode=OneWay}" />
</TextBlock>
</StackPanel>
</UniformGrid>
</Window>