After the previous post, I wanted to take a look at how I might re-work the Visual Studio 2013 template such that it retained the same UI and functionality but was built on PRISM rather than code-behind and so on.
This doesn’t mean that I’m going to end up with some kind of “best practise”, I was just keen to see what dropped out if I tried to combine these two worlds.
So, I made a blank Grid XAML project in Visual Studio 2013 and started to move it around a little.
Adding in PRISM and “Friends”
The first thing I did was took my new blank project and went out and added in a bit of PRISM from NuGet and, while I was there I figured I’d probably be wanting some kind of IoC container so I brought in Autofac as well. This leaves me with a few new references;
All good stuff – next I wanted to encapsulate the code that already existed for accessing the data.
Moving the Data Access into a Service
The next thing I wanted to do was to take as much of the existing code (which loads up the SampleData.json file at runtime) and move it into a service that I can abstract behind an interface whose implementation can then be injected into view models.
I wanted to leave the code alone as much as possible but some of the existing code relied on statics so had to be changed a little (I can’t implement a static interface) but I dropped out a Services folder into my project and an Abstractions folder to with it;
and I defined myself a new interface IDataService;
namespace PrismGridTemplate.Abstractions { interface IDataService { Task<IEnumerable<DataGroup>> GetGroupsAsync(); Task<DataGroup> GetGroupAsync(string uniqueId); Task<DataItem> GetItemAsync(string uniqueId); } }
which is essentially just a refactoring of the public methods on the Grid’s template’s SampleDataSource class into an interface. I implemented that interface in my DataService class mostly by stealing the implementation from the existing SampleDataSource code;
using PrismGridTemplate.Abstractions; using PrismGridTemplate.Model; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using Windows.Data.Json; using Windows.Storage; namespace PrismGridTemplate.Services { internal class DataService : IDataService { // MT: This is purely here to support this class nicely de-serializing itself from the // sample data that shipped with the original template. public ObservableCollection<DataGroup> Groups { get; set; } public async Task<IEnumerable<DataGroup>> GetGroupsAsync() { await GetSampleDataAsync(); return this.groups; } public async Task<DataGroup> GetGroupAsync(string uniqueId) { await GetSampleDataAsync(); var matches = this.groups.Where((group) => group.UniqueId.Equals(uniqueId)); if (matches.Count() == 1) return matches.First(); return null; } public async Task<DataItem> GetItemAsync(string uniqueId) { await GetSampleDataAsync(); // Simple linear search is acceptable for small data sets var matches = this.groups.SelectMany(group => group.Items).Where((item) => item.UniqueId.Equals(uniqueId)); if (matches.Count() == 1) return matches.First(); return null; } private async Task GetSampleDataAsync() { if (this.groups != null) return; this.groups = new List<DataGroup>(); Uri dataUri = new Uri("ms-appx:///SampleData/SampleData.json"); StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(dataUri); string jsonText = await FileIO.ReadTextAsync(file); JsonObject jsonObject = JsonObject.Parse(jsonText); JsonArray jsonArray = jsonObject["Groups"].GetArray(); foreach (JsonValue groupValue in jsonArray) { JsonObject groupObject = groupValue.GetObject(); DataGroup group = new DataGroup(groupObject["UniqueId"].GetString(), groupObject["Title"].GetString(), groupObject["Subtitle"].GetString(), groupObject["ImagePath"].GetString(), groupObject["Description"].GetString()); foreach (JsonValue itemValue in groupObject["Items"].GetArray()) { JsonObject itemObject = itemValue.GetObject(); group.Items.Add(new DataItem(itemObject["UniqueId"].GetString(), itemObject["Title"].GetString(), itemObject["Subtitle"].GetString(), itemObject["ImagePath"].GetString(), itemObject["Description"].GetString(), itemObject["Content"].GetString())); } this.groups.Add(group); } } List<DataGroup> groups; } }
I don’t think I changed a whole bunch of code there from the original. Perhaps the main changes would be that the original code did some work to only load the data once (i.e. it stuck the loaded data into some static storage slot). I haven’t taken this decision here – I’m going to lead the decision around the instancing of this service to the way in which I configure my IoC container.
Moving the Data Classes into a Model
I took the original data classes that are called SampleDataGroup and SampleDataItem and renamed them and dropped them into a folder I called Model. This might be me being slightly pretentious but it seemed like a good place to put them. I called them DataGroup and DataItem and, beyond the naming, I don’t think I changed them at all.
While I was at it, I made a folder called SampleData and put the SampleData.json file in there for safe-keeping.
and, for completeness, those classes look like;
using System; namespace PrismGridTemplate.Model { internal class DataItem { public DataItem(String uniqueId, String title, String subtitle, String imagePath, String description, String content) { this.UniqueId = uniqueId; this.Title = title; this.Subtitle = subtitle; this.Description = description; this.ImagePath = imagePath; this.Content = content; } public string UniqueId { get; private set; } public string Title { get; private set; } public string Subtitle { get; private set; } public string Description { get; private set; } public string ImagePath { get { return (imagePath); } private set { imagePath = value; } } string imagePath; public string Content { get; private set; } public override string ToString() { return this.Title; } } }
and;
using System; using System.Collections.ObjectModel; namespace PrismGridTemplate.Model { internal class DataGroup { public DataGroup(String uniqueId, String title, String subtitle, String imagePath, String description) { this.UniqueId = uniqueId; this.Title = title; this.Subtitle = subtitle; this.Description = description; this.ImagePath = imagePath; this.Items = new ObservableCollection<DataItem>(); } public string UniqueId { get; private set; } public string Title { get; private set; } public string Subtitle { get; private set; } public string Description { get; private set; } public string ImagePath { get; private set; } public ObservableCollection<DataItem> Items { get; private set; } public override string ToString() { return this.Title; } } }
I chose to leave these classes alone because they lined up with the SampleData.json file and I figured it wouldn’t be too much pain to wrap some view models around them.
Adding ViewModels – Generalities
PRISM ships a ViewModel base class of its own. It’s mostly an implementation of INotifyPropertyChanged along with an (empty) implementation of INavigationAware which is a PRISM interface that can be used to involve your ViewModels in navigation events – e.g. as navigation occurs from “Page 1” to “Page 2” this interface allows you to hook a ViewModel into that navigation and grab navigation parameters. For me, this is one of the places where the Visual Studio templates fall down a bit. They tie up navigation with the views which makes it hard to implement your ViewModels.
PRISM also gives you an implementation of a NavigationService which it abstracts via an interface INavigationService. This means that it’s “easy” to inject some componentry that knows how to do navigation into your ViewModels whereas, again, in the Visual Studio templates the navigation is usually quite tied to the UI componentry like Frame, Page and so on.
If you haven’t already been through this stuff with PRISM then you could start at this earlier post and use it as a launching point into the other PRISM documentation which I can’t recommend highly enough.
I’m not claiming to have this quite right but I figured that my ViewModels would almost certainly want to surface implementations of ICommand so I derived from ViewModel and added that capability;
class CommandableViewModel : ViewModel { public CommandableViewModel() { this.commands = new Dictionary<string, ICommand>(); } protected void AddCommand(string name, Action action) { this.commands[name] = new DelegateCommand(action); } protected void AddCommand(string name, Action action, Func<bool> enabledAction) { this.commands[name] = new DelegateCommand(action, enabledAction); } public IReadOnlyDictionary<string, ICommand> Commands { get { return (this.commands); } } Dictionary<string, ICommand> commands; }
The idea of this is that it’s easy for a derived class to add commands into the Commands dictionary with something like;
class NavigationViewModel : CommandableViewModel { public NavigationViewModel(INavigationService navService) { this.navService = navService; base.AddCommand("GoBackCommand", GoBack, this.navService.CanGoBack); }
and then a piece of XAML can bind to a command like that by using something like;
<Button Command="{Binding Commands[GoBackCommand]}">
I also figured that my ViewModels might want to be able to deal with navigation so I evolved a variant that had access to an INavigationService;
using Microsoft.Practices.Prism.StoreApps.Interfaces; namespace PrismGridTemplate.ViewModels { class NavigationViewModel : CommandableViewModel { public NavigationViewModel(INavigationService navService) { this.navService = navService; base.AddCommand("GoBackCommand", GoBack, this.navService.CanGoBack); } protected void GoBack() { this.navService.GoBack(); } protected void Navigate(string token, object parameter) { this.navService.Navigate(token, parameter); } protected INavigationService NavigationService { get { return (this.navService); } } INavigationService navService; } }
And so now anything that derives from this can offer the UI a way to bind to a GoBackCommand which will work with the underlying INavigationService (which I’m assuming will be injected) to do the actual work of navigation (under there somewhere is a Frame but in this sort of code that’s the last thing I want to know about ).
Being a little bit lazy, I figured that I would now want to produce specific view models (e.g. to surface my DataGroups and DataItems) and I figured that to some extent these would be aggregating those classes. I find aggregation in .NET a bit of a pain. If I have some type Foo then sometimes I’d like to be able to wrap some type Bar around Foo such that Bar surfaces all the public properties/methods of Foo. I want to write syntax something like;
class Bar aggregates Foo public properties { Bar(Foo f) { } }
but, clearly, this is fantasy! so instead I derived another ViewModel that does this in a less elegant way (I could derive from Foo of course but that’s another story) and so I came up with the horribly named;
using Microsoft.Practices.Prism.StoreApps.Interfaces; namespace PrismGridTemplate.ViewModels { class NavigationViewModelWrapsModel<T> : NavigationViewModel { public NavigationViewModelWrapsModel(T model, INavigationService navService) : this(navService) { this.model = model; } public NavigationViewModelWrapsModel(INavigationService navService) : base(navService) { } // Note, unless the Model property itself does INotifyPropertyChanged then 2-way // bindings to properties within that Model won't work. For this example, everything // is read-only so this doesn't matter. public T Model { get { return (this.model); } set { base.SetProperty(ref this.model, value); } } T model; } }
and, as the comment in the code says, unless the underlying type T happens to implement INotifyPropertyChanged (and I don’t constrain it here to do that) I’m not going to get property changed notifications firing from any properties of T as they change value which is no problem in this particular template because there’s the Grid template only surfaces read-only data.
All these little base classes might seem like overkill and I could definitely have produced some ViewModels without them but they seem to make the specific ViewModel code a whole lot more succinct and (arguably for me) more elegant so I introduce them. I wouldn’t need to write them again, they’d do for other projects in the future where I was using PRISM.
Adding ViewModels – Specifics
I have 3 views ( GroupedItemsPage, GroupDetailPage, ItemDetailPage ) and I have 2 entities within my model ( DataGroup, DataItem ). They come together in the sense that;
- GroupedItemsPage displays groups and items.
- GroupDetailPage displays one group and its items.
- ItemDetailPage displays one item.
In the interest of keeping things “short”, I decided that I could deal with all of those needs with 3 ViewModels. That is – when the GroupedItemsPage displays an Item it’s going to be using the exact same ViewModel as when the ItemDetailPage is displaying one item. I can argue back/forth about whether this is “right” or not but, hey, it works for me in this particular situation.
PRISM has conventions around naming of Views/ViewModels which work perfectly well for me and so I went along with them (you can easily change them) and named a folder ViewModels;
and dropped all my classes related to ViewModels in there but the three that are specifically about pages are ItemDetailPageViewModel, GroupDetailPageViewModel, GroupedItemsPageViewModel.
Taking a look at those alongside their corresponding, data-bound UI…
ItemDetailPageViewModel and ItemDetailPageView
The ItemDetailPageViewModel class I ended up is as below;
using Microsoft.Practices.Prism.StoreApps.Interfaces; using PrismGridTemplate.Abstractions; using PrismGridTemplate.Model; using System.Collections.Generic; using System.Threading.Tasks; using Windows.UI.Xaml.Navigation; namespace PrismGridTemplate.ViewModels { class ItemDetailPageViewModel : NavigationViewModelWrapsModel<DataItem> { public ItemDetailPageViewModel(IDataService dataService, INavigationService navService) : base(navService) { this.dataService = dataService; base.AddCommand("ItemInvokedCommand", OnItemInvoked); } public override void OnNavigatedTo(object navigationParameter, NavigationMode navigationMode, Dictionary<string, object> viewModelState) { base.OnNavigatedTo(navigationParameter, navigationMode, viewModelState); string uniqueItemId = (string)navigationParameter; this.LoadDataAsync(uniqueItemId); } async Task LoadDataAsync(string uniqueItemId) { this.Model = await this.dataService.GetItemAsync(uniqueItemId); } void OnItemInvoked() { this.Navigate("ItemDetail", this.Model.UniqueId); } IDataService dataService; } }
There’s not a whole lot to see – the ViewModel surfaces a property of type DataItem as its Model property and it takes a dependency on IDataService and INavigationService (for its base class) and when the ViewModel is “navigated to” it uses the IDataService to load the data using pretty much the same methods as the original Grid template from Visual Studio (albeit re-packaged as a service).
But there’s a little bit of weirdness here. What is the idea of the ICommand that can be invoked via Commands[“ItemInvokedCommand”] and which uses the INavigationService to navigate to the ItemDetail view? Isn’t this ViewModel supposed to already be supporting that very same view? Are we navigating to ourselves?
No, this comes back to the idea that this ViewModel sits behind the ItemDetailPageView (where it will be navigated to and will load data) but it’s also going to be used in my other 2 views where the user will be able to tap it, invoke this command and drive navigation to the ItemDetailPageView. So, it’s serving more than one purpose which might point to it needing to be re-worked but I went with it for now.
Here’s the UI that goes alongside it taken from ItemDetailPageView.xaml which I dropped into a Views folder;
<Page x:Name="pageRoot" x:Class="PrismGridTemplate.Views.ItemDetailPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:PrismGridTemplate" xmlns:data="using:PrismGridTemplate.Data" xmlns:common="using:PrismGridTemplate.Common" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:p="using:Microsoft.Practices.Prism.StoreApps" xmlns:svcs="using:PrismGridTemplate.Services" mc:Ignorable="d" p:ViewModelLocator.AutoWireViewModel="True" d:DataContext="{Binding Groups[0].Items[0], Source={d:DesignData Source=/SampleData/SampleData.json,Type=svcs:DataService}}"> <Page.Resources> <common:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/> </Page.Resources> <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> <Grid.ChildrenTransitions> <TransitionCollection> <EntranceThemeTransition/> </TransitionCollection> </Grid.ChildrenTransitions> <Grid.RowDefinitions> <RowDefinition Height="140"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Grid Grid.Row="1" x:Name="contentRegion" DataContext="{Binding Model}" d:DataContext="{Binding}"> <TextBlock Margin="120,0,0,0" Text="{Binding Description}"/> </Grid> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="120"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <AppBarButton x:Name="backButton" Icon="Back" Height="95" Margin="10,46,10,0" Command="{Binding Commands[GoBackCommand]}" Visibility="{Binding IsEnabled, Converter={StaticResource BooleanToVisibilityConverter}, RelativeSource={RelativeSource Mode=Self}}" AutomationProperties.Name="Back" AutomationProperties.AutomationId="BackButton" AutomationProperties.ItemType="Navigation Button"/> <TextBlock x:Name="pageTitle" DataContext="{Binding Model}" d:DataContext="{Binding}" Text="{Binding Title}" Style="{StaticResource HeaderTextBlockStyle}" Grid.Column="1" IsHitTestVisible="false" TextWrapping="NoWrap" VerticalAlignment="Bottom" Margin="0,0,30,40"/> </Grid> </Grid> </Page>
Bits that I’d perhaps highlight on this page;
- The Page asks PRISM to auto-wire its ViewModel which it will do based on convention and wire up a new ItemDetailPageViewModel instance.
- The AppBarButton binds its command to Commands[GoBackCommand]
- Anything else is bound to properties on the Model at runtime.
- The Page is trying (and succeeding) in using VS/Blend’s design time data support by taking the SampleData.json file and asking the environment to load it up as a deserialized version of my DataService class and then indexing into the Groups/Items of that data to get to the 1st group’s 1st item. Note that at design time this item is a DataItem and will directly have properties like Title, Description whereas at runtime the DataContext here will be a ItemDetailPageViewModel which wraps the DataItem as a property called Model. Because of this, you’ll see pieces in the XAML where I play with the design-time DataContext to add/remove the Model part of the path as necessary.
That all works fine.
GroupDetailPageViewModel and GroupDetailPageView
Having looked at the ItemDetailPageViewModel the GroupDetailPageViewModel probably doesn’t come as much of a surprise. Here’s the code;
using Microsoft.Practices.Prism.StoreApps.Interfaces; using PrismGridTemplate.Abstractions; using PrismGridTemplate.Model; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Windows.UI.Xaml.Navigation; namespace PrismGridTemplate.ViewModels { class GroupDetailPageViewModel : NavigationViewModelWrapsModel<DataGroup> { public GroupDetailPageViewModel( IDataService dataService, INavigationService navService, Func<ItemDetailPageViewModel> itemDetailPageViewModelFactory) : base(navService) { this.dataService = dataService; this.itemDetailPageViewModelFactory = itemDetailPageViewModelFactory; base.AddCommand("GroupInvokedCommand", OnGroupInvoked); } void OnGroupInvoked() { base.Navigate("GroupDetail", this.Model.UniqueId); } public IEnumerable<ItemDetailPageViewModel> Items { get { if ((this.itemViewModels == null) && (this.Model != null) && (this.Model.Items != null)) { this.itemViewModels = this.Model.Items.Select( model => { ItemDetailPageViewModel viewModel = this.itemDetailPageViewModelFactory(); viewModel.Model = model; return (viewModel); } ); } return (this.itemViewModels); } } public override void OnNavigatedTo(object navigationParameter, NavigationMode navigationMode, Dictionary<string, object> viewModelState) { base.OnNavigatedTo(navigationParameter, navigationMode, viewModelState); string groupUniqueId = (string)navigationParameter; this.LoadDataAsync(groupUniqueId); } async Task LoadDataAsync(string groupUniqueId) { DataGroup group = await this.dataService.GetGroupAsync(groupUniqueId); this.Model = group; base.OnPropertyChanged("Items"); } IEnumerable<ItemDetailPageViewModel> itemViewModels; IDataService dataService; Func<ItemDetailPageViewModel> itemDetailPageViewModelFactory; } }
This ViewModel is being used to support 2 views. One is the GroupDetailsPage which displays a DataGroup and its DataItems but this ViewModel is also going to be used on the main page of the app.
In one mode of use, the view will be navigated to and it will use the underlying IDataService to load up the specific group that it has been passed as a navigation parameter. In the other mode of operation, this view model will be created and passed a Model and in that mode the UI will ask it to surface an ICommand (via Commands[“GroupInvokedCommand”]) which will navigate to the details page for that DataGroup.
Perhaps the only other “interesting” thing here is that this ViewModel has 2 different ways of surfacing the DataItems that below to the group. One way is via the Model.Items property which will surface up an enumerable set of DataItem and the other means is via the Items property which surfaces up an enumerable of ItemDetailPageViewModels wrapped around those models. That latter collection is the one which would be bound to by any UI that wanted to display DataItems with bindable commands to “invoke” and cause the app to navigate the item detail page for that item.
In order to create that Items property value this ViewModel needs to be able to instantiate a ItemDetailPageViewModel along with its injected dependencies so it takes a dependency on a Func<ItemDetailPageViewModel> in its constructor and it uses that in the Items property to lazily construct that “list” of ItemDetailPageViewModel over the top of the existing “list” of DataItem.
In terms of the UI that presents the GroupDetailPage;
<Page x:Name="pageRoot" x:Class="PrismGridTemplate.Views.GroupDetailPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:PrismGridTemplate" xmlns:data="using:PrismGridTemplate.Data" xmlns:common="using:PrismGridTemplate.Common" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:p="using:Microsoft.Practices.Prism.StoreApps" xmlns:svcs="using:PrismGridTemplate.Services" mc:Ignorable="d" p:ViewModelLocator.AutoWireViewModel="True" d:DataContext="{Binding Groups[0], Source={d:DesignData Source=/SampleData/SampleData.json, Type=svcs:DataService}}"> <Page.Resources> <common:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/> <!-- Collection of items displayed by this page --> <CollectionViewSource x:Name="itemsViewSource" Source="{Binding Items}"/> </Page.Resources> <!-- This grid acts as a root panel for the page that defines two rows: * Row 0 contains the back button and page title * Row 1 contains the rest of the page layout --> <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> <Grid.ChildrenTransitions> <TransitionCollection> <EntranceThemeTransition/> </TransitionCollection> </Grid.ChildrenTransitions> <Grid.RowDefinitions> <RowDefinition Height="140"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!-- Horizontal scrolling grid --> <GridView x:Name="itemGridView" AutomationProperties.AutomationId="ItemGridView" AutomationProperties.Name="Items In Group" TabIndex="1" Grid.RowSpan="2" Padding="120,126,120,50" ItemsSource="{Binding Source={StaticResource itemsViewSource}}" SelectionMode="None" IsSwipeEnabled="false"> <GridView.ItemTemplate> <DataTemplate> <Button Command="{Binding Commands[ItemInvokedCommand]}" Template="{x:Null}"> <Grid Height="110" Width="480" Margin="10" DataContext="{Binding Model}" d:DataContext="{Binding}"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}" Width="110" Height="110"> <Image Source="{Binding ImagePath}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/> </Border> <StackPanel Grid.Column="1" VerticalAlignment="Top" Margin="10,0,0,0"> <TextBlock Text="{Binding Title}" Style="{StaticResource TitleTextBlockStyle}" TextWrapping="NoWrap"/> <TextBlock Text="{Binding Subtitle}" Style="{StaticResource CaptionTextBlockStyle}" TextWrapping="NoWrap"/> <TextBlock Text="{Binding Description}" Style="{StaticResource BodyTextBlockStyle}" MaxHeight="60"/> </StackPanel> </Grid> </Button> </DataTemplate> </GridView.ItemTemplate> <GridView.Header> <StackPanel Width="480" Margin="0,4,14,0" DataContext="{Binding Model}" d:DataContext="{Binding}"> <TextBlock Text="{Binding Subtitle}" Margin="0,0,0,20" Style="{StaticResource SubheaderTextBlockStyle}" MaxHeight="60"/> <Image Source="{Binding ImagePath}" Height="400" Margin="0,0,0,20" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/> <TextBlock Text="{Binding Description}" Margin="0,0,0,0" Style="{StaticResource BodyTextBlockStyle}"/> </StackPanel> </GridView.Header> <GridView.ItemContainerStyle> <Style TargetType="FrameworkElement"> <Setter Property="Margin" Value="52,0,0,2"/> </Style> </GridView.ItemContainerStyle> </GridView> <!-- Back button and page title --> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="120"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <AppBarButton x:Name="backButton" Icon="Back" Height="95" Margin="10,46,10,0" Command="{Binding Commands[GoBackCommand]}" Visibility="{Binding IsEnabled, Converter={StaticResource BooleanToVisibilityConverter}, RelativeSource={RelativeSource Mode=Self}}" AutomationProperties.Name="Back" AutomationProperties.AutomationId="BackButton" AutomationProperties.ItemType="Navigation Button"/> <TextBlock DataContext="{Binding Model}" d:DataContext="{Binding}" x:Name="pageTitle" Text="{Binding Title}" Style="{StaticResource HeaderTextBlockStyle}" Grid.Column="1" IsHitTestVisible="false" TextWrapping="NoWrap" VerticalAlignment="Bottom" Margin="0,0,30,40"/> </Grid> </Grid> </Page>
The page is set up with its DataContext as either a GroupDetailPageViewModel instance or, at design time, the DataContext will be an instance of DataGroup pulled out of the data that comes from the sample .json file.
Either way, the CollectionViewSource will bind into the Items collection which, for the former will be some list of ItemDetailPageViewModel and for the latter will be some list of DataItem.
Because of that, you will see places further on in the XAML where I attempt to set DataContext and the d : DataContext differently because the bindings change (adding or removing a “Model” into the path) depending on which scenario it is.
It’s perhaps worth pointing out that the display of items is wrapped in a Button which is bound to Commands[ItemInvokedCommand] – this is part of the DataTemplate for the item template. The button is there to let me easily add an ICommand to be invoked.
It’s perhaps also worth pointing out that the sample data aspect of this doesn’t work. My attempt (line 55 or so) to change the DataContext at runtime/design time inside of a DataTemplate doesn’t work. It seems like there’s bug filed on this so I’m hopeful that it’ll get resolved prior to Visual Studio 2013 shipping.
I think that my setting up of the design time DataContext is fine but it’s hard to know until Visual Studio fixes that bug.
GroupedItemsPage.xaml and GroupedItemsPageViewModel
The last ViewModel follows the pattern of the previous two so there’s perhaps little new in there at this point;
using Microsoft.Practices.Prism.StoreApps.Interfaces; using PrismGridTemplate.Abstractions; using PrismGridTemplate.Model; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Windows.UI.Xaml.Navigation; namespace PrismGridTemplate.ViewModels { class GroupedItemsPageViewModel : NavigationViewModel { public IEnumerable<GroupDetailPageViewModel> Groups { get { return (this.groups); } private set { base.SetProperty(ref this.groups, value); } } public GroupedItemsPageViewModel(IDataService dataService, INavigationService navService, Func<GroupDetailPageViewModel> dataGroupViewModelFactory) : base(navService) { this.dataService = dataService; this.dataGroupViewModelFactory = dataGroupViewModelFactory; } public override void OnNavigatedTo(object navigationParameter, NavigationMode navigationMode, Dictionary<string, object> viewModelState) { base.OnNavigatedTo(navigationParameter, navigationMode, viewModelState); this.LoadDataAsync(); } async Task LoadDataAsync() { IEnumerable<DataGroup> dataGroups = await this.dataService.GetGroupsAsync(); this.Groups = dataGroups.Select( model => { GroupDetailPageViewModel viewModel = this.dataGroupViewModelFactory(); viewModel.Model = model; return (viewModel); } ); } Func<GroupDetailPageViewModel> dataGroupViewModelFactory; IEnumerable<GroupDetailPageViewModel> groups; IDataService dataService; } }
In essence, as this first page in the app is navigated to, it will LoadDataAsync() using the underlying IDataService and for each DataGroup model instance that’s returned from the data service, this ViewModel manufactures a GroupDetailPageViewModel instance wrapped around that model and surfaces the resulting list in the Groups property ready for binding.
There’s nothing else to it. The view sitting on top of it is bound;
<Page x:Class="PrismGridTemplate.Views.GroupedItemsPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:PrismGridTemplate" xmlns:data="using:PrismGridTemplate.Data" xmlns:common="using:PrismGridTemplate.Common" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:p="using:Microsoft.Practices.Prism.StoreApps" xmlns:svcs="using:PrismGridTemplate.Services" p:ViewModelLocator.AutoWireViewModel="True" mc:Ignorable="d"> <Page.Resources> <common:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/> <x:String x:Key="ChevronGlyph"></x:String> <CollectionViewSource x:Name="groupedItemsViewSource" Source="{Binding Groups}" IsSourceGrouped="true" ItemsPath="Items" d:Source="{Binding Groups, Source={d:DesignData Source=/SampleData/SampleData.json, Type=svcs:DataService}}"/> </Page.Resources> <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> <Grid.ChildrenTransitions> <TransitionCollection> <EntranceThemeTransition/> </TransitionCollection> </Grid.ChildrenTransitions> <Grid.RowDefinitions> <RowDefinition Height="140"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <GridView x:Name="itemGridView" AutomationProperties.AutomationId="ItemGridView" AutomationProperties.Name="Grouped Items" Grid.RowSpan="2" Padding="116,137,40,46" ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}" SelectionMode="None" IsSwipeEnabled="false" IsItemClickEnabled="True"> <GridView.ItemTemplate> <DataTemplate> <Button Command="{Binding Commands[ItemInvokedCommand]}" Template="{x:Null}"> <Grid HorizontalAlignment="Left" Width="250" Height="250" DataContext="{Binding Model}" d:DataContext="{Binding}" > <Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}"> <Image Source="{Binding ImagePath}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/> </Border> <StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}"> <TextBlock Text="{Binding Title}" Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}" Style="{StaticResource TitleTextBlockStyle}" Height="60" Margin="15,0,15,0"/> <TextBlock Text="{Binding Subtitle}" Foreground="{StaticResource ListViewItemOverlaySecondaryForegroundThemeBrush}" Style="{StaticResource CaptionTextBlockStyle}" TextWrapping="NoWrap" Margin="15,0,15,10"/> </StackPanel> </Grid> </Button> </DataTemplate> </GridView.ItemTemplate> <GridView.ItemsPanel> <ItemsPanelTemplate> <ItemsWrapGrid GroupPadding="0,0,70,0"/> </ItemsPanelTemplate> </GridView.ItemsPanel> <GridView.GroupStyle> <GroupStyle> <GroupStyle.HeaderTemplate> <DataTemplate> <Grid Margin="1,0,0,6"> <Button Foreground="{StaticResource ApplicationHeaderForegroundThemeBrush}" AutomationProperties.Name="Group Title" Command="{Binding Commands[GroupInvokedCommand]}" Style="{StaticResource TextBlockButtonStyle}" > <StackPanel Orientation="Horizontal" DataContext="{Binding Model}" d:DataContext="{Binding}"> <TextBlock Text="{Binding Title}" Margin="3,-7,10,10" Style="{StaticResource SubheaderTextBlockStyle}" TextWrapping="NoWrap" /> <TextBlock Text="{StaticResource ChevronGlyph}" FontFamily="Segoe UI Symbol" Margin="0,-7,0,10" Style="{StaticResource SubheaderTextBlockStyle}" TextWrapping="NoWrap" /> </StackPanel> </Button> </Grid> </DataTemplate> </GroupStyle.HeaderTemplate> </GroupStyle> </GridView.GroupStyle> </GridView> <!-- Back button and page title --> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="120"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <AppBarButton x:Name="backButton" Icon="Back" Height="95" Margin="10,46,10,0" Command="{Binding Commands[GoBackCommand]}" Visibility="{Binding IsEnabled, Converter={StaticResource BooleanToVisibilityConverter}, RelativeSource={RelativeSource Mode=Self}}" AutomationProperties.Name="Back" AutomationProperties.AutomationId="BackButton" AutomationProperties.ItemType="Navigation Button"/> <TextBlock x:Name="pageTitle" Text="{StaticResource AppName}" Style="{StaticResource HeaderTextBlockStyle}" Grid.Column="1" IsHitTestVisible="false" TextWrapping="NoWrap" VerticalAlignment="Bottom" Margin="0,0,30,40"/> </Grid> </Grid> </Page>
in terms of this view, it tries to use a similar technique of the previous one of having a CollectionViewSource with a Source bound to a Groups property on different objects at runtime versus design-time. At runtime, this will be an instance of the GroupedItemsPageViewModel whereas at design time it will be an instance of the DataService class which I hacked ever so slightly to include a Groups property purely so that it would deserialize from the data stored in the SampleData.json file and serve this purpose of being the design time DataContext for this view.
I’d perhaps revisit that if I was taking this further or if I was doing things a little more from scratch.
Like the previous view, there are places where I need to “add” a property path of “Model” into some of the bindings by changing the runtime DataContext versus the design time DataContext and, like the previous view, this needs to be done inside of DataTemplates which means that I hit the same bug in Visual Studio 2013 Preview which is a shame as it messes up my design time and gives me a nasty error in the XAML editor (at design time, not build time);
and that’s the end of the ViewModels/Views supporting the 3 pages – there’s no code in any of the code-behind files for the 3 views other than a single call to InitializeComponent() in each case.
Boot-Strapping the App’s Startup
In terms of getting the application up and running, there’s a need to setup my IoC container and navigate the app to my first view. That’s made very simple by PRISM in that my App.xaml.cs simply looks like;
using Autofac; using Microsoft.Practices.Prism.StoreApps; using Microsoft.Practices.Prism.StoreApps.Interfaces; using PrismGridTemplate.Abstractions; using PrismGridTemplate.Services; using PrismGridTemplate.ViewModels; using System; using Windows.ApplicationModel.Activation; namespace PrismGridTemplate { sealed partial class App : MvvmAppBase { IContainer container; protected override object Resolve(Type type) { return (container.Resolve(type)); } protected override void OnInitialize(IActivatedEventArgs args) { base.OnInitialize(args); ContainerBuilder builder = new ContainerBuilder(); builder.RegisterType<DataService>().As<IDataService>().InstancePerLifetimeScope(); builder.RegisterInstance(this.NavigationService).As<INavigationService>(); builder.RegisterType<GroupedItemsPageViewModel>().AsSelf(); builder.RegisterType<GroupDetailPageViewModel>().AsSelf(); builder.RegisterType<ItemDetailPageViewModel>().AsSelf(); this.container = builder.Build(); } protected override void OnLaunchApplication(LaunchActivatedEventArgs args) { this.NavigationService.Navigate("GroupedItems", null); } } }
The OnInitialize override sets up an Autofac container and adds in my IDataService, INavigationService and my 3 ViewModels into that container. The IDataService is told to instance itself in a “singleton”-like manner and the INavigationService is registered as an existing instance which PRISM has already created for me.
The Resolve override simply delegates creating things to the Autofac container and the OnLaunchApplication asks the INavigationService to navigate to the first view in the application.
That’s it – clean and simple.
Wrapping Up
I wanted to take the Grid template and see what it was like moving it across to sit on top of PRISM with a bit more binding and commanding than the original template had and I think that I managed that reasonably well here without too much pain.
I’m reasonably happy that at the end of the process (minus unit-tests) I have something that’s more clearly layered than the initial template but I did introduce quite a few new base classes along the way and picking up PRISM (or another framework like this) involves a bit of conceptual learning over and above the regular Visual Studio template approach.
I think ultimately that I’d prefer to see Visual Studio taking an approach that was closer to this kind of pattern than the mixture of bindings, code-behind and so on as, for me, I find that more confusing in terms of trying to figure out which piece of code is responsible for what.
Beyond that, some of the attempt here to use design-time data in Visual Studio isn’t working for me so if I wanted to get that working better I might have to take a slightly different approach and you could definitely argue that my use of d : DataContext and d : Source sprinkled in a few places throughout the XAML isn’t very maintainable – I should perhaps try and do that in a better way but I was originally trying to duplicate the way that the design time data is loaded up in the Grid project as it ships.
I’ll perhaps post again on this topic but, for now, here’s the source that I’ve got to if you want to play around in it.