company_banner

XAML: Вложенные конвертеры

    Интро

    В XAML (SilverLight /Wpf /Metro) конвертеры используются для самых различных целей: приведение типов, форматирование строк, калькуляция скалярного значения сложного объекта. В рамках проекта мы можем создать очень много классов-конвертеров, решающих смежные задачи (вычисление состояния заказа и конвертация его в Visibility, конвертация состояния заказа в Cursor, конвертация булевого значения в Visibility/Invisibility и т.д.). Нетривиальная ситуация: мы написали конвертер для необычно сложного форматирования TimeSpan, и теперь требуется форматировать Duration таким же образом – необходимо писать аналогичный конвертер, но уже с предварительной распаковкой TimeSpan из Duration. Вариантов преобразования строк может быть множество, и для всех преобразований потребуется такое же множество конвертеров.
    Естественно, стараясь обобщить код, мы разбиваем конвертацию на более мелкие процедуры, и, как следствие, у нас встречаются классы-конвертеры, состоящие из двух строчек кода, используемые только один раз.
    Многие не знают, что для упрощения ситуации и уменьшения количества строчек кода, возможно комбинирование преобразований не в классах конвертеров, но в XAML разметке, путем создания цепочек конвертеров. Для этого необходимо написать свой абстрактный конвертер, от которого мы будем наследовать все наши преобразования.

    Реализация

    Создадим конвертер, который перед собственным абстрактным преобразованием выполнит преобразование другого, вложенного конвертера.
    Декларация класса:
    [ContentProperty("Converter")]
    public abstract class ChainConverter : IValueConverter
    

    Атрибут ContentProperty – указывает свойство, которое будет использоваться в разметке XAML неявно.
    Далее в классе описываем сам вложенный конвертер:
    public IValueConverter Converter { get; set; }
    

    Разрешаем ему быть IValueConverter – это даст нам возможность использовать уже существующие конвертеры в качестве вложенного.
    Код конвертации прост:
    object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (Converter != null)
            value = Converter.Convert(value, ThisType ?? targetType, parameter, culture);
        return Convert(value, targetType, parameter, culture);
    }
    
    public abstract object Convert(object value, Type targetType, object parameter, CultureInfo culture);
    

    Поясню свойство ThisType. Оно объявляет тип значения, которое мы ожидаем на входе (на выходе из вложенного конвертера). Субконвертер, возможно, умеет вычислять значения разных типов и если в случае с простой привязкой – это идеально (один и тот же конвертер используется для разных целевых типов), то в нашем случае мы, скорее всего, не хотим, что бы вложенный конвертер знал тип конечной цели. Если есть уверенность, что разрабатываемый конвертер не будет использоваться в качестве контейнера для других конвертеров, меняющих своё поведение в зависимости от переданного из привязки значения targetType, то можно не переопределять это свойство – по умолчанию оно вернёт null. (В общем случае, мы не можем знать, как будет использован конвертер и из-за отсутствия типизации в конвертерах в худшем случае можем не получить ни compile-time, ни run-time ошибок, поэтому советую указывать внутренний целевой тип как можно чаще)

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

    В этом примере мы реализуем два простых преобразования: BoolToVisibilityConverter и InvertBooleanConverter. Идея, думаю, понятна: при установке значения true — элемент управления будет прятаться, при установке false – показываться.
    Код BoolToVisibilityConverter:
    public class BoolToVisibilityConverter : ChainConverter
    {
        public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return (bool) value ? Visibility.Visible : Visibility.Collapsed;
        }
    
        public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }
    

    Код InvertBooleanConverter:
    public class InvertBooleanConverter : ChainConverter
    {
        public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return !(bool) value;
        }
    
        public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }
    

    Использование:
    <TextBlock Text="Контрольный текст">
        <TextBlock.Visibility>
            <Binding Path="IsChecked" ElementName="checkBox">
                <Binding.Converter>
                    <Converters:BoolToVisibilityConverter>
                        <Converters:InvertBooleanConverter />
                    </Converters:BoolToVisibilityConverter>
                </Binding.Converter>
            </Binding>
        </TextBlock.Visibility>
    </TextBlock>
    <CheckBox Content="Невиден ли контрольный текст" x:Name="checkBox" />
    

    Ссылка на проект
    Если ещё не понятно, как можно использовать эту технику, представьте себе такой код:
    <TextBlock>
        <TextBlock.Text>
            <Binding Path="Order">
                <Binding.Converter>
                    <TakeFirstNSymbolsConverter SymbolsCount="5">
                        <OrderStateToStringConverter>
                            <OrderToOrderStateConverter />
                        </OrderStateToStringConverter>
                    </TakeFirstNSymbolsConverter>
                </Binding.Converter>
            </Binding>
        </TextBlock.Text>
    </TextBlock>
    

    Здесь я использовал 3 конвертера: самый глубокий — OrderToOrderStateConverter –вычисление статуса заказа, на уровень выше – конвертирование статуса заказа в строку, и последний (первый в коде) – извлечении из строки 5 первых символов (В проекте есть подобный пример работы со строками).

    Заключение

    Имея ChainConverter на борту, просто наследуйте новые конвертеры от него. И в какой-то момент вместо создания нового конвертера, вам нужно будет лишь скомбинировать два или более уже реализованных.
    Этот подход сильно облегчает жизнь, если какой-то вид конвертации у нас встречается в коде один раз. Если мы используем одну и ту же комбинацию конвертеров более одного раза – остается целесообразным объявить новый класс. Благодаря описанному подходу новый конвертер мы можем описать в XAML разметке.
    Tinkoff.ru
    it’s Tinkoff.ru — просто о сложном

    Похожие публикации

    Комментарии 6

      0
      Мне привычнее смотреть на конвертеры с т.з. теории категорий.
      необходим id-конвертер без преобразований, и конвертер, соединяющий два других.
      ComposeConverter c = new ComposeConverter(new BtoC(), new AtoB());
      

      В точности, как операция композиции функций в фп языках.
        0
        Можно создать конвертер, объединяющий другие в цепочку, а можно добавить этот механизм к механизму markup extension и тогда цепочку можно записывать в строку, а не иерархически, что иногда более понятно.
        А ещё нужно обратное конвертирование поддерживать в базисном классе.
          0
          Да, в проекте по ссылке лежит весь код, там реализовано обратное конвертирование по тому-же принципе.

          Через Markup Extension много всего можно сделать, вплоть до своего механизма связывания данных
        0
        Сама идея очень неплохая, но по-моему в большинстве случаев лучше будет создать дополнительное свойство в классе модели.
          0
          Это — Декларативное программирование VS Императивное программирование. Кому как больше нравится — мне больше нравится по-максимуму все декларировать — Ioc, XAML, атрибуты и т.д.
          + Дополнительное свойство в модели обязует нас использовать MVVM. В указанном примере я бы не хотел использовать MVVM, что бы не увеличивать количество строчек кода и ошибок.

          0
          В функции «object Convert(object value, Type targetType, object parameter, CultureInfo culture)» возникает исключение, если просто переключать пользователей Windows 8.1, оказалось, только в этом случае в переменной «value» всегда приходит NULL. Чем вызвано такое поведение? В программе выполняется статический биндинг ComboBox'a к перечислению Enum. Конвертер используется для получения у полей перечисления свойства «Description».

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

          Самое читаемое