Пользователь
0,0
рейтинг
16 января в 15:34

Разработка → BindableConverter для WPF recovery mode

C#*
Проблема: WPF классная технология, но местами недоработанная. Например, вот такой код выплюнет не помню точно какой Exception, поскольку ConverterParameter не является наследником DependencyObject'a:

<...Text={Binding SourceProperty, Converter={StaticResource SomethingToSomethingElseConverter} ConverterParameter={Binding AnotherSourceProperty}} />

Собственно, это и проблема. А ниже ее решение.

Обновление от 18.01.16:
1) Если базовый класс конвертера унаследовать от Freezable, то прокси просто не нужен. В таком случае конвертер работает в точности как работал бы обычный:
<что-то.Resources>
<converters:такой-то_конвертер x:Key="такой-то_ключ" BindingParameter1="{Binding такое-то_свойство}" />
</что-то.Resources>

… пожалуйста, учтите это при чтении!


В принципе, решить проблему можно двумя путями: первое DependencyProperty.Register(..), второе — .RegisterAttached(..). Разница в том, что второй вариант и концептуально, и архитектурно ущербный. Поэтому вот так:

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace BindableConverter
{
    [ValueConversion(typeof(object), typeof(object))]
    public class BindableConverterBase : DependencyObject, IValueConverter, IMultiValueConverter
    {
        #region BindableParameters

        #region BindableParameter1
        public object BindableParameter1
        {
            get { return GetValue(BindableParameter1Property); }
            set { SetValue(BindableParameter1Property, value); }
        }

        public static readonly DependencyProperty BindableParameter1Property = DependencyProperty.Register(
                nameof(BindableParameter1),
                typeof(object),
                typeof(BindableConverterBase),
                new PropertyMetadata(String.Empty)
                );
        #endregion

        #region BindableParameter2
        public object BindableParameter2
        {
            get { return GetValue(BindableParameter2Property); }
            set { SetValue(BindableParameter2Property, value); }
        }

        public static readonly DependencyProperty BindableParameter2Property = DependencyProperty.Register(
                nameof(BindableParameter2),
                typeof(object),
                typeof(BindableConverterBase),
                new PropertyMetadata(String.Empty)
                );
        #endregion

        #region BindableParameter3
        public object BindableParameter3
        {
            get { return GetValue(BindableParameter3Property); }
            set { SetValue(BindableParameter3Property, value); }
        }

        public static readonly DependencyProperty BindableParameter3Property = DependencyProperty.Register(
                nameof(BindableParameter3),
                typeof(object),
                typeof(BindableConverterBase),
                new PropertyMetadata(String.Empty)
                );
        #endregion

        #endregion  

        #region IValueConverter
        public virtual object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
        #endregion

        #region IMultiValueConverter
        public virtual object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        public virtual object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
        #endregion
    }
}


Поскольку класс реализовал оба интерфейса, то будет работать и с обычным Binding'ом, и с MultiBinding. Собственно, осталось только унаследоваться от BindableConverter, переопределив нужный (или нужные, что редкость) методы.

В использовании в XAML есть один принципиальный момент. Наивная попытка типа:
<Window.Resources>
 <local:NameAndAgeToVladimirPutinConverter x:Key="NameAndAgeToVladimirPutin" 
                 BindableParameter1="{Binding FirstName}"
                 BindableParameter2="{Binding Age}" />
</Window.Resources>

… приведет к тому, что оба параметра будут равны дефолтному значению. Всегда — независимо от того, где именно в ресурсах вы объявите ссылку на конвертор.

Признаюсь, окончательного понимания почему так происходит, у меня нет. В общих словах смысл в том, что конвертор находится вне Logical\VisualTree UI-элементов, поэтому привязаться ему просто не к кому. Во всяком случае, именно такое объяснение я раскопал на StackOverflow. Решение проблемы выглядит вот так:

<bc:BindingProxy x:Key="BindingProxy" Data="{Binding}" />
<local:NameAndAgeToVladimirPutinConverter x:Key="NameAndAgeToVladimirPutin" 
              BindableParameter1="{Binding Source={StaticResource BindingProxy}, Path=Data.FirstName}"
              BindableParameter2="{Binding Source={StaticResource BindingProxy}, Path=Data.Age}"/>
...
<TextBlock Grid.Row="0" Text="{Binding Name, Converter={StaticResource NameAndAgeToVladimirPutin}}" />


using System.Windows;

namespace BindableConverter
{
    public class BindingProxy : Freezable
    {
        protected override Freezable CreateInstanceCore()
        {
            return new BindingProxy();
        }

        /// <summary>
        /// Binding data.
        /// </summary>
        public object Data
        {
            get { return GetValue(DataProperty); }
            set { SetValue(DataProperty, value); }
        }

        public static readonly DependencyProperty DataProperty = DependencyProperty.Register(
                nameof(Data), 
                typeof(object),
                typeof(BindingProxy), 
                new UIPropertyMetadata(null)
            );
    }
}


P.S. В планах есть прикрутить MarkupExtension, чтобы можно было вытворять что-то типа Converter={bc:BindableConverter BindingProxy={StaticResource Proxy}, Parameter1=FirstName, Parameter2=Age}. Если у кого-то есть идеи как это сделать еще красивее и лаконичнее, то пожалуйста.

Также очень актуален вопрос о том, почему все таки без BindingProxy не работает.
@52hertz
карма
0,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (8)

  • 0
    Exception, поскольку ConverterParameter не является наследником DependencyObject'a

    Да ну? Пример с эксепшном не приведёте?
    • 0
      ну вот прям чесслово! сами попробуйте прибиндить ConverterParameter куда-нить!
      • 0
        Да, работать не будет, вы правы. Но вы как-то скомкано описали причину. Binding.ConverterParameter — не DependencyProperty, поэтому ему нельзя присвоить Binding, вот и всё.
        Также очень актуален вопрос о том, почему все таки без BindingProxy не работает.

        Смысл ответа, который вы нашли, близок к истине. Создаваемый объект конвертера не наследует DataContext окна. Да и как ему это делать? Мало того, что он не визуальный элемент, так у него даже свойства DataContext нет. :) Где ему искать FirstName и Age?

        Поясните, а зачем этот велосипед? Почему бы просто не использовать MultiBinding / IMultiValueConverter?

        • +1
          Поясните, а зачем этот велосипед? Почему бы просто не использовать MultiBinding / IMultiValueConverter?


          По этой же причине хотел вступить в полемику с автором поста, но зашел в его профиль и посмотрел предыдущие записи в блоге… После этого, желание полемизировать отпало…
        • 0
          Ну да, не депендеси. Я по-моему про это и написал.

          Я все-таки не считаю это велосипедом. Не знаю, следите ли вы за интервью и выступлениями команды, которая занимается wpf, на 9channel, но и они признают, что wpf нужно развивать также в сторону все более и более легкого, удобного, интуитивно понятного использования конечным пользователем вроде нас с вами. Лично мне ужасно не нравится печатать многа букаф, поэтому вариант с
          <MultiBinding> <Bindint 1> <Binding 2> ...
          
          отпадает. Да и места занимает гораздо больше (как на мой вкус — одна из самых неприятных проблем с XAML'ом — количество текста).

          Кстати, если конвертор унаследовать напрямую от Freezable, то прокси не нужен. И тогда объявление конвертора — в точности такое же, как обычного — где-нибудь в ресурсах, только с использованием лаконичного markup-ex {Binding Name}. Собственно, для меня смысл в этом.
          • 0
            А не проще запилить именованый инстанс вспомогательного класса и к нему как к ресурсу привязаться. Конвертер же тоже ресурс, он DependencyObject из чисто архитектурных соображений не может быть. Т.е. можно нагородить велосипедов, но зачем?!
            • 0
              В каком смысле — не проще? Не проще в смысле экономии времени? Да, в этом (и только в этом!) смысле — действительно проще. Но ИМХО это засоряет код, что уже само по себе смертный грех. И почему это конвертер не может быть объектом зависимости? Кто именно запрещает? По-моему, это очень логично: конвертору для нормальной работы в условиях реальных задач необходимы вспомогательные параметры; по сути, результат конвертации ЗАВИСИТ от этих параметров, что можно перевести в «конвертор (конкретный экземпляр соотв. класса) зависит от этих параметров». Что еще нужно?
              • 0
                A IMultiValueConverter тогда вам чем не угодил? А, ну да, букв много.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.