In this WinRT tutorial we will build a single page application using the following building blocks:
- XAML & C#
- MVVM
- Data binding
- GridView
- SemanticZoom –control
We will start from the scratch, using the “Blank application” template. Then we continue by creating the view model. After the view model is ready we will create the view by adding a page using the “Grouped Items Page” template. At last step we’re going to modify the page to support the SemanticZoom –control.
Update: Make sure to read about the GridView's performance problems.
Update: The examples and source code has been updated to work with Windows 8 RTM.
1. Creating the project
Let’s start by creating a new application using the “Blank application” template. This template contains a single page, “BlankPage.xaml”, which we can delete. Our application will contain only one page and we’re going to add this as a last step, using the built-in “Grouped Items Page” template. This template contains the GridView which we’ll use to to implement the semantic zoom.
But before creating the view, we’ll need a view model.
2. Creating the view model
Add a C# class and name it “MoviesPageViewModel.cs”. This is going to be our view model and it’ll work as the DataContext for our view.
The models
Our application shows movies grouped by their categories. For this we’ll need a new class which represents a single movie. This class can be added to the MoviesPageViewModel:
public class Movie
{
public string Title { get; set; }
public string Subtitle { get; set; }
public string Image { get; set; }
public string Category { get; set; }
}
We also need a model which represents a single movie category:
public class MovieCategory
{
public string Title { get; set; }
public List<Movie> Items { get; set; }
}
The data
We’re not going to connect our application into the net, instead we’re using static data. The movie instances can be created inside the MoviePageViewModel’s constructor:
var movies = new List<Movie>
{
new Movie {Title = "The Ewok Adventure", Category = "Adventure", Subtitle = "The Towani family civilian shuttlecraft crashes on the forest moon of Endor.", Image = "http://cf2.imgobject.com/t/p/w500/y6HdTlqgcZ6EdsKR1uP03WgBe0C.jpg"},
new Movie {Title = "The In-Laws", Category = "Adventure", Subtitle = "In preparation for his daughter's wedding, dentist Sheldon ", Image = "http://cf2.imgobject.com/t/p/w500/9FlFW9zhuoOpS8frAFR9cCnJ6Sg.jpg"},
new Movie {Title = "The Man Called Flintstone", Category = "Adventure", Subtitle = "In this feature-length film based on the Flintstones TV show", Image = "http://cf2.imgobject.com/t/p/w500/6qyVUkbDBuBOUVVplIDGaQf6jZL.jpg"},
new Movie {Title = "Super Fuzz", Category = "Comedy", Subtitle = "Dave Speed is no ordinary Miami cop--he is an irradiated Miami cop ", Image = "http://cf2.imgobject.com/t/p/w500/bueVXkpCDPX0TlsWd3Uk7QKO3kD.jpg"},
new Movie {Title = "The Knock Out Cop", Category = "Comedy", Subtitle = "This cop doesn't carry a gun - his fist is loaded!", Image = "http://cf2.imgobject.com/t/p/w500/mzlw8rHGUSDobS1MJgz8jXXPM06.jpg"},
new Movie {Title = "Best Worst Movie", Category = "Comedy", Subtitle = "A look at the making of the film Troll 2 (1990) ", Image = "http://cf2.imgobject.com/t/p/w500/5LjbAjkPBUOD9N2QFPSuTyhomx4.jpg"},
new Movie {Title = "The Last Unicorn", Category = "Fantasy", Subtitle = "A brave unicorn and a magician fight an evil king", Image = "http://cf2.imgobject.com/t/p/w500/iO6P5vV1TMwSuisZDtNBDNpOxwR.jpg"},
new Movie {Title = "Blithe Spirit", Category = "Fantasy", Subtitle = "An English mystery novelist invites a medium", Image = "http://cf2.imgobject.com/t/p/w500/gwu4c10lpgHUrMqr9CBNq2FYTpN.jpg"},
new Movie {Title = "Here Comes Mr. Jordan", Category = "Fantasy", Subtitle = "Boxer Joe Pendleton, flying to his next fight", Image = "http://cf2.imgobject.com/t/p/w500/9cnWl7inQVX6wznjYNmQmJXVD6J.jpg"},
};
Grouping the data
Then we can group the movies by their categories and create a “MovieCategory” instance for each category:
var moviesByCategories = movies.GroupBy(x => x.Category)
.Select(x => new MovieCategory { Title = x.Key, Items = x.ToList() });
Publishing the data from the view model to the view
We’re going to use data binding to create the view. As we now have the movies in the format we want, we’ll only have to create a public property which the view can bind against. For this we’ll add a new property to the view model:
public List<MovieCategory> Items { get; set; }
The property can be initialized after we have grouped the movies:
Items = moviesByCategories.ToList();
And that’s it. We now have the view model ready and it’s quite simple. The data is created inside the constructor and the view model only has one public property. The view can bind its GridView against this property in order to access the movies.
It’s important to notice that the view model’s public property contains grouped data. We want to display the data in a grouped format, where each movie category is displayed separately. This is why the property contains a list of data where every item has a title and a collection of child items.
The whole view model
Here's how our MoviesPageViewModel.cs ended up looking:
using System.Collections.Generic;
using System.Linq;
namespace WinRT_MVVM_GridView_SemanticZoom
{
public class MoviesPageViewModel
{
public List<MovieCategory> Items { get; set; }
public MoviesPageViewModel()
{
var movies = new List<Movie>
{
new Movie {Title = "The Ewok Adventure", Category = "Adventure", Subtitle = "The Towani family civilian shuttlecraft crashes on the forest moon of Endor.", Image = "http://cf2.imgobject.com/t/p/w500/y6HdTlqgcZ6EdsKR1uP03WgBe0C.jpg"},
new Movie {Title = "The In-Laws", Category = "Adventure", Subtitle = "In preparation for his daughter's wedding, dentist Sheldon ", Image = "http://cf2.imgobject.com/t/p/w500/9FlFW9zhuoOpS8frAFR9cCnJ6Sg.jpg"},
new Movie {Title = "The Man Called Flintstone", Category = "Adventure", Subtitle = "In this feature-length film based on the Flintstones TV show", Image = "http://cf2.imgobject.com/t/p/w500/6qyVUkbDBuBOUVVplIDGaQf6jZL.jpg"},
new Movie {Title = "Super Fuzz", Category = "Comedy", Subtitle = "Dave Speed is no ordinary Miami cop--he is an irradiated Miami cop ", Image = "http://cf2.imgobject.com/t/p/w500/bueVXkpCDPX0TlsWd3Uk7QKO3kD.jpg"},
new Movie {Title = "The Knock Out Cop", Category = "Comedy", Subtitle = "This cop doesn't carry a gun - his fist is loaded!", Image = "http://cf2.imgobject.com/t/p/w500/mzlw8rHGUSDobS1MJgz8jXXPM06.jpg"},
new Movie {Title = "Best Worst Movie", Category = "Comedy", Subtitle = "A look at the making of the film Troll 2 (1990) ", Image = "http://cf2.imgobject.com/t/p/w500/5LjbAjkPBUOD9N2QFPSuTyhomx4.jpg"},
new Movie {Title = "The Last Unicorn", Category = "Fantasy", Subtitle = "A brave unicorn and a magician fight an evil king", Image = "http://cf2.imgobject.com/t/p/w500/iO6P5vV1TMwSuisZDtNBDNpOxwR.jpg"},
new Movie {Title = "Blithe Spirit", Category = "Fantasy", Subtitle = "An English mystery novelist invites a medium", Image = "http://cf2.imgobject.com/t/p/w500/gwu4c10lpgHUrMqr9CBNq2FYTpN.jpg"},
new Movie {Title = "Here Comes Mr. Jordan", Category = "Fantasy", Subtitle = "Boxer Joe Pendleton, flying to his next fight", Image = "http://cf2.imgobject.com/t/p/w500/9cnWl7inQVX6wznjYNmQmJXVD6J.jpg"},
};
var moviesByCategories = movies.GroupBy(x => x.Category)
.Select(x => new MovieCategory { Title = x.Key, Items = x.ToList() });
Items = moviesByCategories.ToList();
}
}
public class Movie
{
public string Title { get; set; }
public string Subtitle { get; set; }
public string Image { get; set; }
public string Category { get; set; }
}
public class MovieCategory
{
public string Title { get; set; }
public List<Movie> Items { get; set; }
}
}
3. Creating the view
Now that the view model is ready, we can focus on the view. Let’s start by adding a new page using the “Grouped Items Page” template. We can name the page “Movies.xaml”. This page template comes with a GridView configured to show grouped data, so it’s an ideal place to start.
Configuring application’s start-up page
After adding the page, we must modify the app.xaml.cs so that the new page will be used as the first page of the application. To do this we can modify the OnLaunched-method:
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
…
if (!rootFrame.Navigate(typeof(Movies)))
…
}
Creating the connection between the page and the view model
We now have the page (the view) and the view model. Next step is to create an instance of the view model and set it as the data context of the page. This can be done in the page’s constructor:
public Movies()
{
this.InitializeComponent();
var viewModel = new MoviesPageViewModel();
this.DataContext = viewModel;
}
Binding the GridView to a correct property
As you may recall, we named the view model’s public property as “Items”. By default the grouped items page is bound to a property called “Groups”. To change this we can modify the CollectionViewSource declared in the beginning of the Movies.xaml:
<CollectionViewSource
x:Name="groupedItemsViewSource"
Source="{Binding Items}"
IsSourceGrouped="true"
ItemsPath="Items"/>
Running the application the first time
Now that we have the view and the view model ready, we can run the application the first time:
And it actually looks pretty good! You may wonder how the view can display the movie poster, name and the description even though we haven’t created any data bindings between the items and the movies. The reason this works is because the default “Grouped Items Page” template defines the ItemTemplate like this:
The “Standard250x250ItemTemplate” is defined inside the CommonStandardStyles.xaml:
<DataTemplate x:Key="Standard250x250ItemTemplate">
<Grid HorizontalAlignment="Left" Width="250" Height="250">
<Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}">
<Image Source="{Binding Image}" Stretch="UniformToFill"/>
</Border>
<StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}">
<TextBlock Text="{Binding Title}" Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}" Style="{StaticResource TitleTextStyle}" Height="60" Margin="15,0,15,0"/>
<TextBlock Text="{Binding Subtitle}" Foreground="{StaticResource ListViewItemOverlaySecondaryForegroundThemeBrush}" Style="{StaticResource CaptionTextStyle}" TextWrapping="NoWrap" Margin="15,0,15,10"/>
</StackPanel>
</Grid>
</DataTemplate>
Here’s our Movie-class again:
public class Movie
{
public string Title { get; set; }
public string Subtitle { get; set; }
public string Image { get; set; }
public string Category { get; set; }
}
If you compare the item template against the movie-class, you can see that the template’s binding match the class’ properties.
Now we just need the SemanticZoom.
4. Adding the SemanticZoom –control
Semantic zoom is an UI concept defined by Microsoft like the following:
Semantic Zoom is a touch-optimized technique used by Metro style apps in Windows 8 Consumer Preview for presenting and navigating large sets of related data or content within a single view (such as a photo album, app list, or address book).
SemanticZoom then is a XAML-control which can be used to add the semantic zoom concept into a WinRT application. The SemanticZoom –control requires that we have two representations of the same data: The ZoomedInView and the ZoomedOutView. In our case the ZoomednView is already done because we can use the working GridView. But we need an another List which displays the data when the user zooms out using the Ctrl+Mousewheel. The SemanticZoom only supports the GridView and the ListView controls.
Replacing the ScrollViewer with SemanticZoom
The first and most important thing to do is to completely remove the ScrollViewer named “itemGridScrollViewer” from the Movies.xaml. The SemanticZoom control won’t work correctly if it’s inside the ScrollViewer.
Many people have problems getting the ZoomedOutView and ZoomedInView “in sync”. The usual problem is that when the item in ZoomedOutView is clicked, the ZoomedInView doesn’t scroll to the correct position. The reason for this problem usually is that the SemanticZoom –control is inside a ScrollViewer.
We can then replace the ScrollViewer with a SemanticZoom. Note that the Margin and Grid.Row properties should be copied from the ScrollViewer to the SemanticZoom:
<SemanticZoom x:Name="Zoom" Grid.Row="1" Margin="0,-3,0,0">
<SemanticZoom.ZoomedInView>
</SemanticZoom.ZoomedInView>
<SemanticZoom.ZoomedOutView>
</SemanticZoom.ZoomedOutView>
</SemanticZoom>
Creating the ZoomedInView
Creating the ZoomedInView is easy because we’ll already done that. We just need to move the “itemGridView” inside the ZoomedInView-section.
Creating the ZoomedOutView
We don’t have the ZoomedOutView ready but the following GridView can do the trick:
<GridView VerticalAlignment="Center" Margin="200 -200 0 0">
<GridView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock HorizontalAlignment="Center"
Text="{Binding Group.Title}"
Style="{StaticResource SubheaderTextStyle}"
/>
</StackPanel>
</DataTemplate>
</GridView.ItemTemplate>
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem">
<Setter Property="Margin" Value="4" />
<Setter Property="Padding" Value="5" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
</GridView.ItemContainerStyle>
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<WrapGrid ItemWidth="400" ItemHeight="70"
Orientation="Horizontal" VerticalChildrenAlignment="Center" MaximumRowsOrColumns="3"></WrapGrid>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
</GridView>
The most important things to notice:
- The GridView’s ItemsSource isn’t set in the XAML
- The TextBox’s data binding is set correctly to “Group.Title”. In this case the Title is bound against the MovieCategory-instances Title-property
The whole SemanticZoom XAML
Overall, this is how the SemanticZoom -control is defined in the XAML:
<SemanticZoom x:Name="Zoom" Grid.Row="1" Margin="0,-3,0,0">
<SemanticZoom.ZoomedInView>
<GridView
x:Name="itemGridView"
AutomationProperties.AutomationId="ItemGridView"
AutomationProperties.Name="Grouped Items"
Margin="116,0,40,46"
ItemsSource="{Binding Source={StaticResource groupedItemsViewSource}}"
ItemTemplate="{StaticResource Standard250x250ItemTemplate}">
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<Grid Margin="1,0,0,6">
<Button
AutomationProperties.Name="Group Title"
Content="{Binding Title}"
Style="{StaticResource TextButtonStyle}"/>
</Grid>
</DataTemplate>
</GroupStyle.HeaderTemplate>
<GroupStyle.Panel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid Orientation="Vertical" Margin="0,0,80,0"/>
</ItemsPanelTemplate>
</GroupStyle.Panel>
</GroupStyle>
</GridView.GroupStyle>
</GridView>
</SemanticZoom.ZoomedInView>
<SemanticZoom.ZoomedOutView>
<GridView VerticalAlignment="Center" Margin="200 -200 0 0">
<GridView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock HorizontalAlignment="Center"
Text="{Binding Group.Title}"
Style="{StaticResource SubheaderTextStyle}"
/>
</StackPanel>
</DataTemplate>
</GridView.ItemTemplate>
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem">
<Setter Property="Margin" Value="4" />
<Setter Property="Padding" Value="5" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
</GridView.ItemContainerStyle>
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<WrapGrid ItemWidth="400" ItemHeight="70"
Orientation="Horizontal" VerticalChildrenAlignment="Center" MaximumRowsOrColumns="3"></WrapGrid>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
</GridView>
</SemanticZoom.ZoomedOutView>
</SemanticZoom>
Configuring the ZoomedOutView’s ItemsSource:
The last step before running the app is to modify the Movies.xaml.cs. In this file we must define how the ListView inside the ZoomedOutView gets its data. We can add the required code to OnNavigatedTo-method:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
var collectionGroups = groupedItemsViewSource.View.CollectionGroups;
((ListViewBase)this.Zoom.ZoomedOutView).ItemsSource = collectionGroups;
}
Running the complete application
Our application is ready. The front page looks identical to the previous version:
But if you use the Ctrl+Mousewheel, you can see the semantic zoom in action:
The source code
This tutorial's source code is available from the GitHub.
Links