With this short article, I want to show you the easy way to provide data to a semantic zoom in a Windows 8 Store app. I’ve seen too many developers trying to massage their business data to make it consumable for a semantic zoom, by pulling it through custom implementations of ISemanticZoomInformation or through other data adapter classes. You see this pattern even in the official XAML GridView grouping and SemanticZoom sample. All you need to remember from this article, is that such infrastructure is not necessary: the grouped data source for the CollectionViewSource that populates the two zoom levels of a semantic zoom control, can be generated by a simple LINQ query that your ViewModel can pull straight out of your Models.
As usual, I built a simple MVVM app to illustrate my point. It comes with two Model classes: Item and Category, where each item has a category. I do realize that this will not help me win the Oscar for the most original scenario. The data access layer provides a list of both. The only View in the app just shows a semantic zoom that pulls its data (items with a category) from a property in the main ViewModel.
Here’s how the two levels of the semantic zoom look like at runtime.
Zoomed in:
Zoomed out:
The CollectionViewSource is defined as a resource in XAML, and bound to the zoomed-in view of the semantic zoom:
<Page.Resources>
<CollectionViewSource x:Name="collectionViewSource"
IsSourceGrouped="True"
Source="{Binding ItemsByCategory}" />
</Page.Resources>
<!-- ... -->
<SemanticZoom x:Name="semanticZoom">
<SemanticZoom.ZoomedInView>
<GridView ItemsSource="{Binding Source={StaticResource collectionViewSource}}"
SelectionMode="None">
<!-- ... -->
</GridView>
</SemanticZoom.ZoomedInView>
<!-- ... -->
</SemanticZoom>
|
The CollectionViewSource gets its data from the viewmodel through a LINQ query that yields the items, grouped by category:
itemsByCategory = from item in Items
orderby item.Name
group item by item.Category into g
orderby g.Key.Name
select g;
By the way, this is how that same query looks like in the official sample:
internal List<GroupInfoList<object>> GetGroupsByCategory()
{
List<GroupInfoList<object>> groups = new List<GroupInfoList<object>>();
var query = from item in Collection
orderby ((Item)item).Category
group item by ((Item)item).Category into g
select new { GroupName = g.Key, Items = g };
foreach (var g in query)
{
GroupInfoList<object> info = new GroupInfoList<object>();
info.Key = g.GroupName;
foreach (var item in g.Items)
{
info.Add(item);
}
groups.Add(info);
}
return groups;
}
|
That's a lot of obsolete code. For the sake of simplicity, I spare you from the definition of the -also obsolete- GroupInfoList class. Looking at this code, one can only agree when a tweet like this one pops up in his timeline:
Enough ranting, let's go back to the sample.
The LINQ query returns an IEnumerable<IGrouping<Category, Item>>. And that’s enough to provide a source for all semantic zoom bindings.
The zoomed-out view of the semantic zoom has several templates. The item template is bound to each item, so the binding expressions are straightforward:
<GridView.ItemTemplate>
<DataTemplate>
<!-- Bound to an Item -->
<StackPanel>
<Image Source="{Binding Image}"
... />
<Border ... >
<TextBlock Text="{Binding Name}"
... />
</Border>
</StackPanel>
</DataTemplate>
</GridView.ItemTemplate>
Each grouping (= category) has a group style header, of which the template is bound to an IGrouping. That interface only provides a Key attribute, but that's enough to provide the title:
<GroupStyle.HeaderTemplate>
<DataTemplate>
<!-- Bound to an IGrouping with Category as Key-->
<Grid Margin="0">
<TextBlock Text='{Binding Key.Name}'
... />
</Grid>
</DataTemplate>
</GroupStyle.HeaderTemplate>
The zoomed-in view of the semantic zoom is bound to the collection of groups in the data source. We (still) have to do this in code behind:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
// IObservableVector<IGrouping<Category, Item>>
(this.semanticZoom.ZoomedOutView as ListViewBase).ItemsSource = collectionViewSource.View.CollectionGroups;
}
This is an ICollectionViewGroup, so it comes with Group and GroupItems properties. These properties come in handy to provide binding expressions. Group.Key returns the category instance, but you could also choose to just follow the links in the viewmodel from the items' perspective:
<GridView.ItemTemplate>
<DataTemplate>
<!-- Bound to an ICollectionViewGroup -->
<StackPanel>
<Border>
<TextBlock Text="{Binding Group.Key}"
... />
</Border>
<Image Source="{Binding GroupItems[0].Category.Image}"
... />
</StackPanel>
</DataTemplate>
</GridView.ItemTemplate>