В WPF существует три вида привязок: Binding, PriorityBinding и MultiBinding. Все три привязки наследуются от одного базового класса BindingBase. PriorityBinding и MultiBinding позволяют к одному свойству привязать несколько других привязок, например:
<MultiBinding Converter="{StaticResource JoinStringConverter}" ConverterParameter=" ">
<Binding Path="FirstName" />
<Binding Path="MiddleName" />
<Binding Path="LastName" />
</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<object>().ToArray();
}
}
Список привязок MultiBinding-а — это коллекция типа Collection<BindingBase>. Логично было бы предположить, что внутри MultiBinding-а можно использовать еще один MultiBinding.
<MultiBinding Converter="{StaticResource JoinStringConverter}" ConverterParameter=" ">
<Binding Path="MyProperty1" />
<MultiBinding Converter="{StaticResource JoinStringConverter}" ConverterParameter=", ">
<Binding Path="MyProperty2" />
<Binding Path="MyProperty3" />
<Binding Path="MyProperty4" />
</MultiBinding>
</MultiBinding>
Но при выполнении такого кода ловим исключение "BindingCollection не поддерживает элементы типа MultiBinding. Допускается только тип Binding.". Зачем же было тогда использовать Collection<BindingBase>, а не Collection<Binding>? А потому, что если использовать Collection<Binding>, мы бы поймали другое исключение "Binding нельзя использовать в коллекции «Collection<Binding>». «Binding» можно задать только в параметре DependencyProperty объекта DependencyObject.".
Для решения проблемы вложенных привязок был написан класс NestedBinding, который позволяет использовать внутри себя другие привязки Binding и NestedBinding.
[ContentProperty(nameof(Bindings))]
public class NestedBinding : MarkupExtension
{
public NestedBinding()
{
Bindings = new Collection<BindingBase>();
}
public Collection<BindingBase> 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<BindingBase>)
{
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<NestedBindingNode>();
}
public IMultiValueConverter Converter { get; set; }
public object ConverterParameter { get; set; }
public CultureInfo ConverterCulture { get; set; }
public List<NestedBindingNode> 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:
<TextBlock>
<TextBlock.Text>
<n:NestedBinding Converter="{StaticResource JoinStringConverter}" ConverterParameter=", ">
<Binding Path="A" />
<n:NestedBinding Converter="{StaticResource JoinStringConverter}" ConverterParameter=" ">
<Binding Path="B" />
<Binding Path="C" />
<n:NestedBinding Converter="{StaticResource JoinStringConverter}" ConverterParameter="">
<Binding Source="(" />
<Binding Path="D" />
<Binding Path="E" />
<Binding Source=")" />
</n:NestedBinding>
</n:NestedBinding>
<Binding Path="F" UpdateSourceTrigger="PropertyChanged" />
</n:NestedBinding>
</TextBlock.Text>
</TextBlock>
На выходе получаем строку «A, B C (DE), F».
Исходники выложены в репозитории GitHub.
Автор: adeptuss