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

в 8:43, , рубрики: .net, silverlight
Интро

В 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 разметке.

Автор: gorbovoy

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


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