Разберём интересный и нестандартный сценарий использования конвертеров — Inline Converter.
image
Наверно, некоторые разработчики сталкивались с той проблемой, что при использовании конвертеров в параметры конвертера не передаётся информации о представлении, его контексте данных либо самом визуальном элементе, к которому осуществлена привязка. С одной стороны это хорошо, получается некоторая защита и разделение логики, не слишком правильно в конвертере напрямую работать с контролом, с другой же стороны в редких случаях именно из-за такого ограничения приходится идти на различные ухищрения.

Старая добрая событийная модель по-прежнему не утратила своей актуальности даже несмотря на то, что получил развитие мощный и эффективный механизм привязки данных (Data Binding). Конечно, не стоит использовать события в ущерб прогрессивным средствам разработки, но иногда их применение получается удобным и естественным.

Почему бы не скомбинировать оба этих способа? К примеру, таким образом

ICompositeConverter
using System.Windows.Data;

namespace Aero.Converters.Patterns
{
    public interface ICompositeConverter : IValueConverter
    {
        IValueConverter PostConverter { get; set; }
        object PostConverterParameter { get; set; }
    }
}


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

namespace Aero.Converters.Patterns
{
    public class ConverterEventArgs : EventArgs
    {
        public object ConvertedValue { get; set; }
        public object Value { get; private set; }
        public Type TargetType { get; private set; }
        public object Parameter { get; private set; }
        public CultureInfo Culture { get; private set; }

        public ConverterEventArgs(object value, Type targetType, object parameter, CultureInfo culture)
        {
            TargetType = targetType;
            Parameter = parameter;
            Culture = culture;
            Value = value;
        }
    }

    public interface IInlineConverter : IValueConverter
    {
        event EventHandler<ConverterEventArgs> Converting;
        event EventHandler<ConverterEventArgs> ConvertingBack;
    }

    //public interface IInlineConverter : IValueConverter
    //{
    //    event Func<object, Type, object, CultureInfo, object> Converting;
    //    event Func<object, Type, object, CultureInfo, object> ConvertingBack;
    //}
}


InlineConverter
using System;
using System.Globalization;
using System.Windows.Data;
using Aero.Converters.Patterns;

namespace Aero.Converters
{
    public class InlineConverter : IInlineConverter, ICompositeConverter
    {
        public IValueConverter PostConverter { get; set; }
        public object PostConverterParameter { get; set; }
        public event EventHandler<ConverterEventArgs> Converting;
        public event EventHandler<ConverterEventArgs> ConvertingBack;

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var args = new ConverterEventArgs(value, targetType, parameter, culture);
            var handler = Converting;
            if (handler != null) handler(this, args);
            return PostConverter == null
                ? args.ConvertedValue
                : PostConverter.Convert(args.ConvertedValue, targetType, PostConverterParameter, culture);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var args = new ConverterEventArgs(value, targetType, parameter, culture);
            var handler = ConvertingBack;
            if (handler != null) handler(this, args);
            return PostConverter == null
                ? args.ConvertedValue
                : PostConverter.ConvertBack(args.ConvertedValue, targetType, PostConverterParameter, culture);
        }
    }
}


Что мы получаем? Экземпляр конвертера нам нужно встроить в ресурсы контрола или представления, а в бехаин коде (Code Behind) представления сделать нужные обработчики для событий Converting и ConvertingBack, после чего эти события станут вызываться во время срабатывания привязки, а в обработчиках через указатель this будет доступно как само представление c визуальным деревом, так и контекст данных! Неожиданно получилась большая свобода действий, к тому же всё осталось идеологически верно, ведь в сам конвертер не попало интерфейсной логики, а она осталась лишь в бехаин коде.

Вот простой пример использования этого подхода

<Grid>
    <Grid.Resources>
        <InlineConverter x:Key="InlineConverter" Converting="InlineConverter_OnConverting"/>
    </Grid.Resources>
    <TextBlock Text="{Binding Number, Converter={StaticResource InlineConverter}}"/>
</Grid>

        private void InlineConverter_OnConverting(object sender, ConverterEventArgs e)
        {
            e.ConvertedValue =
                string.Format("Title: {0} \nDataContext:\n{1} \nConverter Value: {2}",
                    Title,
                    DataContext,
                    e.Value);
        }

Дополнительное использование паттерна Composite Converter [ICompositeConverter ] позволяет объединять различные конвертеры в цепочки, модифицируя логику, без необходимости создания новых классов.

Увидеть в действии Inline Converter и некоторые другие можно в демонстрационном проекте HelloAero библиотеки Aero Framework [резервная ссылка].

Благодарю за внимание!

P.S. Предыдущая статья о динамическом Grid

Update
Применение
Необходимость в представленном методе на практике возникает нечасто, однако в то же время он может крайне облегчить жизнь в «неудобных» ситуациях. К примеру, таким образом можно заместить MultiBinding при портировании кода с WPF на Windows Phone, поскольку на мобильной платформе множественная привязка не поддерживается. Просто помните о встраиваемых конвертерах и в нужный момент вы найдёте для них применение.

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


  1. impwx
    29.01.2016 18:34

    На первый взгляд выглядит как костыль, подпирающий архитектурную ошибку. Поэтому очень хочется реальный пример ситуации, когда конвертеру нужен контекст.


  1. Makeman
    29.01.2016 20:30

    Согласен, что такие ситуации скорее исключение из правил, но в то же время они очень «неудобные».

    В примере свойство Text привязывается к свойству Number вью-модели через конвертер, но по условию к этому числу нужно добавить заголовок окна приложения.

    Если мы будем делать дополнительное свойство Title во вью-модели, то она будет загрязняться интерфейсным кодом, а также придётся использовать что-то вроде MultiBinding, который, кстати, не на всех платформах поддерживается, плюс создавать IMultiValueConverter. Всё довольно усложняется несмотря на тривиальность задачи. С Inline Converter выглядит просто и понятно.

    Также может возникнуть и другая ситуация: основная привязка свойства Text должна идти к свойству Number во вью-модели, но, например, в зависимости от нескольких других различных значений свойств этой вью модели, сама строка должна декорироваться тем или иным образом. Поскольку в обычном конвертере у нас нет доступа к самой вью-модели, приходится искать обходные пути, не всегда красивые. В нашем же случае есть прямой доступ к контесту данных представления, нужной нам вью-модели и всем её свойствам.


    1. bstdman
      30.01.2016 16:05

      Описанный вами случай довольно просто решается через MultiBinding и RelativeSource, без использования конвертеров вообще. Другой вопрос мультиплатформенность.

      Но опять же code behind — это лютое зло, которое проблематично тестировать. Для WPF при использовании паттерна MVVM рекомендуется его использование только для изменения состояния представления.


      1. Makeman
        31.01.2016 19:33

        Каким бы ни было ужасным явление Code Behind, стоит признать, что в некоторых ситуациях его применение уместно. Конечно, тут важно не переусердствовать, поэтому ответственность лежит на разработчике.

        Представленный в статье «трюк» позволяет обойти некоторые ограничения при выполнении привязок. Безусловно, когда мы получаем больше свободы стоит помнить о безопасности, однако и возможности наши возратают.


    1. IL_Agent
      31.01.2016 00:01

      В примере свойство Text привязывается к свойству Number вью-модели через конвертер, но по условию к этому числу нужно добавить заголовок окна приложения.

      И вы предлагаете часть конвертации выполнять в самом конвертере, а другую часть — code behind. Это не хорошо. Вся логика должна быть в конвертере.


      1. Makeman
        31.01.2016 19:40

        Не спорю, здесь есть к чему придраться за нарушение «правильной» идеологии :)

        Но, на мой взгляд, лучше использовать универсальный способ для решения обширного круга проблемных ситуаций, чем каждый раз изобретать новый хак.