Cross-platform design-time view models using portable class libraries

Stuart Lodge is working on a fantastic series of videos and blog posts showing how to build cross-platform mobile applications with MvvmCross. One of his tips is about exposing design-time data. His method is straightforward and efficient, but in case you don’t want to copy sample files to a location inside Windows Program Files folder, you may consider other alternatives, such as storing design-time information as project content files or embeding it into an assembly.

This task becomes more complicated in case you want to show some images at design-time, and BitmapImage class is not part of portable class libraries. I used to solve it by representing view model image properties using opaque object class and setting its values in platform-specific part of the view models. The unfortunate consequence of this approach was declaring a view model per platform serving design-time purposes. The platform view models inherited from a common portable view model and only added a tiny bit of non-portable logic – like reading image resources. Here is how I used to show design-time data in my ODataPad application:

public class ServiceViewModel 
{ 
    public string Name { get; set; } 
    public string Description { get; set; } 
    public string Url { get; set; } 
    public object Image { get; set; } 
} 

public class WinRTDesignHomeViewModel : DesignHomeViewModel 
{ 
    public WinRTDesignHomeViewModel() 
    { 
        foreach (var service in this.Services) 
        { 
            service.Image = new BitmapImage(
                new Uri("ms-appx:///Samples/" + service.Name + ".png")); 
        } 
    } 
} 

public class Net45DesignHomeViewModel : DesignHomeViewModel 
{ 
    public Net45DesignHomeViewModel() 
    { 
        foreach (var service in this.Services) 
        { 
            service.Image = new BitmapImage(
                new Uri(@"pack://application:,,,/ODataPad.UI.Net45;component/Samples/" +
                    service.Name + ".png")); 
        } 
    } 
}

XAML files for Windows Store and WPF applications included Image element with binding to an Image property of ServiceViewModel.

<Image Source="{Binding Image}" Stretch="UniformToFill" />

Here are the screenshots of design-time views:

ODataPadWinRT

ODataPadNet45

ODataPadWP8

This method works but as I already pointed out, it feels heavy because each design-time data set is backed with its own design-time view model – due to non-portability of image .NET types and image management methods.

But there is another way – storing image properties using portable data types, and what can be more portable than a standard string? Enter base64 strings.

Here’s a revised ServiceViewModel class with image data stored as strings:

public class ServiceViewModel 
{ 
    public string Name { get; set; } 
    public string Description { get; set; } 
    public string Url { get; set; } 
    public string ImageBase64 { get; set; } 
}

We no longer need WinRTDesignHomeViewModel and Net45DesignHomeViewModel classes. Instead we will store all design-time information in a portable class library as an embedded resource. Not that Assembly.GetManifestResourceStream method is part of most of portable class libraries profiles, so we can share the following PCL code between all platforms:

public DesignHomeViewModel()
{
    IEnumerable services = null;
    var namespaceName = typeof(DesignHomeViewModel).Namespace;

    var stream = typeof (DesignHomeViewModel).Assembly
        .GetManifestResourceStream(string.Join(".", namespaceName, "SampleServices.xml"));
    using (var reader = new StreamReader(stream))
    {
        this.Services = SamplesService.ParseSamplesXml(reader.ReadToEnd());
    }
}

Wait, but that can’t be sufficient – our XAML views contain Image elements, we can’t just throw base64 strings at them. Well, almost: it’s all about converters.

Here’s revised XAML code:

<Image Source="{Binding ImageBase64, Converter={StaticResource Base64ToImage}}" 
       Stretch="UniformToFill"/>

And these are converers for Windows Store, WPF and Windows Phone applications:

namespace ODataPad.UI.WinRT.Common
{
    public sealed class Base64ImageConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, 
                              object parameter, string language)
        {
            return ConvertAsync(value, targetType, parameter, language).Result;
        }

        public object ConvertBack(object value, Type targetType, 
                                  object parameter, string language)
        {
            return null;
        }

        public async Task ConvertAsync(object value, Type targetType, 
                                       object parameter, string language)
        {
            var image = new BitmapImage();

            if (value != null)
            {
                var bytes = System.Convert.FromBase64String((string)value);

                var ras = new InMemoryRandomAccessStream();
                using (var writer = new DataWriter(ras.GetOutputStreamAt(0)))
                {
                    writer.WriteBytes(bytes);
                    await writer.StoreAsync();
                }

                image.SetSource(ras);
            }
            return image;
        }
    }
}

namespace ODataPad.UI.Net45.Common
{
    public class Base64ImageConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, 
                              object parameter, CultureInfo culture)
        {
            var image = new BitmapImage();
            if (value != null)
            {
                var bytes = System.Convert.FromBase64String((string)value);

                image.BeginInit();
                image.StreamSource = new MemoryStream(bytes);
                image.EndInit();
            }
            return image;
        }

        public object ConvertBack(object value, Type targetType, 
                                  object parameter, CultureInfo culture)
        {
            return null;
        }
    }
}

namespace ODataPad.UI.WP8.Common
{
    public class Base64ImageConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, 
                              object parameter, CultureInfo culture)
        {
            var image = new BitmapImage();
            if (value != null)
            {
                var bytes = System.Convert.FromBase64String((string)value);
                image.SetSource(new MemoryStream(bytes));
            }
            return image;
        }

        public object ConvertBack(object value, Type targetType, 
                                  object parameter, CultureInfo culture)
        {
            return null;
        }
    }
}

Now all my design-time data are stored in a single portable class library and I display them using a single view model also declared in a PCL. All platform-specific data such as images are stored as strings and only converted to bitmaps when rendering views using value converters.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s