Вложенные привязки в WPF

В WPF существует три вида привязок: Binding, PriorityBinding и MultiBinding. Все три привязки наследуются от одного базового класса BindingBase. PriorityBinding и MultiBinding позволяют к одному свойству привязать несколько других привязок, например:


    
    
    



Исходный код класса JoinStringConverter
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().ToArray();
    }
}




Список привязок MultiBinding-а — это коллекция типа Collection. Логично было бы предположить, что внутри MultiBinding-а можно использовать еще один MultiBinding.


    
    
        
        
        
    



Но при выполнении такого кода ловим исключение »BindingCollection не поддерживает элементы типа MultiBinding. Допускается только тип Binding.». Зачем же было тогда использовать Collection, а не Collection? А потому, что если использовать Collection, мы бы поймали другое исключение »Binding нельзя использовать в коллекции «Collection». «Binding» можно задать только в параметре DependencyProperty объекта DependencyObject.».

Для решения проблемы вложенных привязок был написан класс NestedBinding, который позволяет использовать внутри себя другие привязки Binding и NestedBinding.

Исходный код класса 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;
    }
}



Исходный код классов NestedBindingNode и NestedBindingsTree
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; }
}



Исходный код класса NestedBindingConverter
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.

6be64dbf000545098b72b207f086fc26.jpg

Конвертер получает список значений всех привязок Binding и структуру исходного дерева. Далее рекурсией производится обход дерева, и вычисляются значения конвертеров.

Пример использования NestedBinding:


    
        
            

            
                
                

                
                    
                    
                    
                    
                
            

            
        
    



На выходе получаем строку «A, B C (DE), F».

Исходники выложены в репозитории GitHub.

© Habrahabr.ru