Расширения привязки и xaml-разметки на примере локализации
Одним из ключевых моментов в разработке xaml-ориентированных приложений является использование привязок (Bindings). Привязка — это медиатор (посредник), с помощью которого синхронизируются значения свойств между связанными объектами.Стоит отметить не очевидный, но важный нюанс: хотя привязка так или иначе ссылается на взаимодействующие объекты, она не удерживает их от сборки мусора!
Наследование от класса Binding разрешено, но в целях безопасности кода переопределение метода ProvideValue, который связан с основной логикой работы, не допускается. Это так или иначе провоцирует разработчиков на применение паттерна Converter, который тесно переплетается с темой привязок.
Привязки очень мощный инструмент, но в некоторых случаях их декларирование получается многословным и неудобным при регулярном использовании, например, для локализации. В этой статье мы разберём простой и элегантный способ, делающий код намного более чистым и красивым.Объявлять привязки в xaml допустимо двумя образами:
Неправильно выполнив эти моменты, вы гарантировано получите утечки памяти, если представления создаются и исчезают динамически в процессе работы приложения, а во многих случаях это именно так. То есть, добавляя казалось бы не самую сложную функцию, придётся столкнуться с нетривиальными темами слыбых ссылок и слабых подписок на события. Да и код получится не очень простой.
Более того, на 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 запись выглядит удобно и красиво, но есть некоторые ограничения парсеров разметки на различных платформах: