WPF: конвертеры как MarkupExtension

    Конверторы являются одной из важнейшей особенностью механизма привязки в WPF. Они позволяют управлять тем, как источник привязки будет представлен в UI. В данной статье я покажу, как немного упростить использование конвертеров в XAML коде.

    Рассмотрим простейший пример:
    public class DateConverter : IValueConverter
    {
    	public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    	{
    		DateTime date = (DateTime)value;
    		return date.ToShortDateString();
    	}
    
    	public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    	{
    		return null;
    	}
    }
    

    Тут всё просто: конвертер на вход получает значение типа DateTime и конвертирует его в строку. Обратная конвертация не предусмотрена.

    Используется конвертер следующим образом:
    <Window x:Class="TestConvertorMarkup.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:converters="clr-namespace:TestConvertorMarkup.Converters"
            Title="MainWindow" Height="350" Width="525">
        <Window.Resources>
            <converters:DateConverter x:Shared="false" x:Key="dateConverter"/>
        </Window.Resources>
    	
        <Label Content="{Binding Path=Date, Converter={StaticResource dateConverter}}" />
    </Window>
    

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

    Вначале модифицируем сам конвертер:

    public class NumberToStringConverterExtension: MarkupExtension, IValueConverter
    {
    	public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    	{
    		DateTime date = (DateTime)value;
    		return date.ToShortDateString();
    	}
    
    	public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    	{
    		throw new NotImplementedException();
    	}
    
    	public override object ProvideValue(IServiceProvider serviceProvider)
    	{
    		if (_converter == null)
    			_converter = new NumberToStringConverterExtension();
    		return _converter;
    	}
    
    	private static NumberToStringConverterExtension _converter = null;
    }
    

    После такой модификации всё, что нужно для его использования в XAML, это:
    <Label Content="{Binding Path=Date, Converter={converters:DateTimeToString}}" />
    

    Естественно. нужно не забыть добавить соотвествуещее пространство имён «converters».
    Приятным бонусом будет то, что при наборе отображается список доступных конвертеров:

    Давайте не будем останавливаться на достигнутом, а для того, чтобы максимально упростить написание новых конвертеров, введём базовый класс:
    public abstract class ConvertorBase<T> : MarkupExtension, IValueConverter
    	where T : class, new()
    {
    	/// <summary>
    	/// Must be implemented in inheritor.
    	/// </summary>
    	public abstract object Convert(object value, Type targetType, object parameter,
    		CultureInfo culture);
    
    	/// <summary>
    	/// Override if needed.
    	/// </summary>
    	public virtual object ConvertBack(object value, Type targetType, object parameter,
    		CultureInfo culture)
    	{
    		throw new NotImplementedException();
    	}
    
    	#region MarkupExtension members
    
    	public override object ProvideValue(IServiceProvider serviceProvider)
    	{
    		if (_converter == null)
    			_converter = new T();
    		return _converter;
    	}
    
    	private static T _converter = null;
    
    	#endregion
    }
    

    Теперь унаследуем от него наш DateConverter и имплементируем в нём метод Convert. Окончательная версия будет выглядеть так:
    public class DateTimeToString : ConvertorBase<DateTimeToString>
    {
    	public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    	{
    		DateTime date = (DateTime)value;
    		return date.ToShortDateString();
    	}
    
    	public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    	{
    		return null;
    	}
    }
    

    XAML код остаётся идентичен второму примеру.

    Таким образом, мы получили возможность использовать упрощённый синтаксис в XAML разметке, а код конвертера практически не изменился.

    P.S. Проекты с примерами можно скачать здесь.
    Share post

    Similar posts

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

    More
    Ads

    Comments 17

    • UFO just landed and posted this here
      0
      а зачем по две пустые строки после каждой строчки кода?
        +4
        Это не пустые строки, это дурацкие css хабра. Во всех статьях с кодом так. Это отстой. Почему администрация это оставляет, хотя народ постоянно жалуется — загадка.
          +4
          Можно пользоваться тегом <source lang="cs">
        +5
        Насколько я понимаю достаточно просто вернуть себя из функции ProvideValue, поскольку объект одновременно является и конвертером и уже создан. Так зачем же создавать ещё один? Пользуюсь этой системой уже давно. Пока проблем не заметил.

        public override object ProvideValue(IServiceProvider serviceProvider){
            return this;
        }


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

        <Label Content="{Binding Path=Date, Converter={converters:DateConverter Format=ShortString, Calendar=Gregorian}}" />
        
        public class DateTimeToString : ...
        {
            public override object Convert(...){...}
        
            public string Format{get;set;}
            public CalendarType Calendar{get;set;}
        }
        

        Параметры конечно только для примера, но иногда нужно определить более одного параметра и ConverterParameter с этим не справляется
          0
          «Насколько я понимаю достаточно просто вернуть себя из функции ProvideValue, поскольку объект одновременно является и конвертером и уже создан. Так зачем же создавать ещё один? „
          А и правда, зачем. Видимо, это избыточно…

          “ но иногда нужно определить более одного параметра и ConverterParameter с этим не справляется»
          О, спасибо большое, это действительно классный способ!

          Если вы не возражаете, я дополню вашим кодом статью.
            0
            Конечно можно.
            В принципе есть некоторое преимущество в использовании статического конвертера. Например, если вам нужно создать дата темплейт, включающий конвертер и использовать его для длинного списка элементов в ItemsControl. Тогда новый экземпляр конвертера будет создан, а главное прикреплен к каждому Item. Если же используется статический элемент, то будет создан и тут же уничтожен конвертер как MarkupExtension, а привязанный надолго статический конвертер останется в одном экземпляре. Жаль что тогда теряется возможность использования параметров. Или просто при таком использовании нужно просто воспользоваться старым способом с ресурсами и тогда единичный экземпляр конвертера будет создан как ресурс, а в качестве временного MarkupExtension будет выступать StaticResource. По поводу DynamicResource не уверен, что он сам не зависает в памяти надолго. Я бы использовал ваш вариант для конвертера без параметров и мой для конвертера с параметрами.
            0
            Не просто можно вернуть себя из функции ProvideValue, но и нужно, т.к. в противном случае значения дополнительных параметров не будут проинициализированы, т.к. создаётся новый экземпляр конвертора.

            И да, огромный респект автору за статью!
            0
            Что ещё хуже, подобное использование приводит к тому, что при каждом использовании конвертера пораждается новый экземпляр, что может сильно увеличить количество потребляемой памяти.

            А разве если использовать StaticResource не используется тот же самый объект?

            За метод огромное спасибо! Надоело постоянно ресурсы создавать, а то что предложил urrri это вообще праздник какой-то ))
              +1
              Пример забавный. Но именно такую задачу, я бы решал вот так:
              <TextBlock Text="{Binding Path=Date,StringFormat={}{0:d}}" />
              


                0
                Пример выбран простейших, чтобы не заморачиваться с кодом самой конвертации, статья не про то.
                +1
                Да, в этом автор статьи погорячился. Используется один и тот же объект. Отключается это с помощью аттрибута x:Shared=«false». По умолчанию это значение = true.

                Статья интересная, спасибо, возможно будем использовать подобный подход в своих проектах.
                  0
                  «Используется один и тот же объект. Отключается это с помощью аттрибута x:Shared=«false». По умолчанию это значение = true.»
                  Да я давно уже с ресурсами не возился, думал по дефолту стоит как раз-таки false.
                  Спасибо за замечание, вечером отредактирую статью.

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