Blue Flower

Одной из ключевых концепция в XAML-ориентированной разработке является механизм привязки данных [data binding mechanism].
Привязка (Binding) - это медиатор посредством которого значения свойств одного или более объектов синхронизируются между собой.
Очень важно то, что привязки не удерживают взаимодействующие объекты от сборки мусора [garbage collection] благодяря использоваию механизмов слабых ссылок и подписок [weak reference and subscription mechanisms]!

Наследование от класса Binding допустимо, но по соображеиям безопасности переопределение метода ProvideValue [который тесно связан с основной логикой] запрещено. Это провоцирует разработчиков на использование паттерна Конвертер [Converter]. Привязки - очень мощный инструмент, но в некоторых случаях их декларация получается многословной и неудобной при регулярном использовании, например, для локализации. В этой статье мы обсудим простой и элегантный путь, основанный на заказных расширениях привязки [custom binding extensions], для создания кода намного более чистого и защищённого от утечек памяти [memory leaks].


Существует два способа декларации привязки:

<TextBlock>
	<TextBlock.Text>
		<Binding ...>
	</TextBlock.Text>
</TextBlock>

<TextBlock Text="{Binding ...}"/>

Очевидно, что второй метод основанный на расширениях разметки [markup extensions] выглядит более лаконично. На некоторых XAML-платформах, таких как WPF или классический Silverlight, существует возможность создавать custom markup extensions путём наследования от класса MarkupExtension и переопределения метода ProvideValue. К примеру, это полезно для локализации.

<TextBlock Text="{Localizing AppTitle}"/>

Но наиболее простые реализации класса Localizing не поддерживают горячую смену языка во время выполнения приложения. Чтобы осуществить это улучшение, важно, во-первых, to store a link to the localized interface element, second, less obvious, anyway have application link to itself instance class Localizing to protect it from the garbage collection, and, thirdly, required to competently implement subscriptions and unsubscribe from the event to change the language.

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

Более того класс MarkupExtension не доступен на некоторых других платформах, например в Windows Phone Silverlight.
Всё это наталкивает на идею использования Привязки в качестве custom markup extension...

Взгляните на класс ABindingExtension:

  
    public abstract class ABindingExtension : Binding, IValueConverter
    {
        protected ABindingExtension()
        {
            Source = Converter = this;
        }
 
        protected ABindingExtension(object source) // Source, RelativeSource, null for DataContext
        {
            var relativeSource = source as RelativeSource;
            if (relativeSource == null && source != null) Source = source;
            else RelativeSource = relativeSource;
            Converter = this;
        }
 
        public abstract object Convert(object value, Type targetType, object parameter, CultureInfo culture);
 
        public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

Примечательно, что привязка [Binding] является конвертером [Converter] для самой себя. В результате получаем поведение подобное наследованию от класса MarkupExtension, но также остаётся возможность использования стандартных механизмов контроля сборки мусора!

Теперь логика для локализации выглядит очень просто, поддерживает горячцю смену языка и защищена от утечек памяти!


    public class Localizing : Patterns.ABindingExtension
    {
        public Localizing()
        {
            Source = LocalizationSource.Wrap;
            Path = new PropertyPath(LocalizationSource.Wrap.ActivePath);
        }

        public Localizing(string key) : this()
        {
            Key = key;
        }

        public string Key { get; set; }

        public Letter Case { get; set; }

        public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return LocalizationSource.Wrap[Key, culture].To(Case);
        }
    }

Синтаксис XAML выглядит аккуратно, но существуют некоторые ограничения накладываемые парсерами раметки на различных платформах:

<!--WPF-->
<TextBlock Text="{Localizing AppTitle, Case=Upper}"/>
<TextBlock Text="{Localizing Key=AppDescription}"/>

<!--WPF, Windows Phone-->
<TextBlock Text="{m:Localizing Key=AppTitle, Case=Upper}"/>
<TextBlock Text="{m:Localizing Key=AppDescription}"/>

<!--WPF, Windows Phone, Windows Store-->
<TextBlock>
	<TextBlock.Text>
		<m:Localizing Key=AppDescription>
	</TextBlock.Text>
</TextBlock>

Чтобы избежать на WPF обязательного префикса m:, следует поместить markup extension в отдельную сборку и указать в Properties/AssemblyInfo.cs следующие директивы:

[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "Aero.Markup")]
[assembly: XmlnsPrefix("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "m")]

To control the prefix name on Windows Phone or Store:

[assembly: XmlnsDefinition("clr-namespace:Aero.Markup;assembly=Aero.Phone", "Aero.Markup")]
[assembly: XmlnsPrefix("clr-namespace:Aero.Markup;assembly=Aero.Phone", "m")]

Использование класса ABindingExtension не исключает классического MarkupExtension, но в некоторых случаях первый предоставляет более безопасный и простой путь. Конечно, всё это не ограничено только локализацией и пригодно для множества других целей...

Полные исходные коды

• Makeloft Foundation (Google Drive) •

• Makeloft Foundation (Dropbox) •