Расширения привязки и xaml-разметки на примере локализации

в 13:33, , рубрики: .net, silverlight, windows phone, windows store, wpf, XAML, Программирование, метки:

Одним из ключевых моментов в разработке xaml-ориентированных приложений является использование привязок (Bindings). Привязка — это медиатор (посредник), с помощью которого синхронизируются значения свойств между связанными объектами.

Стоит отметить не очевидный, но важный нюанс: хотя привязка так или иначе ссылается на взаимодействующие объекты, она не удерживает их от сборки мусора!

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

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

Объявлять привязки в xaml допустимо двумя образами:

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

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

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

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

В простейшем случае нужно унаследоваться от класса MarkupExtension и реализовать метод ProvideValue, в котором по ключу получить нужное значение.

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

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

Более того, на xaml-платформах Windows Phone, Windows Store и Xamarin.Forms нет возможности создавать пользовательские расширения разметки, что наталкивает на идею использования привязок в качестве расширений разметки

Не будем ходить вокруг да около, вот то, что нам нужно:

    public abstract class BindingExtension : Binding, IValueConverter
    {
        protected BindingExtension()
        {
            Source = Converter = this;
        }

        protected BindingExtension(object source) // set Source to null for using DataContext
        {
            Source = source;
            Converter = this;
        }

        protected BindingExtension(RelativeSource relativeSource)
        {
            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();
        }
    }

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

Теперь логика для локализации выглядит проще некуда:

    public partial class Localizing : Base.BindingExtension
    {
        public static readonly Manager ActiveManager = new Manager();

        public Localizing()
        {
            Source = ActiveManager;
            Path = new PropertyPath("Source");
        }

        public Localizing(string key)
        {
            Key = key;
            Source = ActiveManager;
            Path = new PropertyPath("Source");
        }

        public string Key { get; set; }

        public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var key = Key;
            var resourceManager = value as ResourceManager;
            var localizedValue = resourceManager == null || string.IsNullOrEmpty(key)
                ? ":" + key + ":"
                : (resourceManager.GetString(key) ?? ":" + key + ":");

            return localizedValue;
        }
    }

    public partial class Localizing
    {
        public class Manager : INotifyPropertyChanged
        {
            private ResourceManager _source;

            public ResourceManager Source
            {
                get { return _source; }
                set
                {
                    _source = value;
                    PropertyChanged(this, new PropertyChangedEventArgs("Source"));
                }
            }

            public string Get(string key, string stringFormat = null)
            {
                if (_source == null || string.IsNullOrWhiteSpace(key)) return key;
                var localizedValue = _source.GetString(key) ?? ":" + key + ":";
                return string.IsNullOrEmpty(stringFormat)
                    ? localizedValue
                    : string.Format(stringFormat, localizedValue);
            }

            public event PropertyChangedEventHandler PropertyChanged = (sender, args) => { };
        }
    }

Легко добавить возможность для смены регистра букв:

    public partial class Localizing : Base.BindingExtension
    {
        public enum Cases
        {
            Default,
            Lower,
            Upper
        }

        public static readonly Manager ActiveManager = new Manager();

        public Localizing()
        {
            Source = ActiveManager;
            Path = new PropertyPath("Source");
        }

        public Localizing(string key)
        {
            Key = key;
            Source = ActiveManager;
            Path = new PropertyPath("Source");
        }

        public string Key { get; set; }
        public Cases Case { get; set; }

        public override string ToString()
        {
            return Convert(ActiveManager.Source, null, Key, Thread.CurrentThread.CurrentCulture) as string ??
                   string.Empty;
        }

        public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var key = Key;
            var resourceManager = value as ResourceManager;
            var localizedValue = resourceManager == null || string.IsNullOrEmpty(key)
                ? ":" + key + ":"
                : (resourceManager.GetString(key) ?? ":" + key + ":");

            switch (Case)
            {
                case Cases.Lower:
                    return localizedValue.ToLower();
                case Cases.Upper:
                    return localizedValue.ToUpper();
                default:
                    return localizedValue;
            }
        }
    }

В 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: нужно поместить расширение разметки в отдельную сборку и в 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")]

Для регулирования имени префикса на Windows Phone или Store:

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

Использование расширений привязки (Binding Extensions) на WPF не исключает обычных расширений разметки, но в некоторых случаях является даже более безопасным и простым вариантом.

Продемонстрированный подход интенсивно используется в библиотеке Aero Framework, о которой было рассказано несколько ранее. К ней также прилагается пример проекта, где вы можете увидеть все эти механизмы в действии. Благодарю за внимание!

Автор: Makeman

Источник


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


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