Mobile Corner
Rapid Windows Phone Application Development Using MvvmCross
Nick Randolph takes MvvmCross for a spin and shows how quickly you can build an application for Windows Phone and other mobile platforms.
- By Nick Randolph
- 02/25/2014
One of the strategies for building cross-platform mobile applications is to leverage the Xamarin toolset, which effectively allows you to reuse your existing .NET code for building iOS and Android applications. Whilst this is an important foundation block, it doesn't go far enough in terms of bridging some of the platform differences for which you have to account when building applications. This is where MvvmCross comes in. It provides some of the application infrastructure pieces, such as data binding, navigation, dependency injection and so on, that real applications require. In this article I'm going to build a simple Windows Phone application that covers getting started with MvvmCross, and the use of services and plug-ins to power your application.
For more on MvvmCross, check out these resources:
The app will be based on one of the public Northwind OData feeds. To get started I'll need a new solution containing both a Windows Phone application, MvxNorthwind, and a Portable Class Library, MvxNorthwind.Core. The MvxNorthwind.Core library is going to do all the heavy lifting, containing all the view models and services the application requires. The view models will be data bound to views in the MvxNorthwind project. The services will be used to load data from the Northwind OData feed. For this to work, a reference needs to be added to the MvxNorthwind.Core project into the MvxNorthwind project (this can be done by right-clicking on the MvxNorthwind project in Solution Explorer and selecting Add Reference; then selecting the MvxNorthwind.Core project).
The next step is to set up MvvmCross and add a reference to it. This is done by right-clicking on the solution node in Solution Explorer and selecting Manage NuGet Packages for Solution. Next, you search for "mvvmcross" in the online node, then click the Install button alongside the MvvmCross package (see Figure 1). You need to make sure the package is installed into both MvxNorthwind and MvxNorthwind.Core projects.
In addition to adding references to the appropriate MvvmCross assemblies, the NuGet package also adds a Views folder to the MvxNorthwind project and a ViewModels folder to the MvxNorthwind.Core project. Inside these folders are FirstView.xaml/FirstView.xaml.cs and FirstViewModel.cs, respectively, which represent the View (a.k.a. Page) and ViewModel pairing that MvvmCross uses. As you create new pages in your application, you need to create both a new view (SecondView.xaml, for example) in the Views folder and a new view model (SecondViewModel.cs, for example) in the ViewModels folder.
Most of the setting up of MvvmCross has already been done. However, there are a couple of steps you still need to do. In the MvxNorthwind project there's a ToDo-MvvmCross folder with a _Windows Phone UI.txt document that includes the remaining steps. You only actually need to do Steps 3 and 4, which involve minor modifications to the App.xaml.cs file. The remaining steps have already been completed by the NuGet package. At this point you should be able to run your Windows Phone application and see a TextBox and TextBlock, which are data bound to the same property in the instance of FirstViewModel. As such, when you change the contents of the TextBox and click out of the TextBox (Windows Phone only applies the value back through the data binding when the TextBox loses focus), you'll see the new value appear in the TextBlock.
This application needs to consist of two pages: The first contains a list of all product categories; the second contains a list of products that belong to the category the user selected on the first page. I'll start by creating classes that map to the product and category objects in the Northwind database. For simplicity I've only included the Name and Id properties, and additionally the CategoryId property on the Product:
[DataContract(Name="Products")]
public class Product
{
public int ProductID { get; set; }
public string ProductName { get; set; }
public int CategoryID { get; set; }
}
[DataContract(Name="Categories")]
public class Category
{
[DataMember]
public int CategoryID { get; set; }
[DataMember]
public string CategoryName { get; set; }
}
The first step is to retrieve the list of categories and populate them on the FirstView. You'll add a Categories ObservableCollection to the FirstViewModel, as well as adding a Constructor and an Init method, as shown in Listing 1.
Listing 1: Adding Categories ObsverableCollection, Contructor and Init Method to FirstViewModel
public class FirstViewModel : MvxViewModel
{
private readonly ObservableCollection<Category> categories =
new ObservableCollection<Category>();
public ObservableCollection<Category> Categories
{
get
{
return categories;
}
}
public IDataService DataService { get; set; }
public FirstViewModel(IDataService dataService)
{
DataService = dataService;
}
public async Task Init()
{
var catData = await DataService.RetrieveCategories();
foreach (var cat in catData)
{
Categories.Add(cat);
}
}
}
There's quite a bit of convention here. For starters, the FirstViewModel constructor takes an IDataService parameter. So long as you create a class that implements the IDataService interface, MvvmCross will handle creating an instance of that class and injecting it as a parameter to the constructor when creating a new instance of the FirstViewModel. Also, the Init method is automatically called by MvvmCross to initialize the view model at the appropriate point in the page lifecycle, making it a suitable place to load the data for the page.
You'll need to define and implement the IDataService interface. This interface has two methods for retrieving available categories and then retrieving products that match a specified category. The IDataService interface and corresponding implementation, DataService, should be created in a Services folder in the MvxNorthwind.Core project.
public interface IDataService
{
Task<IEnumerable<Category>> RetrieveCategories();
Task<IEnumerable<Product>> RetrieveProducts(Category first);
}
The DataService class will be responsible for pulling data from the OData feed. Rather than writing the decoding logic, we're going to reference another NuGet package, Simple.OData.Client, which will provide a strongly typed wrapper for accessing OData. While adding NuGet packages, take this opportunity to add "MvvmCross – Json Plugin" and "MvvmCross – File Plugin," which you'll need later on.
The code in Listing 2 shows the DataService class. You might think the structure of both RetrieveCategories and RetrieveProducts is slightly weird because they call Task.Run to invoke FindEntries on a different thread. While it's an asynchronous version of FindEntries, it seems that creating the instance of the ODataClient on the UI thread causes the entire application to block. As such, the whole data access operation has been done in a separate thread.
Listing 2: The DataService Class
public class DataService : IDataService
{
private ODataClient client;
private ODataClient Client
{
get { return client ?? (client = new ODataClient(Constants.ODataServiceUrl)); }
}
public async Task<IEnumerable<Category>> RetrieveCategories()
{
var catData = await Task.Run(() =>
{
return Client
.For<Category>()
.FindEntries().ToArray();
});
return catData;
}
public async Task<IEnumerable<Product>> RetrieveProducts(Category first)
{
var productData = await Task.Run(() =>
{
return Client
.For<Product>().Filter(p => p.CategoryID == first.CategoryID)
.FindEntries().ToArray();
});
return productData;
}
}
public class Constants
{
public const string ODataServiceUrl =
"http://services.odata.org/V3/Northwind/Northwind.svc";
}
In order for the categories to be displayed, a ListBox needs to be added to the FirstView. The XAML in Listing 3 simply displays the CategoryName in the ListBox and would need to be restyled for a production-ready application.
Listing 3: The CategoryName in the ListBox
<views:MvxPhonePage
x:Class="MvxNorthwind.Views.FirstView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:Cirrious.MvvmCross.WindowsPhone.Views;assembly=
Cirrious.MvvmCross.WindowsPhone">
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Margin="12,17,0,28">
<TextBlock Text="NORTHWIND" Style="{StaticResource PhoneTextNormalStyle}"/>
<TextBlock Text="categories" Margin="9,-7,0,0"
Style="{StaticResource PhoneTextTitle1Style}"/>
</StackPanel>
<Grid Grid.Row="1" Margin="12,0,12,0">
<ListBox ItemsSource="{Binding Categories}"
SelectionChanged="CategoryChanged"
DisplayMemberPath="CategoryName" />
</Grid>
</Grid>
</views:MvxPhonePage>
In the SelectionChanged event handler we're going to retrieve the selected item, reset the selection on the ListBox and then invoke the DisplayCategory method on the view model:
private void CategoryChanged(object sender, SelectionChangedEventArgs e)
{
var lst = sender as ListBox;
var category = lst.SelectedItem as Category;
if (category == null) return;
lst.SelectedIndex = -1;
(DataContext as FirstViewModel).DisplayCategory(category);
}
Before we look at the code for DisplayCategory, we need to create the page that will display the list of products for the selected category. Create a new Windows Phone page in the Views folder of the MvxNorthwind project called CategoryView, and change its base class in both the XAML and in the Xaml.cs file to MvxPhonePage (as it is in FirstView.xaml and FirstView.xaml.cs). Also, create a new class, CategoryViewModel, in the ViewModels folder in the MvxNorthwind.Core project, and change its base class to MvxViewModel. Now you can return to the DisplayCategory method in FirstViewModel, which will navigate to the newly created CategoryView:
public void DisplayCategory(Category category)
{
ShowViewModel<CategoryViewModel>(category);
}
The structure of the CategoryViewModel is similar to the FirstViewModel, except it populates a collection of Product entities based on the Category, as shown in Listing 4. Note that the Category is passed in as a parameter into the Init method. MvvmCross is clever enough to assign the Category passed into the ShowViewModel method as the parameter on the Init method.
Listing 4: The CategoryViewModel Populates a Collection of Product Entities Based on the Category
public class CategoryViewModel : MvxViewModel
{
private readonly ObservableCollection<Product> products =
new ObservableCollection<Product>();
public ObservableCollection<Product> Products
{
get
{
return products;
}
}
public IDataService DataService { get; set; }
public CategoryViewModel(IDataService dataService)
{
DataService = dataService;
}
public async Task Init(Category category)
{
var productData = await DataService.RetrieveProducts(category);
foreach (var p in productData)
{
Products.Add(p);
}
}
}
The CategoryView needs to be updated to include a ListBox, which will display the products returned, as shown in Listing 5.
Listing 5: Updating CategoryView To Include a ListBox.
<views:MvxPhonePage
x:Class="MvxNorthwind.Views.CategoryView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:Cirrious.MvvmCross.WindowsPhone.Views;assembly=
Cirrious.MvvmCross.WindowsPhone">
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Margin="12,17,0,28">
<TextBlock Text="NORTHWIND" Style="{StaticResource PhoneTextNormalStyle}"/>
<TextBlock Text="products" Margin="9,-7,0,0"
Style="{StaticResource PhoneTextTitle1Style}"/>
</StackPanel>
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<ListBox ItemsSource="{Binding Products}"
DisplayMemberPath="ProductName" />
</Grid>
</Grid>
</views:MvxPhonePage>
Running the application at this point will display a list of categories after a second or so. You can then click on a category to reveal the products in that category. Press the Back button to return to the categories and select a different category.
Each time you run the application you'll notice that it takes a second or so for the categories to populate. This is because each time you run the application it has to go off and query the categories. It's unlikely the list of categories will change frequently, which means it's a good candidate for caching. With the File and JSON plug-ins added earlier as references, you can upgrade the DataService to return a cached version of the categories.
The very first time the application is run, the DataService will retrieve the list of categories from the Northwind service, as before. However, in the future, the cached version will be returned. Each time the application is run the DataService updates the categories list from Northwind, but this is done asynchronously after the cached version is returned, as shown in Listing 6. Now when you run the application, you'll see that it still takes a second or so for the categories to appear the first time. Next time you run the application the categories should be there almost immediately.
Listing 6: DataService updates categories list asynchronously after the cached version is returned.
private IMvxFileStore FileStore { get; set; }
private IMvxJsonConverter JsonConverter { get; set; }
public DataService(IMvxFileStore fileStore, IMvxJsonConverter jsonConverter)
{
FileStore = fileStore;
JsonConverter = jsonConverter;
}
private Category[] Categories { get; set; }
private bool CategoriesRefreshed { get; set; }
public async Task<IEnumerable<Category>> RetrieveCategories()
{
return await LoadCategoriesFromCacheOrService();
}
private const string CategoriesCacheFileName = "categories";
private async Task<Category[]> LoadCategoriesFromCacheOrService()
{
try
{
var cats = LookupCachedCategoriesData();
if (CategoriesRefreshed) return cats;
CategoriesRefreshed = true;
var refreshCategories = LoadCategoriesFromService();
return cats ?? (cats = await refreshCategories);
}
catch (Exception exception)
{
Debug.WriteLine(exception.Message);
return null;
}
}
private Category[] LookupCachedCategoriesData()
{
try
{
if (Categories == null)
{
string json;
// See if it's on disk
if (FileStore.TryReadTextFile(CategoriesCacheFileName, out json))
{
Categories = JsonConverter.DeserializeObject<Category[]>(json);
}
}
return Categories;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
return null;
}
}
private async Task<Category[]> LoadCategoriesFromService()
{
var catData = await Task.Run(() =>
{
try
{
var cats = Client
.For<Category>()
.FindEntries().ToArray();
var json = JsonConverter.SerializeObject(cats);
FileStore.WriteFile(CategoriesCacheFileName, json);
Categories = cats;
return cats;
}
catch (Exception exception)
{
Debug.WriteLine(exception.Message);
return null;
}
});
return catData;
}
In this article I've shown how to quickly put together an application using the MvvmCross framework. This includes navigation with a parameter between pages, loading data using a Service, and caching using the File and JSON plug-ins. The framework includes many more goodies that I didn't cover here, so be sure to spend some time reviewing the online tutorials and videos on MvvmCross to unlock its full potential.