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

    В 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.



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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 3

      +1
      Зачем городить конвертер? В WPF это можно сделать и без него:

              <MultiBinding StringFormat="{}{0} {1} {2}">
                  <Binding Path="FirstName" />
                  <Binding Path="MiddleName" />
                  <Binding Path="LastName" />
              </MultiBinding>
      
        0
        Просто для примера
        Про такую возможность StringFormat не знал, спасибо
        –1
        Когда-то давно использовал WPF, то теперь, поработав с WEB-UI фреймворками, смотрю на это, как на код динозавра, хоть это и разные ниши, но слишком все связно и избыточно. Для интерфейса это плохо.

        Only users with full accounts can post comments. Log in, please.