Вложенные привязки в WPF
В WPF существует три вида привязок: Binding, PriorityBinding и MultiBinding. Все три привязки наследуются от одного базового класса BindingBase. PriorityBinding и MultiBinding позволяют к одному свойству привязать несколько других привязок, например:
public class JoinStringConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var separator = parameter as string ?? " ";
return string.Join(separator, values);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
var separator = parameter as string ?? " ";
return (value as string)?.Split(new[] { separator }, StringSplitOptions.None).Cast
Список привязок MultiBinding-а — это коллекция типа Collection
Но при выполнении такого кода ловим исключение »BindingCollection не поддерживает элементы типа MultiBinding. Допускается только тип Binding.». Зачем же было тогда использовать Collection
Для решения проблемы вложенных привязок был написан класс NestedBinding, который позволяет использовать внутри себя другие привязки Binding и NestedBinding.
[ContentProperty(nameof(Bindings))]
public class NestedBinding : MarkupExtension
{
public NestedBinding()
{
Bindings = new Collection();
}
public Collection Bindings { get; }
public IMultiValueConverter Converter { get; set; }
public object ConverterParameter { get; set; }
public CultureInfo ConverterCulture { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (!Bindings.Any())
throw new ArgumentNullException(nameof(Bindings));
if (Converter == null)
throw new ArgumentNullException(nameof(Converter));
var target = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
if (target.TargetObject is Collection)
{
var binding = new Binding
{
Source = this
};
return binding;
}
var multiBinding = new MultiBinding
{
Mode = BindingMode.OneWay
};
var tree = GetNestedBindingsTree(this, multiBinding);
var converter = new NestedBindingConverter(tree);
multiBinding.Converter = converter;
return multiBinding.ProvideValue(serviceProvider);
}
private static NestedBindingsTree GetNestedBindingsTree(NestedBinding nestedBinding, MultiBinding multiBinding)
{
var tree = new NestedBindingsTree
{
Converter = nestedBinding.Converter,
ConverterParameter = nestedBinding.ConverterParameter,
ConverterCulture = nestedBinding.ConverterCulture
};
foreach (var bindingBase in nestedBinding.Bindings)
{
var binding = bindingBase as Binding;
var childNestedBinding = binding?.Source as NestedBinding;
if (childNestedBinding != null && binding.Converter == null)
{
tree.Nodes.Add(GetNestedBindingsTree(childNestedBinding, multiBinding));
continue;
}
tree.Nodes.Add(new NestedBindingNode(multiBinding.Bindings.Count));
multiBinding.Bindings.Add(bindingBase);
}
return tree;
}
}
public class NestedBindingNode
{
public NestedBindingNode(int index)
{
Index = index;
}
public int Index { get; }
public override string ToString()
{
return Index.ToString();
}
}
public class NestedBindingsTree : NestedBindingNode
{
public NestedBindingsTree() : base(-1)
{
Nodes = new List();
}
public IMultiValueConverter Converter { get; set; }
public object ConverterParameter { get; set; }
public CultureInfo ConverterCulture { get; set; }
public List Nodes { get; private set; }
}
public class NestedBindingConverter : IMultiValueConverter
{
public NestedBindingConverter(NestedBindingsTree tree)
{
Tree = tree;
}
private NestedBindingsTree Tree { get; }
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var value = GetTreeValue(Tree, values, targetType, culture);
return value;
}
private object GetTreeValue(NestedBindingsTree tree, object[] values, Type targetType, CultureInfo culture)
{
var objects = tree.Nodes.Select(x => x is NestedBindingsTree ? GetTreeValue((NestedBindingsTree)x, values, targetType, culture) : values[x.Index]).ToArray();
var value = tree.Converter.Convert(objects, targetType, tree.ConverterParameter, tree.ConverterCulture ?? culture);
return value;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Реализован NestedBinding через обычный MultiBinding. Но т.к. MultiBinding не может принимать другой MultiBinding, то дерево разворачивается в список Binding-ов. Позиция этих Binding-ов и их конвертеры сохраняются для дальнейшей генерации исходного дерева в конвертере NestedBindingConverter.
Конвертер получает список значений всех привязок Binding и структуру исходного дерева. Далее рекурсией производится обход дерева, и вычисляются значения конвертеров.
Пример использования NestedBinding:
На выходе получаем строку «A, B C (DE), F».
Исходники выложены в репозитории GitHub.