有没有比使用动态 WPF 路径索引更好的选择?
Is there a better option than using dynamic for WPF path indexing?
在 XAML PropertyPaths 中使用索引器的 Microsoft 文档说要使用:
<Binding Path="[key]" ... />
但是 key
必须进行硬编码才能使用这种类型的索引。
我知道 'flexible' 索引的解决方案需要 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, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
如果您注意到我的评论,这将仅适用于绑定 string[]
对象。但是,可以为 类 创建自定义索引运算符,我想对此进行概括。我不能对每个索引器运算符键类型和 return 类型进行硬编码,对吗?至少不是没有 dynamic
.
public class DynamicIndexerConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
dynamic indexable = (dynamic)values[0];
dynamic index = (dynamic)values[1];
return indexable[index];
}
public object[] ConvertBack(object values, Type[] targetType, object parameter, 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, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
This question is educational in that I don't have a bunch of ViewModels but I'd like to know if there is a better approach if I did have a ton.
为了澄清问题的教育性质,我添加了第二个答案,更广泛地涵盖了这个主题。
答案最后会有一个解决方案,结果尽可能接近Binding with indices.
1) 索引类型。
尽管文档指出可以显式指定密钥的类型,但实际上并不能那样工作。
The type of each parameter can be specified with parentheses
绑定只接受索引作为字符串的一部分。
应用此索引时,会确定索引的类型并尝试将字符串转换为该类型。
WPF 中所有常用的类型都有自己的 TypeConverter 声明。
因此,这对他们来说不是问题。
但是在创建自定义类型时,很少实现这样的转换器。
并且您将无法为这种类型的键设置字符串索引。
如果索引的类型可以采用字符串可以转换成的几种类型,也会有歧义。
假设索引类型为 object 并且存在索引为 (string) "0", (int) 0 和 (double) 0 的元素。
几乎不可能预测使用 Path = "0" 的结果会是什么。
最有可能是“0”,但并非总是如此。
2) 索引实例比较
所以要获取索引,会从一个字符串创建一个实例,那么默认情况下它不会等于在 collection.
中创建索引时使用的实例
获取索引需要实现按值比较
这已针对默认值类型实现。
但是对于自定义的,你需要另外实现它。
这可以通过覆盖 Equals 和 GetHashCode 直接在类型中实现,也可以在实现 IEqualityComparer 接口的单独 class 中实现。
3) Collection类型.
由于我们已经得出索引必须提供按值比较的结论,这意味着 collection 必须具有唯一键。
因此我们得出两种类型的 collections.
第一个是索引是元素的序号:Array、List、Collection等实现了IList.
的类型
第二个是字典,即 IDictionary 的一个实现。
接口当然可以不实现,但是实现的原则还是要做到的。
4) 索引可变性。
如果索引(以下简称key)是值类型,按值比较的实现就没有问题。
由于无法隐式更改存储在 collection.
中的内部密钥
要更改它,您需要调用 collection 方法,collection 将执行与此相关的所有必要更改。
但这是处理引用类型的方法...
Equals方法直接比较两个实例的值。
但是在collection中找到一系列key之前,会计算HashCode。
如果key放入range后,它的Hash会发生变化,那么在查找的时候,可能会得到没有key的错误结果。
有两种方法可以解决这个问题。
第一种是在保存之前创建密钥的副本。
克隆后,对原始实例的更改不会导致对已保存克隆的更改。
这样的实现适合Binding。
但是在 Sharp 中使用它时,它可能会向程序员显示意想不到的行为。
第二个(最常用的)就是所有用于计算HashCode的键值必须是不可变的(即只读字段和属性)。
5) 通过绑定自动更新
对于属性,auto-update 由 INotifyPropertyChanged 接口提供。
对于索引 collections,INotifyCollectionChanged 接口。
但是对于字典,没有默认界面来通知它的变化。
因此,如果你设置一个绑定到一个字典,然后与这个键关联的值将在这个字典中改变,auto-update将不会发生。
各种converters/multiconverters也没有解决这个问题
ObservableDictionary的实现有很多,但是都非常糟糕,不得已而为之。
即使是 MS 本身的实现 «internal ObservableDictionary»,在发生任何更改时,都会导致更新此字典的所有绑定,创建一个带有空参数的 PropertyChanged。
6) 我的发现。
应尽可能避免在绑定中使用索引。
它们只能在绝对无法解决的情况下使用。
在实施时,必须考虑所有 above-described 细微差别。
或许,除了他们之外,还有一些我没有想到的哎呀
7) 具有可变路径的通用绑定。
作为这样的“世界末日解决方案”,我创建了一个代理。
它的工作原理是字符串插值。
有一行插值表达式。
有一组参数可以对其进行插值。
插值表达式可以同时包含索引和某种复合路径。
该解决方案包括几个 classes:一个主代理、一个简单的辅助代理、一个(或多个)值到数组转换器、用于简化参数数组绑定的标记扩展。
所有这些 class 都可以单独应用。
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(object), 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), typeof(object), typeof(PathBindingProxy), 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), typeof(PathBindingProxy),
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[]), typeof(PathBindingProxy), new PropertyMetadata(null, (d, 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), typeof(string), typeof(PathBindingProxy), 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, Type targetType, object parameter, CultureInfo culture)
=> new object[] { value };
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values.Clone();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, 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, Mode=OneWay, 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,0,50,0"/>
<TextBlock Text="Enter X or Y: "/>
<TextBox x:Name="enterXY"
Width="{Binding ActualHeight, Mode=OneWay, RelativeSource={RelativeSource Self}}"
HorizontalContentAlignment="Center"/>
<TextBlock>
<Run Text=" ="/>
<Run Text="{Binding Value, Source={StaticResource pointProxy}, Mode=OneWay}" />
</TextBlock>
</StackPanel>
</UniformGrid>
</Window>
在 XAML PropertyPaths 中使用索引器的 Microsoft 文档说要使用:
<Binding Path="[key]" ... />
但是 key
必须进行硬编码才能使用这种类型的索引。
我知道 'flexible' 索引的解决方案需要 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, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
如果您注意到我的评论,这将仅适用于绑定 string[]
对象。但是,可以为 类 创建自定义索引运算符,我想对此进行概括。我不能对每个索引器运算符键类型和 return 类型进行硬编码,对吗?至少不是没有 dynamic
.
public class DynamicIndexerConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
dynamic indexable = (dynamic)values[0];
dynamic index = (dynamic)values[1];
return indexable[index];
}
public object[] ConvertBack(object values, Type[] targetType, object parameter, 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, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
This question is educational in that I don't have a bunch of ViewModels but I'd like to know if there is a better approach if I did have a ton.
为了澄清问题的教育性质,我添加了第二个答案,更广泛地涵盖了这个主题。 答案最后会有一个解决方案,结果尽可能接近Binding with indices.
1) 索引类型。
尽管文档指出可以显式指定密钥的类型,但实际上并不能那样工作。
The type of each parameter can be specified with parentheses
绑定只接受索引作为字符串的一部分。
应用此索引时,会确定索引的类型并尝试将字符串转换为该类型。
WPF 中所有常用的类型都有自己的 TypeConverter 声明。
因此,这对他们来说不是问题。
但是在创建自定义类型时,很少实现这样的转换器。
并且您将无法为这种类型的键设置字符串索引。
如果索引的类型可以采用字符串可以转换成的几种类型,也会有歧义。
假设索引类型为 object 并且存在索引为 (string) "0", (int) 0 和 (double) 0 的元素。
几乎不可能预测使用 Path = "0" 的结果会是什么。
最有可能是“0”,但并非总是如此。
2) 索引实例比较
所以要获取索引,会从一个字符串创建一个实例,那么默认情况下它不会等于在 collection.
中创建索引时使用的实例
获取索引需要实现按值比较
这已针对默认值类型实现。
但是对于自定义的,你需要另外实现它。
这可以通过覆盖 Equals 和 GetHashCode 直接在类型中实现,也可以在实现 IEqualityComparer
3) Collection类型.
由于我们已经得出索引必须提供按值比较的结论,这意味着 collection 必须具有唯一键。
因此我们得出两种类型的 collections.
第一个是索引是元素的序号:Array、List
的类型
第二个是字典,即 IDictionary 的一个实现。
接口当然可以不实现,但是实现的原则还是要做到的。
4) 索引可变性。
如果索引(以下简称key)是值类型,按值比较的实现就没有问题。
由于无法隐式更改存储在 collection.
中的内部密钥
要更改它,您需要调用 collection 方法,collection 将执行与此相关的所有必要更改。
但这是处理引用类型的方法...
Equals方法直接比较两个实例的值。
但是在collection中找到一系列key之前,会计算HashCode。
如果key放入range后,它的Hash会发生变化,那么在查找的时候,可能会得到没有key的错误结果。
有两种方法可以解决这个问题。
第一种是在保存之前创建密钥的副本。
克隆后,对原始实例的更改不会导致对已保存克隆的更改。
这样的实现适合Binding。
但是在 Sharp 中使用它时,它可能会向程序员显示意想不到的行为。
第二个(最常用的)就是所有用于计算HashCode的键值必须是不可变的(即只读字段和属性)。
5) 通过绑定自动更新
对于属性,auto-update 由 INotifyPropertyChanged 接口提供。
对于索引 collections,INotifyCollectionChanged 接口。
但是对于字典,没有默认界面来通知它的变化。
因此,如果你设置一个绑定到一个字典,然后与这个键关联的值将在这个字典中改变,auto-update将不会发生。
各种converters/multiconverters也没有解决这个问题
ObservableDictionary的实现有很多,但是都非常糟糕,不得已而为之。
即使是 MS 本身的实现 «internal ObservableDictionary»,在发生任何更改时,都会导致更新此字典的所有绑定,创建一个带有空参数的 PropertyChanged。
6) 我的发现。
应尽可能避免在绑定中使用索引。
它们只能在绝对无法解决的情况下使用。
在实施时,必须考虑所有 above-described 细微差别。
或许,除了他们之外,还有一些我没有想到的哎呀
7) 具有可变路径的通用绑定。
作为这样的“世界末日解决方案”,我创建了一个代理。
它的工作原理是字符串插值。
有一行插值表达式。
有一组参数可以对其进行插值。
插值表达式可以同时包含索引和某种复合路径。
该解决方案包括几个 classes:一个主代理、一个简单的辅助代理、一个(或多个)值到数组转换器、用于简化参数数组绑定的标记扩展。
所有这些 class 都可以单独应用。
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(object), 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), typeof(object), typeof(PathBindingProxy), 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), typeof(PathBindingProxy),
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[]), typeof(PathBindingProxy), new PropertyMetadata(null, (d, 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), typeof(string), typeof(PathBindingProxy), 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, Type targetType, object parameter, CultureInfo culture)
=> new object[] { value };
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values.Clone();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, 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, Mode=OneWay, 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,0,50,0"/>
<TextBlock Text="Enter X or Y: "/>
<TextBox x:Name="enterXY"
Width="{Binding ActualHeight, Mode=OneWay, RelativeSource={RelativeSource Self}}"
HorizontalContentAlignment="Center"/>
<TextBlock>
<Run Text=" ="/>
<Run Text="{Binding Value, Source={StaticResource pointProxy}, Mode=OneWay}" />
</TextBlock>
</StackPanel>
</UniformGrid>
</Window>