WinRT XAML GridView Performance Problems on Windows RT Tablets
Let’s start with the bad news: If yourWinRT XAML app uses the GridView with grouping, you’re app is quite likely going to hang and crash on Windows RT tablets. It doesn’t matter if the GridView has thousands of items or 50, it will crash. Just start the app on a Windows RT tablet, do some navigation between the GridView and item details and you’ll notice that 1) the application will stop responding to touch and 2) it just closes. And all this time the app will work just fine on the simulator and on the desktop.
The problem
When the grouping is enabled on the GridView, the virtualization doesn’t work. And when the virtualization doesn’t work, the GridView will have severe performance problems and your app will crash. Without the virtualization your app will use much more memory but the problem isn’t entirely caused by this: I’ve seen apps taking less than 70 MB hang and crash when the grouping has been enabled.
The crashing will happen when you navigate back to a page which has:
- GridView with grouping enabled
- NavigationCacheMode set to enabled
The solution
Never enable the grouping on the GridView. Without grouping the GridView can handle thousands and thousands of items. The performance will be great.
If you need to group the items, the solution is to do the groups manually:
- Put all the items into a single collection. The collection should contain not just the items but also the groups. For example here’s a collection with 5 items, from which 2 are groups: 2012, Movie 1, Movie 2, 2011, Movie 3.
- Use the GridView’s ItemTemplateSelector to display the items and groups differently.
- If you require Semantic zoom, create a separate collection which contains just the groups. So one collection with all the items and groups as described in 1 and, in addition to that, a collection with just the groups.
The app will not end up looking just like with the built-in grouping, but it will look good enough. And what’s important, it will not crash.
Let’s use the steps described above to transform a crashing WinRT XAML app to an app with great performance.
Example app
Here’s an example app which loads movie details from a Finnish Video On Demand service and displays them on a GridView, grouped by the release year:
It loads and displays 125 movies. It will perform great on the desktop but on the Windows RT tablet it will hang and crash. Before crashing the performance is already sluggish. But navigate few times (usually between 3 to 10 times) to the movie details and back and you’ll notice that the app will stop responding to touch and it will eventually crash.
When we fix the performance by creating the groups manually, we can show thousands movies in a single GridView and still have access to features like the semantic zoom. The trade-off is that the end result looks different: The group headers are part of the grid.
Creating the groups manually
The sample app’s code shows all the steps required to create the groups manually but let’s go through some of the basics.
The templates
First of all, you mush have two templates: One for the items (movies) and one for the group headers (years):
<DataTemplate x:Key="MovieTemplate"> <Grid HorizontalAlignment="Left" Width="250" Height="250"> <Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}"> <Image Source="{Binding Cover}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/> </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 Year}" Foreground="{StaticResource ListViewItemOverlaySecondaryForegroundThemeBrush}" Style="{StaticResource CaptionTextStyle}" TextWrapping="NoWrap" Margin="15,0,15,10"/> </StackPanel> </Grid> </DataTemplate> <DataTemplate x:Key="MovieCategoryTemplate"> <Grid HorizontalAlignment="Left" Width="250" Height="250"> <StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}"> <TextBlock Text="{Binding}" Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}" Style="{StaticResource TitleTextStyle}" Height="60" Margin="15,0,15,0"/> </StackPanel> </Grid> </DataTemplate>
You also need a TemplateSelector which can select the correct template based on the item:
public class MyTemplateSelector : DataTemplateSelector { protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) { var movie = item as MovieInfo; if (movie != null) return (DataTemplate) App.Current.Resources["MovieTemplate"]; return (DataTemplate)App.Current.Resources["MovieCategoryTemplate"]; } }
The GridView
The GridView shouldn’t set the ItemTemplate, instead it points to the TemplateSelector:
<GridView x:Name="itemGridView" TabIndex="1" Grid.RowSpan="2" Padding="116,157,40,46" ItemsSource="{Binding Items}" ItemTemplateSelector="{StaticResource MyTemplateSelector}" SelectionMode="None" IsSwipeEnabled="false" IsItemClickEnabled="True" ItemClick="ItemView_ItemClick"> </GridView>
The data
In the example above, the GridView uses an ObservableCollection called “Items” as the item source. This collection shouldn’t be grouped by any way. Instead it should contain both the movie groups and the movies:
public ObservableCollection<object> Items { get; set; } ... var moviesByYear = movies.GroupBy(x => x.Year); foreach (var group in moviesByYear) { this.Items.Add(group.Key.ToString()); foreach (var movieInfo in group) { this.Items.Add(movieInfo); } }
Semantic zoom
If semantic zoom is required, the movie data should be split into two collections: One containing both the movies and the years and the other containing only the years:
public ObservableCollection<object> Items { get; set; } public ObservableCollection<string> Groups { get; set; } ... var moviesByYear = movies.GroupBy(x => x.Year); foreach (var group in moviesByYear) { // The group is added to two collections: Collection containing only the groups and the collection containing movies and the groups this.Groups.Add(group.Key.ToString()); this.Items.Add(group.Key.ToString()); // The movies are only added to the collection containing movies and groups foreach (var movieInfo in group) { this.Items.Add(movieInfo); } }
The ZoomedOutView should use the Groups as ItemsSource:
<SemanticZoom.ZoomedOutView> <GridView VerticalAlignment="Center" Margin="200,-100,0,0" x:Name="ZoomedOutGrid" ItemsSource="{Binding Groups}" SelectionMode="None">
In order for the semantic zoom to work correctly the ZoomedInView’s GridView should be manually scrolled to the selected group:
private void SemanticZoom_OnViewChangeStarted(object sender, SemanticZoomViewChangedEventArgs e) { if (e.IsSourceZoomedInView) return; this.itemGridView.Opacity = 0; }
private void SemanticZoom_OnViewChangeCompleted(object sender, SemanticZoomViewChangedEventArgs e) { if (e.IsSourceZoomedInView) return; try { var selectedGroup = e.SourceItem.Item as string; if (selectedGroup == null) return; itemGridView.ScrollIntoView(selectedGroup, ScrollIntoViewAlignment.Leading); } finally { this.itemGridView.Opacity = 1; } }
We play with the Opacity to get rid of some flickering.
It’s also possible to zoom out the view when a user clicks a group header, making the GridView to behave like a JumpList in Windows Phone:
void ItemView_ItemClick(object sender, ItemClickEventArgs e) { if (e.ClickedItem is MovieInfo) this.Frame.Navigate(typeof (MovieDetailsPage)); else this.Zoom.IsZoomedInViewActive = false; }
Conclusion and the source code
The GridView control is a great way to show lots of items to the user. Unfortunately the built-in support for grouping will hang and crash your application on a Windows RT tablet. If grouping is required, create the groups manually.
The sample app (WinRT-GridView-XAML-Performance-Problems) is available from GitHub. By default it starts with the page which has good performance and doesn’t crash on a Windows RT tablet. To try out the version with built-in grouping turned on, change the start page of the app to BadPerformancePage.