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:
- Implement the IValueConverter interface in the converter class,
- Create a StaticResource for this class that you use in XAML,
- 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!
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!
Комментариев нет:
Отправить комментарий