Статьи

Решение проблем производительности WinRT XAML GridView на планшетах RT

screenshot_11092012_072155

Давайте начнем с плохих новостей: если ваше  приложение  WinRT XAML использует  GridView с группировкой , ваше приложение может  зависнуть и зависнуть  на планшетах с Windows RT. Не имеет значения, будет ли в GridView тысячи элементов или 50, он потерпит крах. Просто запустите приложение на планшете Windows RT, выполните некоторую навигацию между GridView и сведениями об элементе, и вы заметите, что 1) приложение перестанет реагировать на прикосновения и 2) оно просто закроется. И все это время приложение будет отлично работать на симуляторе и на рабочем столе.

Эта проблема

Когда в GridView включена группировка,  виртуализация не работает . А когда виртуализация не работает, GridView будет иметь серьезные проблемы с производительностью, и ваше приложение будет зависать. Без виртуализации ваше приложение будет использовать гораздо больше памяти, но проблема не полностью вызвана этим: я видел, что приложения, занимающие менее 70 МБ, зависали и зависали при включении группировки.

Сбой произойдет, когда вы вернетесь на страницу, которая имеет:

  • GridView с включенной группировкой
  • NavigationCacheMode включен

Решение

Никогда не включайте группировку в GridView. Без группировки GridView может обрабатывать тысячи и тысячи элементов. Производительность будет отличной.

Если вам нужно сгруппировать элементы, решение состоит в том, чтобы  сделать группы вручную :

  1. Поместите все предметы в одну коллекцию. Коллекция должна содержать не только предметы, но и группы. Например, вот коллекция из 5 предметов, из которых 2 группы: 2012, Фильм 1, Фильм 2, 2011, Фильм 3.
  2. Используйте ItemTemplateSelector GridView для отображения элементов и групп по-разному.
  3. Если вам требуется Semantic zoom, создайте отдельную коллекцию, которая содержит только группы. Итак, одна коллекция со всеми элементами и группами, как описано в 1, и, кроме того, коллекция только с группами.

Приложение не будет выглядеть так, как при встроенной группировке, но будет выглядеть достаточно хорошо. И что важно, он не потерпит крах.

Давайте используем шаги, описанные выше, чтобы преобразовать сбойное приложение WinRT XAML в приложение с высокой производительностью.

Пример приложения

Вот пример приложения, которое загружает сведения о фильме из финской службы «Видео по запросу» и отображает их в GridView, сгруппированном по году выпуска:

screenshot_11092012_074014

Он загружает и отображает 125 фильмов. Он отлично работает на рабочем столе, но на планшете с Windows RT он зависает и падает. Перед сбоем производительность уже вялая. Но перейдите несколько раз (обычно от 3 до 10 раз) к деталям фильма и обратно, и вы заметите, что приложение перестанет реагировать на прикосновения и в конечном итоге произойдет сбой.

Когда мы фиксируем производительность, создавая группы вручную, мы можем показывать тысячи фильмов в одном GridView и при этом иметь доступ к таким функциям, как семантическое масштабирование. Компромисс в том, что конечный результат выглядит иначе: заголовки групп являются частью таблицы.

screenshot_11092012_075755

Создание групп вручную

Код примера приложения показывает все шаги, необходимые для создания групп вручную, но давайте пройдемся по некоторым основам. 

Шаблоны

Прежде всего, у вас есть два шаблона: один для элементов (фильмы) и один для заголовков группы (годы):

<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>

Вам также нужен TemplateSelector, который может выбрать правильный шаблон на основе элемента:

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"]; 
    } 
}

GridView

GridView не должен устанавливать ItemTemplate, вместо этого он указывает на 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>

Данные

В приведенном выше примере GridView использует ObservableCollection с именем «Items» в качестве источника элемента. Эта коллекция ни в коем случае не должна быть сгруппирована. Вместо этого он должен содержать как группы фильмов, так и фильмы:

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);
    }
}

Семантический зум

Если требуется семантическое масштабирование, данные фильма следует разделить на две коллекции: одну, содержащую как фильмы, так и годы, а другую — только годы:

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);
    }
}

ZoomedOutView должен использовать группы как ItemsSource:

<SemanticZoom.ZoomedOutView> 
               <GridView VerticalAlignment="Center" Margin="200,-100,0,0" x:Name="ZoomedOutGrid" ItemsSource="{Binding Groups}"
                         SelectionMode="None">

screenshot_11092012_083312

Чтобы семантическое масштабирование работало правильно, GridView в ZoomedInView следует вручную прокрутить до выбранной группы:

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; 
    } 
}

Мы играем с Opacity, чтобы избавиться от мерцания.

Также возможно уменьшить представление, когда пользователь щелкает заголовок группы, заставляя GridView вести себя как JumpList в Windows Phone:

void ItemView_ItemClick(object sender, ItemClickEventArgs e) 
{ 
    if (e.ClickedItem is MovieInfo) 
        this.Frame.Navigate(typeof (MovieDetailsPage)); 
    else
        this.Zoom.IsZoomedInViewActive = false; 
}

Заключение и исходный код

Элемент управления GridView — отличный способ показать множество элементов пользователю. К сожалению, встроенная поддержка группировки будет зависать и вызывать сбой вашего приложения на планшете Windows RT. Если требуется группировка, создайте группы вручную.

Пример приложения (WinRT-GridView-XAML-Performance-Problem) доступен на  GitHub . По умолчанию он начинается со страницы, которая имеет хорошую производительность и не вылетает на планшете Windows RT. Чтобы попробовать версию с включенной встроенной группировкой, измените начальную страницу приложения на BadPerformancePage.