The Microsoft documentation for using indexers in XAML PropertyPaths says to use:
<Binding Path="[key]" ... />
However key
has to be hard coded to use this type of indexing.
I know the solution for 'flexible' indexing requires MultiBinding
of the form:
<MultiBinding Converter="{StaticResource BasicIndexerConverter}">
<Binding Path="Indexable"/>
<Binding Path="Key"/>
</MultiBinding>
with
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();
}
}
If you notice my comment, this will only work for binding on string[]
objects. However custom index operators can be created for classes and I would like to generalize this. I can't hardcode every indexer operator key type and return type right? At least not without 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();
}
}
This is very simple and I imagine not a terrible use for dynamic
considering bindings are resolved at runtime anyway. But I would like to know if there is a static implementation that isn't too complex or would provide the same functionality with better performance. Is this one of those rare use-cases for dynamic?
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.
In connection with the clarification of the educational nature of the question, I add a second answer that covers this topic more broadly. At the end of the answer, there will be a solution that is as close as possible in result to Binding with indices.
1) Type of indexes.
Although the documentation states that the type of the key can be specified explicitly, in reality it does not work that way.
The type of each parameter can be specified with parentheses
Binding only accepts an index as part of a string.
When this index is applied, the type of the index is determined and an attempt is made to convert the string to that type.
All commonly used types in WPF have their own TypeConverter declared.
Therefore, this is not a problem for them.
But when creating custom types, such a converter is rarely implemented.
And you won't be able to set a string index for a key with this type.
There is also an ambiguity if the type of the index can take several types into which a string can be converted.
Suppose the index type is object and there are elements with index (string) "0", (int) 0 and (double) 0.
It is almost impossible to predict what the result of using Path = "0" will be.
Most likely "0", but not always.
2) Comparison of index instances.
So to get the index, an instance will be created from a string, then it will not be equal by default to the instance used when creating an index in the collection.
To fetch an index, you need to implement comparison by value.
This is already implemented for the default value types.
But for custom ones, you need to implement it additionally.
This is implemented either directly in the type by overriding Equals and GetHashCode, or in a separate class that implements IEqualityComparer<T> Interface.
3) Collection type.
Since we have already come to the conclusion that indexes must provide comparison by value, this means that the collection must have unique keys.
And therefore we come to two types of collections.
The first one is the index is the ordinal number of the element: Array, List<T>, Collection<T> and other types that implement IList.
The second is dictionaries, that is, an implementation of IDictionary.
You can, of course, not implement the interfaces, but the implementation principles they require will still have to be fulfilled.
4) Index mutability.
If the index (hereinafter I will call the key) is a value type with the implementation of comparison by value, then there are no problems.
Since it is impossible to change implicitly the internal key stored in the collection.
To change it, you need to call the collection method and the collection will carry out all the necessary changes related to this.
But here's how to deal with reference types ...
The Equals method compares the values of two instances directly.
But before finding a range of keys in the collection, the HashCode will be calculated.
If, after the key has been placed in the range, its Hash will be changed, then when searching for it, an erroneous result may be obtained about its absence.
There are two ways to solve this problem.
The first is to create a copy of the key before saving it.
After cloning, changes to the original instance will not result in changes to the saved clone.
Such an implementation is suitable for Binding.
But when working with it in Sharp, it can show unexpected behavior for the programmer.
The second (most commonly used) is that all key values used to calculate the HashCode must be immutable (that is, readonly fields and properties).
5) Auto update by binding
For properties, auto-update is provided by the INotifyPropertyChanged interface.
For indexed collections, the INotifyCollectionChanged interface.
But for dictionaries there is no default interface to notify about changes in it.
Therefore, if you set a binding to a dictionary and then the value associated with this key will change in this dictionary, auto-update will not occur.
Various converters/multiconverters do not solve this problem either.
There are many implementations of ObservableDictionary, but they are all very bad and are a necessary last resort.
Even the implementation from MS itself «internal ObservableDictionary», upon any changes, causes all bindings to this dictionary to be updated, creating a PropertyChanged with an empty argument.
6) My findings.
The use of indexes in bindings should be avoided in every possible way.
They can be used only in cases where it will definitely not work out otherwise.
And when implementing it, all the above-described nuances must be taken into account.
Perhaps, besides them, there are still others that have not occurred to me now.
7) Universal binding with mutable path.
As one such «doomsday solution», I created a proxy.
It works on the principle of string interpolation.
There is a line with an interpolation expression.
There is an array of arguments to interpolate it.
The interpolation expression can contain both indices and some kind of compound path.
The solution includes several classes: a main proxy, a simple auxiliary proxy, a value (or values) to array converter, markup extensions to simplify binding of an array of arguments.
All of these classes can be applied on their own.
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;
}
}
}
Usage example.
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>