пятница, 11 апреля 2014 г.

Use Converters in your Windows Phone Apps

In a recent post I have covered several approaches to store data in a Windows Phone application. Another important problem is presenting data in an easy and controllable way that would not interfere with other layers of your app. To some extent XAML handles this task well, but from time to time just markup is not enough and one needs to introduce a portion of imperative code to present data nicely. To make this possible each binding in a XAML-based app may specify a converter, which will be responsible for transforming data between internal and external representations.

The key purpose of data converters is to allow for making an application appealing and intuitive without cluttering XAML significantly or, what is much worse, mixing presentation logic into the model layer. Converters are called by the Data Binding mechanism whenever binding actually occurs (for example, when a control is notified about the change in the underlying property). In many cases a converter is used even if you don’t specify any in the controls’ markup – a common example is the following line:

<Image Source="/Assets/Amazing.png" />

If you peek into the declaration of the Source property, you will notice that its type is actually ImageSource – not a string, which means that the file path should be transformed into a suitable object. This is done behind the curtains by the built-in ImageSourceConverter class, which is implicitly used by the Image control. This way converters allow us to specify a natural value in XAML and have it transformed into something suitable for the particular property of a control. While there is a range of built-in converter classes, the mechanism is so flexible and easy to use that you will likely want to introduce your own converters – that’s what we will discuss here.

Let us start with an example, which might be quite useful in real applications despite its stunning simplicity. Suppose that in your model you have a property that should be either visible to users or hidden from them depending on some flag::

class BooleanToVisibilityViewModel 
    : INotifyPropertyChanged
{
  private bool _allowEdit;
  public bool AllowEdit
  {
    get { return _allowEdit; }
    set
    {
      _allowEdit = value;
      OnPropertyChanged("AllowEdit");
    }
  }

  private string _text;
  public string Text
  {
    get { return _text; }
    set
    {
      _text = value;
      OnPropertyChanged("Text");
    }
  }


  #region INotifyPropertyChanged
  public event PropertyChangedEventHandler PropertyChanged;

  protected void OnPropertyChanged(string propName)
  {
    if (PropertyChanged != null)
    {
      PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propName));
    }
  }
  #endregion
}

The first thing that one might give a try here is to bind Visibility of a control directly to the AllowEdit property:

<TextBox x:Name="text" Text="{Binding Text, Mode=TwoWay}"
     Visibility="{Binding AllowEdit}" />

Unfortunately, this is not going to work because the type of the Visibility property does not match the type of AllowEdit. The naive approach suggests that there is no better way to accomplish what we want than to introduce something like a TextVisible property in the model class and bind visibility to it:

public Visibility TextVisible
{
  get
  {
    return AllowEdit ? Visibility.Visible : Visibility.Collapsed;
  }
}
<TextBox x:Name="text" Text="{Binding Text, Mode=TwoWay}"
     Visibility="{Binding TextVisible}" />

Even though this code will compile, run and produce the desired result it actually looks like a much more severe failure than the last one because we have just pulled pure presentation stuff into the model class. This not only means writing and maintaining more non-reusable code but also makes the class less portable. What we want instead is to bind directly to the AllowEdit property and provide the framework with a clue on how to get Visibility from a bool. Enter the converter:

public class BooleanToVisibilityConverter : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
    bool isVisible = (bool)value;
    return isVisible ? Visibility.Visible : Visibility.Collapsed;
  }

  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  {
    return (Visibility)value == Visibility.Visible;
  }
}

The only requirement that a class must satisfy to be used as a converter is to implement the IValueConverter interface. There are only two methods to implement: Convert and ConvertBack, and when you use OneWay binding it is just OK to leave the latter without any implementation. As you can see, the bool-Visibility converter is very simple and does not differ much from the TextVisible property that we have seen before. The next question is how to use it?

First, we need a way to refer to the class in the markup – for this purpose we introduce a StaticResource - for example, in the application’s resources section:

<!-- In App.xaml -->
<Application.Resources>
  <local:LocalizedStrings x:Key="LocalizedStrings"/>
  <local:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
  <!-- other resources -->
</Application.Resources>

Now, when the converter got its XAML name we only need to tell the binding to use it:

<CheckBox x:Name="allowEdit" 
  IsChecked="{Binding AllowEdit, Mode=TwoWay}" 
  Content="allow edit" />
<TextBox x:Name="text" 
  Text="{Binding Text, Mode=TwoWay}"
  Visibility="{Binding AllowEdit, Converter={StaticResource BooleanToVisibilityConverter}}" />

This is actually all that you have to do to make the text box disappear when the AllowEdit flag is set to false and become visible when it’s true. Even though we have written a bit more code than with an additional property in the model class, the approach with data converter has at least two significant advantages. First, it does not prompt us to change the model class in any way – every piece of new logic goes directly in the presentation layer. Besides, once you have implemented a converter and added a resource for it, you can reuse it anywhere in your application at no cost at all. To get these advantages you need to go through three simple steps:
  1. Implement the IValueConverter interface in the converter class,
  2. Create a StaticResource for this class that you use in XAML,
  3.  Refer to this resource to specify a converter in controls’ Binding.

Precisely as you would expect one is not limited to simple types when data converters are concerned. That said, you can transform anything you want into whatever the controls can consume: visibility, colors, brushes, styles, images, strings and so on. Let us take another example and introduce the Task class:

public class Task : INotifyPropertyChanged
{
  public string Title { get; set; }

  private int _priority;
  public int Priority
  {
    get { return _priority; }
    set
    {
      _priority = value;
      OnPropertyChanged("Priority");
    }
  }

  #region INotifyPropertyChanged
  public event PropertyChangedEventHandler PropertyChanged;

  protected void OnPropertyChanged(string propName)
  {
    if (PropertyChanged != null)
    {
      PropertyChanged.Invoke(
        this,
        new PropertyChangedEventArgs(propName));
    }
  }
  #endregion
}

Completing tasks is impossible without proper prioritization – that’s why we have the Priority property in the class. Priority of any particular task must be clear to user and the higher it is the more attention it should draw. Mere numbers can’t communicate the importance of tasks, so we will use a clever color scheme. For this purpose, we need a converter capable of transforming integer values from 0 to 2 into colors, which would make it clear that 0 is the lowest priority, while 2 is the hottest thing that should be addressed as soon as possible:

public class PriorityToBrushConverter : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
    var priority = (int)value;
    switch (priority)
    {
      case 0:
        return new SolidColorBrush(Colors.Green);
      case 1:
        return new SolidColorBrush(Colors.Yellow);
      case 2:
        return new SolidColorBrush(Colors.Red);
      default:
        return new SolidColorBrush(Colors.Black);
    }
  }

  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  {
    throw new NotImplementedException();
  }
}

Note that the converter doesn’t actually return Color instances – Brush class is used instead. The reason is that we are going to pass the values to the Fill property, whose type is Brush. If you accidentally pass Color where Brush is expected, you might end up with an undecipherable runtime error, so pay close attention to the things of this kind. With the converter in place, we simply define a resource for it and consume it in the binding:

<!-- in App.xaml -->
<Application.Resources>
  <local:PriorityToBrushConverter x:Key="PriorityToBrushConverter"/>
  <!-- other resources -->
</Application.Resources>

<!-- in Page.xaml -->
<StackPanel>
  <Rectangle x:Name="PriorityColor" 
    Fill="{Binding Priority, Converter={StaticResource PriorityToBrushConverter}}"
    Width="200" Height="50" />
  <TextBox x:Name="TitleBox" 
    Text="{Binding Title, Mode=TwoWay}" />
  <Slider Minimum="0" Maximum="2" 
    Value="{Binding Priority, Mode=TwoWay}" />
</StackPanel>

If you launch this on a Windows Phone, you should see a picture like this:


SolidColorBrush is a full-blown presentation class, whose only name suggests that there is little room for it in the model layer. If this is not convincing, imagine that the choice of colors might depend on the current theme or any other piece of the application’s state, which means a lot more dependencies for the model. Any idea how would one test Task class if at some point it wants to peek into the Application to know if user likes light or dark theme or what is their current accent color?

Another inherently presentational concept are resource strings, which give us an easy way to localize applications and make them available to wider range of users. Resources are hardly welcomed guests in the model layer, but you will definitely want to use them to present your objects. Let us look how converters might help here. Suppose, we have a collection of some items identified with a Kind property – a string (it could be an enumeration, integer or a GUID – for now it doesn’t matter.) What does matter is that the descriptions of items, which we want to be visible to users, are stored in the resource dictionary and depend solely on the Kind. This indicates that we don’t need to store descriptions in the Task class – it would mean a degree of duplication – and can use a converter to fetch them instead:

public class Item
{
  public string Kind { get; set; }
}

public class ItemToDescriptionConverter : IValueConverter
{
  private const string _DescriptonKeyFormat = "ItemDescription_{0}";

  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
    var item = (Models.Item)value;
    var resourceKey = String.Format(_DescriptonKeyFormat, item.Kind);
    return AppResources.ResourceManager.GetString(resourceKey, culture);
  }

  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  {
    throw new NotImplementedException();
  }
}

Here I assume that converter gets the entire Item object – not just its Kind – to demonstrate that there are little limitations in what you can bind controls to. Such a thing could be helpful in case the displayed description depends on some other properties of the Item – for instance, you might want to insert certain numbers into the string. In this example we just take the Kind property of the Item argument and use it to retrieve the corresponding resource string from the application’s dictionary. To extract the resource we use an instance of the ResourceManager class, which can be accessed through the AppResources class kindly generated by Visual Studio from the .resx dictionary. The only thing left is to actually utilize the converter in XAML – as usual, we declare the corresponding StaticResource and mention it in the control’s Binding:

<!-- in App.xaml -->
<Application.Resources>
  <local:ItemToDescriptionConverter x:Key="ItemToDescriptionConverter" />
  <!-- other resources -->
</Application.Resources>

<!-- in Page.xaml -->
<phone:LongListSelector ItemsSource="{Binding Items}">
  <phone:LongListSelector.ItemTemplate>
    <DataTemplate>
      <StackPanel>
        <TextBlock Text="{Binding Kind}" Style="{StaticResource PhoneTextLargeStyle}" />
        <TextBlock Text="{Binding Converter={StaticResource ItemToDescriptionConverter}}" />
      </StackPanel>
    </DataTemplate>
  </phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>
public class ObjectToResourceStringViewModel
{
  public IEnumerable<Item> Items { get; private set; }

  public ObjectToResourceStringViewModel()
  {
    Items = new List<Item>
    {
      new Item { Kind = "kind1"},
      new Item { Kind = "kind2"},
      new Item { Kind = "kind3"}
    };
  }
}

The model of the above XAML page contains a single property – a collection of Items, which we display with the LongListSelector. Thanks to the fact that any converter is aware of the current culture, we will have a proper description string displayed for each of our items. As for the look of the page, it should be something like this:


Finally, I would like to do something similar but a bit more appealing. Suppose you have a list of items, which should be displayed in the UI as nice images. The idea here is the same as with the localizable strings: in many cases the choice of image depends only on the values of some properties of an item, so there is no need to make room for the image itself in the model. To demonstrate this we use the same Item class with a single Kind property, to which we bind the Image’s source. As you might guess, we will need to create another converter to get an image for the Kind string. Here it is:

public class SocialKindToIconConverter : IValueConverter
{
  private const string _ImagePathFormat = "Assets/SocialIcons/social_{0}.png";
  private ImageSourceConverter _sourceConverter;

  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
    var kind = (string)value;
    var path = String.Format(_ImagePathFormat, kind);
    return _sourceConverter.ConvertFromString(path);
  }

  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  {
    throw new NotImplementedException();
  }

  public SocialKindToIconConverter()
  {
    _sourceConverter = new ImageSourceConverter();
  }
}

The images are stored in the Assets folder of the application package. Converter creates a path to the image from the argument and uses the ImageSourceConverter, which we mentioned above, to transform the path into actual ImageSource object expected by the control. The markup of the page doesn’t differ much from the previous examples and we still have no special logic in the viewmodel behind it:

public class StringToImageViewModel
{
  public IEnumerable<Item> SocialItems { get; private set; }

  public StringToImageViewModel()
  {
    SocialItems = new string[]
    {
      "facebook",
      "googleplus",
      "linkedin",
      "twitter",
      "microsoft",
      "foursquare"
    }.Select(s => new Item { Kind = s }).ToList();
  }
}

<!-- in App.xaml -->
<Application.Resources>
  <local:SocialKindToIconConverter x:Key="SocialKindToIconConverter" />
  <!-- other resources -->
</Application.Resources>

<!-- in page -->
<phone:LongListSelector ItemsSource="{Binding SocialItems}" 
            LayoutMode="Grid"
            GridCellSize="120,120" >
  <phone:LongListSelector.ItemTemplate>
    <DataTemplate>
      <Grid Width="100" Height="100" >
        <Rectangle Fill="{StaticResource PhoneAccentBrush}" />
        <Image Source="{Binding Kind, Converter={StaticResource SocialKindToIconConverter}}" 
             Width="100" Height="100" />
      </Grid>
    </DataTemplate>
  </phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>

From my point of view, the result of introducing this simple converter is just amazing. First, we have these awesome social networks tiles on the page – one could go ahead and make them clickable, tiltable and otherwise interactive. On the other side, we didn’t have to make our model aware of them in any way: the Item class simply does not care whether its instances have something to do with Images or any other UI elements. Overall, it feels like a pretty good separation of concerns.


I hope the examples above give you some idea on how one can benefit from the data conversion mechanism built into the Binding. Here I didn’t cover the reverse conversion, where one transforms the external representation of some property into the internal one. Such a thing is definitely possible with the ConvertBack method of the IValueConverter interface . The implementation of the backward conversion in some cases might be less trivial and clean than of the Convert method, but the idea is the same – experiment with it on your own.

I can’t easily come up with any significant drawbacks of the data conversion mechanism apart from the fact that the converter classes live in a kind of isolation from other code. This is actually a common problem for all pieces of code in the presentation layer, which are linked together mainly by the XAML markup and auto-generated code behind it. This implies that you can’t always easily tell which components make up the UI of your application and there is little tooling to observe the relations between them. On the other side, this same isolation serves well to decouple different layers of the application. As I can’t stop repeating, converters play decently to make your models independent of the presentation and thus more maintainable, portable and testable. This fact combined with the simplicity and flexibility of the conversion mechanism makes it a very good thing to use in Windows Phone or any other XAML-based applications.

As usual, the code for this post is available on GitHub – you are welcome to play with it. I will also be glad to hear any comments: tell me how your apps benefit from converters, where they don't help much and which important points about them I miss.

If you are not only a developer but a Windows Phone user as well, you might want to check out my recent app - Arithmo - it does utilize a number of custom data converters. :) I will be grateful if you just try it - even more so if you come back with any kind of feedback!

Комментариев нет:

Отправить комментарий