📜 ⬆️ ⬇️

Binding and xaml markup extensions using localization as an example

One of the key points in the development of xaml-oriented applications is the use of bindings. A binding is a mediator (mediator) with which the property values ​​between the related objects are synchronized.

It is worth noting an obvious, but important, nuance: although the binding somehow refers to interacting objects, it does not keep them from garbage collection!

Inheritance from the Binding class is allowed, but for code security purposes, overriding the ProvideValue method, which is related to the main operation logic, is not allowed. This somehow provokes developers to use the Converter pattern, which is closely intertwined with the theme of bindings.
')
Bindings are a very powerful tool, but in some cases their declaration turns out to be verbose and inconvenient when used regularly, for example, for localization. In this article we will examine a simple and elegant way to make the code much cleaner and more beautiful.


It is permissible to declare bindings in xaml in two ways:

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

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

Obviously, the first method does not look very concise, while the second, based on the use of markup extensions , is used most often. On WPF, it is possible to create custom markup extensions . For example, it is convenient to use them for localization.

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

In the simplest case, you need to inherit from the MarkupExtension class and implement the ProvideValue method, in which you get the desired value using the key.

But such an implementation does not support hot language change during program execution. To make this improvement it is necessary, firstly, to keep a reference to the localizable interface element, secondly, which is less obvious, somehow to have in the application a reference to the Localizing class itself to protect it from garbage collection, and, thirdly, It is required to competently implement a subscription and a formal reply from the event of a language change.

By doing these things incorrectly, you are guaranteed to get memory leaks if views are created and disappear dynamically during the operation of the application, and in many cases this is exactly the case. That is, adding the seemingly not the most complex function, you will have to face the nontrivial themes of weak links and weak subscriptions to events . And the code will not be very simple.

Moreover, on the xaml platforms of Windows Phone , Windows Store and Xamarin.Forms there is no possibility to create custom markup extensions, which suggests the idea of ​​using bindings as markup extensions ...

Let's not beat around the bush, here's what we need:

  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(); } } 

It is noteworthy that the binding is a converter for itself. As a result, we get very similar behavior, as when inheriting from the MarkupExtension class, but, in addition, it remains possible to use standard garbage collection control mechanisms!

Now the logic for localization looks easier nowhere:

  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) => { }; } } 

It is easy to add the ability to change the case of letters:

  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; } } } 

In xaml, the entry looks comfortable and beautiful, but there are some limitations to parser markers on various platforms:

 <!--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> 

To get rid of the mandatory m prefix on WPF : put the markup extension in a separate assembly and specify the following directives in 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 regulate 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")] 

The use of binding extensions ( Binding Extensions ) on WPF does not exclude the usual markup extensions, but in some cases is even more secure and simple option. Also, all this is not limited to just one localization, but is suitable for many other purposes ...

The demonstrated approach is extensively used in the Aero Framework library, which was described earlier . An example of a project is also attached to it, where you can see all these mechanisms in action. Thank you for attention!

Source: https://habr.com/ru/post/254115/


All Articles