Welcome to part 2 of this deep-dive analysis of a complete MVVM application. In part 1, we’ve discussed the architecture, explained the DI container and have detailed the NavigationService. In this second part, we will continue to explore more advanced concepts around MVVM.
These articles are a companion to the Advanced MVVM webinar that I hosted at SilverlightShow. You can look at the recorded webcast here.
Accessing data using a repository and the data service layer abstraction
Almost every Windows 8 Store application will need data to perform its job. Calendar apps, note taking apps, Twitter apps… they all need access to some sort of data, whether that data lives in a database behind a service, is saved in the local app data directory…
There’s plenty of information available that explains how to access WCF services, REST services and so on, so I’m not going to spend time on that. Instead, I want to focus on the concepts behind the way I’m doing data access in the Contoso Cookbook application.
For the data access, the application uses the repository pattern. If you look up this pattern, you’ll find something along the lines of “It creates an abstraction between the data persistence and the data consumption code”. That’s exactly what we need: an abstraction in order to achieve loose coupling, which will allow us to change (dare I say replace) the “data access” logic from the code that works with it. I’ve put data access between code, since of course this can be very broad, it can go anywhere from reading a local file to accessing an oData service. The fact remains that you should see the repository as the central location for the data access.
For the consumer of the data, the data access is to be transparent. It shouldn’t be bothered knowing the details of the data access, it should just be able to say: “Give me all RecipeGroups”. That’s what the repository brings us.
A repository is however very often concerned with a single resource collection, so a RecipeRepository would be used to access RecipeGroup and Recipe instances (think parent-child relation). That’s why I won’t let my ViewModels talk with my repositories directly. If I would do so, my ViewModels would actually have to communicate with several repositories and combine the results. This is not the job of the VM (SoC!). Instead, I add another layer that’s more concerned with the business, the functionality of the model. Introducing my data services.
A separate service (or services) for the data access, one per block of functionality, is the point of contact for my VMs. These services in turn communicate with the repositories. Also, these services can be shared easier between VMs, allowing for better sharing of code as well. My data services, just like most other services, are managed by the container.
Another function I use my repositories for is caching. The repository could contain functionality that checks if the data is already available on the device or not. The actual data caching can be done using the local or the roaming data API, which allow us to save files locally or roam to other devices, as well as a dictionary known as the Settings. If it’s cached, it can load from the cache; if not, it can load from the service and cache for a subsequent call. Again, this is not the task of the consumer (the service), since checking if the data is cached and using from cache is data persistence, not data consumption.
Let’s look at some code. Below you can see part of the repository. Notice I haven’t created an interface on this, however, I could have done so.
public class RecipeRepository
{
private ObservableCollection<IRecipeGroup> _recipeGroups = new ObservableCollection<IRecipeGroup>();
public ObservableCollection<IRecipeGroup> RecipeGroups
{
get { return this._recipeGroups; }
}
public async Task<ObservableCollection<IRecipeGroup>> LoadRecipes()
{
if (RecipeGroups.Count == 0)
{
// Retrieve recipe data from Recipes.txt
var file = await Package.Current.InstalledLocation.GetFileAsync("Data\\Recipes.txt");
var stream = await file.OpenReadAsync();
var input = stream.GetInputStreamAt(0);
var reader = new DataReader(input);
uint count = await reader.LoadAsync((uint)stream.Size);
var result = reader.ReadString(count);
// Parse the JSON recipe data
var recipes = JsonArray.Parse(result.Substring(1, result.Length - 1));
// Convert the JSON objects into RecipeDataItems and RecipeDataGroups
CreateRecipesAndRecipeGroups(recipes);
}
return RecipeGroups;
}
}
The RepositoryDataService has a hard reference to the repository. Notice that my data service has an async API. This is very important since the data service will be used from the VMs. If I would not create this API using the await/async, the calls from the VM could not be awaited.
public class RecipeDataService: IRecipeDataService
{
private ObservableCollection<IRecipeGroup> recipeGroups;
public async Task<ObservableCollection<IRecipeGroup>> GetAllRecipeGroups()
{
if (recipeGroups == null)
{
RecipeRepository recipeRepository = new RecipeRepository();
recipeGroups = await recipeRepository.LoadRecipes();
}
return recipeGroups;
}
public async Task<ObservableCollection<IRecipe>> SearchRecipes(string searchValue)
{
ObservableCollection<IRecipe> results = new ObservableCollection<IRecipe>();
if (recipeGroups == null)
{
RecipeRepository recipeRepository = new RecipeRepository();
recipeGroups = await recipeRepository.LoadRecipes();
}
foreach(IRecipeGroup group in recipeGroups)
{
foreach (var recipe in group.Recipes)
{
if (recipe.Title.Contains(searchValue))
{
results.Add(recipe);
}
}
}
return results;
}
}
Once we have the data, we can use it in our UI. Why not bind it to one of the new list controls that come with Windows 8…?
Data binding to list controls
Windows 8 comes with a lot of new list controls. Microsoft has added the GridView, the ListView, the FlipView and the SemanticZoom controls. These allow us to work with lists of data in a touch-driven world by supporting gestures that allow scrolling through (large amounts of) data more easily. List data is all around, think of streams of Flickr images, RSS news items, even files on the local file system. That’s probably why Microsoft decided to include in Windows a bunch of cool list-based controls.
Let’s take a look at how we can work with them in an MVVM scenario. Up first, the GridView. The landing page of the application contains a Grouped GridView (GG). A GG binds easily to a list of lists (each group contains itself a list of items). Just as a reminder, here’s a screenshot of the landing page (RecipeGroupView.xaml).
So here we are binding to an ObservableCollection of RecipeGroups, exposed on the ViewModel.
public ObservableCollection<IRecipeGroup> RecipeGroups { get; set; }
Agreed, this is still a demo, and your actual data might not always be available in this format. It might cost you some more time to transform it into a format that’s usable for binding to a GridView. Let’s see how we can now bind to this collection.
In the XAML, I’ve defined a CollectionViewSource, which is binding on the ObservableCollection.
<CollectionViewSource
x:Name="groupedItemsViewSource"
Source="{Binding RecipeGroups}"
IsSourceGrouped="true"
ItemsPath="Recipes"
/>
My GridView is wrapped in a SemanticZoom, we’ll look at these details in just a second. Here’s the code for the GridView. Notice that the GridView is bound to the CollectionViewSource. For the items, a default item template was created. We are creating a GG, so we are also including a definition of the group: how is a group to be represented. A group contains a header as well. This header is a button that has its content bound to the Title property of the RecipeGroup.
<GridView
x:Name="itemGridView"
AutomationProperties.AutomationId="ItemGridView"
AutomationProperties.Name="Grouped Items"
ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"
ItemTemplate="{StaticResource Standard250x250ItemTemplate}"
SelectedItem="{Binding SelectedRecipe, Mode=TwoWay}"
>
<!-- Old way-->
<!--
<Win8nl_Behavior:EventToCommandBehavior Event="SelectionChanged"
Command="RecipeSelectedCommand"
CommandParameter="{Binding SelectedRecipe, Mode=TwoWay}"
/>-->
<WinRtBehaviors:Interaction.Behaviors>
<Win8nl_Behavior:EventToBoundCommandBehavior Event="SelectionChanged"
Command="{Binding RecipeSelectedCommand}"
CommandParameter="{Binding SelectedRecipe, Mode=TwoWay}"/>
</WinRtBehaviors:Interaction.Behaviors>
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" Margin="116,0,40,46" />
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<Grid Margin="1,0,0,6">
<Button
Command="{Binding DataContext.RecipeGroupSelectedCommand, ElementName=LayoutRoot}"
CommandParameter="{Binding UniqueId}"
AutomationProperties.Name="Group Title"
Content="{Binding Title}"
Style="{StaticResource TextButtonStyle}">
</Button>
</Grid>
</DataTemplate>
</GroupStyle.HeaderTemplate>
<GroupStyle.Panel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid Orientation="Vertical" Margin="0,0,80,0"/>
</ItemsPanelTemplate>
</GroupStyle.Panel>
</GroupStyle>
</GridView.GroupStyle>
</GridView>
So far, there was still no need to do anything in code behind… That’s about to change, when I want to support a semantic zoom (SZ) control. The landing page actually has its GG contained within a SZ, as shown below.
The code for this is shown next (I’ve left out the GG).
<SemanticZoom Grid.Row="1">
<SemanticZoom.ZoomedInView>
...
</SemanticZoom.ZoomedInView>
<SemanticZoom.ZoomedOutView>
<GridView x:Name="groupGridView" Margin="116,0,40,46">
<GridView.ItemTemplate>
<DataTemplate>
<Grid>
<Image Source="{Binding Group.Image}" Width="320" Height="240" Stretch="UniformToFill" />
<StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}">
</StackPanel>
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</SemanticZoom.ZoomedOutView>
</SemanticZoom>
If you take a look at this definition, you see that on the SZ level, there are no data binding statements. Also the zoomed-out level GridView doesn’t contain any. Thing is however, the data binding isn’t going to “fall through” because we’ve defined it for the other GridView. The SZ is not capable of letting us bind to a list of lists and knowing that the zoomed-out level would then bind to the groups. We have to give it a helping hand before it knows that. That’s why I have some code in my code-behind.
What? Code in the code-behind you say? Dear author, didn’t you make a big speech about not adding any code in the code-behind, since this isn’t testable and maintainable and… Yes and no. Yes, I told you to avoid writing code in the code-behind that does things with the model. That should be avoided at all cost. But code that does UI stuff *should* be in the code-behind, not in the VM. And specifying to the zoomed-out level what it should be binding to hardly does anything do the model, apart from reading data. So there you go, a perfect excuse to place some code in the code-behind.
In fact, the code I’m putting in is very limited:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
this.groupGridView.ItemsSource = this.groupedItemsViewSource.View.CollectionGroups;
}
This one line does one thing: setting the ItemsSource for the zoomed-out gridview to the groups of the collection.
Summary
In this second part, we’ve explored the data aspects of an application: accessing the data and using it in combination with the Windows 8 controls. Stay tuned for part 3, the last part of this series.
About the author
Gill Cleeren is Microsoft Regional Director, Silverlight MVP, Pluralsight trainer and Telerik MVP. He lives in Belgium where he works as .NET architect at Ordina. Gill has given many sessions, webcasts and trainings on new as well as existing technologies, such as Silverlight, ASP.NET and WPF at conferences including TechEd, TechDays, DevDays, NDC Oslo, SQL Server Saturday Switserland, Silverlight Roadshow in Sweden, Telerik RoadShow UK… Gill has written 2 books: “Silverlight 4 Data and Services Cookbook” and Silverlight 5 Data and Services Cookbook and is author of many articles for magazines and websites. You can find his blog at www.snowball.be. Twitter: @gillcleeren