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

в 21:07, , рубрики: .net, binding, C#, nested binding, wpf, разработка под windows

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

<MultiBinding Converter="{StaticResource JoinStringConverter}" ConverterParameter=" ">
    <Binding Path="FirstName" />
    <Binding Path="MiddleName" />
    <Binding Path="LastName" />
</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<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.

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

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

    public IMultiValueConverter Converter { get; set; }

    public object ConverterParameter { get; set; }

    public CultureInfo ConverterCulture { get; set; }

    public List<NestedBindingNode> 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.

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

Конвертер получает список значений всех привязок 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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js