Value Converters form an important concept in data-binding because they allow you to customize the appearance of a data property at the time of binding. If you have done any WPF or Windows app development, you are probably familiar with how Value Converters work. Xamarin.Forms provides an almost identical Value Converter interface as part of its API.
One of the biggest benefits to a Value Converter is that it prevents you from having to add a bunch of getter properties to your data model to adjust how things are displayed. For example, imagine you had a status property on your model and you wanted to change the font color of the status when it is displayed based on its value. You could add a getter property to your model that returns a color based on the current value of the status property. This approach works, but it clutters the model and also potentially leaks platform-specific and user interface logic into the model, which should typically remain very lean and agnostic. The more appropriate approach is to create a Value Converter that allows you to bind directly to the status property, but display it differently based on the value.
Another common way that Value Converters are helpful in Xamarin.Forms is to toggle visibility of elements based on a Boolean property. Luckily, the Xamarin.Forms API made the VisualElement
IsVisible
property into a Boolean instead of an enumeration; so showing things based on Boolean properties is fairly straight forward. However, if you want to hide something when a data bound property is true, you will need a Value Converter to convert the true value to a false value when it is bound to IsVisibleProperty
of an element.
In the next section, we will create a reverse visibility converter that we will use to hide controls on the screen until the ViewModel has finished loading. We'll also create a converter that converts our integer rating property to stars for a more appealing visual effect.
There are often cases where your user interface must wait for data to be loaded; in the meantime, the user might see what appears to be a broken or incomplete page. In these situations, it is best to let the user know what is happening by showing some sort of progress indicator and hiding the rest of the user interface, such as labels, until the data is ready.
Right now, our TripLog app is only using local data, so we do not really see any negative visual effects while the ViewModel data is loaded. We will connect our app to a live API in the next chapter, but until then, we can simulate a waiting period by simply adding a three second delay to our MainViewModel
LoadEntries
method before the LogEntries
collection is populated:
async Task LoadEntries() { LogEntries.Clear (); // TODO: Remove this in Chapter 6 await Task.Delay (3000); // ... }
Now, if we run the app, while the list of entries is being loaded, we just see an awkwardly half loaded page as shown in the next image. The empty ListView
is not only unappealing, there is also no visual indicator to explain to the user that their data is on its way.
We can improve this by hiding the ListView
until the data is loaded and by showing an ActivityIndicator
while it's loading.
In order to know if our ViewModel is loading data, we can create a boolean property called IsBusy
that we only set to true while we are actually loading data or doing some sort of lengthy processing. Since we will need to do similar things in other ViewModels, it makes the most sense to include this IsBusy
property on the BaseViewModel
:
bool
property named IsBusy
to the BaseViewModel
class, as follows:bool _isBusy; public bool IsBusy { get { return _isBusy; } set { _isBusy = value; OnPropertyChanged(); OnIsBusyChanged(); } } protected virtual void OnIsBusyChanged() { }
LoadEntries
method in MainViewModel
to toggle the IsBusy
value while it's loading data:async Task LoadEntries() { if (IsBusy) return; IsBusy = true; LogEntries.Clear (); // TODO: Remove this in Chapter 6 await Task.Delay (3000); LogEntries.Add (new TripLogEntry { Title = "Washington Monument", Notes = "Amazing!", Rating = 3, Date = new DateTime(2015, 2, 5), Latitude = 38.8895, Longitude = -77.0352 }); LogEntries.Add (new TripLogEntry { Title = "Statue of Liberty", Notes = "Inspiring!", Rating = 4, Date = new DateTime(2015, 4, 13), Latitude = 40.6892, Longitude = -74.0444 }); LogEntries.Add (new TripLogEntry { Title = "Golden Gate Bridge", Notes = "Foggy, but beautiful.", Rating = 5, Date = new DateTime(2015, 4, 26), Latitude = 37.8268, Longitude = -122.4798 }); IsBusy = false; }
Now that our ViewModel indicates when it is busy, we need to update the UI on MainPage
to hide the ListView
when data is loading and to show a loading indicator instead. We will do this by data binding the IsBusy
property in two places. In order to hide the ListView
when IsBusy
is true, we will need to create a reverse Boolean Value Converter.
Converters
, and, within it, create a new file named ReverseBooleanConverter
that implements Xamarin.Forms.IValueConverter
:public class ReverseBooleanConverter : IValueConverter { }
Convert
and ConvertBack
methods of IValueConverter
. The goal of this converter is to return the opposite of a given Boolean value; so, when something is false, the converter will return true:public class ReverseBooleanConverter : IValueConverter { public object Convert (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (!(value is Boolean)) return value; return !((Boolean)value); } public object ConvertBack (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (!(value is Boolean)) return value; return !((Boolean)value); } }
IsBusy
property to the ListView
on the MainPage
using this converter, so it is only visible (IsVisible
is true) when IsBusy
is false:public MainPage () { // ... var entries = new ListView { ItemTemplate = itemTemplate }; entries.SetBinding (ListView.ItemsSourceProperty, "LogEntries"); entries.SetBinding (ListView.IsVisibleProperty, "IsBusy", converter: new ReverseBooleanConverter ()); // ... }
MainPage
and only show it when IsBusy
is true. We'll do this by adding an ActivityIndicator
and a label to a StackLayout
, and displaying it in the center of the screen. Also, because we now have two elements to show on the screen, we need to update how we're setting Content
:public MainPage () { // ... var loading = new StackLayout { Orientation = StackOrientation.Vertical, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, Children = { new ActivityIndicator { IsRunning = true }, new Label { Text = "Loading Entries..." } } }; loading.SetBinding (StackLayout.IsVisibleProperty, "IsBusy"); var mainLayout = new Grid { Children = { entries, loading } }; Content = mainLayout; }
Now, when we launch the app, we will see a nice loading indicator while the data loads instead of a blank list view, as shown in the following screenshot:
In this section we're going to continue to improve the user experience with the use of another Value Converter. Currently, the DetailPage
binds to the Rating
property and simply displays the integer value in a formatted string, which is a rather boring way to display that data, as shown in the following screenshot:
This rating data would look much nicer and stand out to the user much quicker if it were an image of stars instead of plain text. In order to translate a number value to an image, we will need to create a new Value Converter.
RatingToStarImageNameConverter
that implements Xamarin.Forms.IValueConverter
:public class RatingToStarImageNameConverter : IValueConverter { }
Convert
and ConvertBack
methods of IValueConverter
. In the Convert
method, we need to check that the value is an integer, and then, based on its value, we convert it to an image filename:public class RatingToStarImageNameConverter : IValueConverter { public object Convert (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value is int) { var rating = (int)value; if (rating <= 1) return "star_1"; if (rating >= 5) return "stars_5"; return "stars_" + rating; } return value; } public object ConvertBack (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException (); } }
DetailPage
to use an Image
view instead of a label to display the entry rating. We will still bind to the same ViewModel property; however, we will use the converter we just created to convert it to an image filename:public DetailPage () { // ... var rating = new Image { HorizontalOptions = LayoutOptions.Center }; rating.SetBinding (Image.SourceProperty, "Entry.Rating", converter: new RatingToStarImageNameConverter ()); // ... Content = mainLayout; }
Now if we run the app and navigate to one of the entries, we will see a much nicer display that immediately causes the rating to standout to the user, as shown in the following screenshot:
3.140.197.10