Панели и виртуализация в WinRT

117

В классе ListBoxWithItemTemplate из предыдущего проекта я сделал нечто такое, чего почти не делал ранее: я оставил фрагмент отладочного кода. Сделано это было с одной целью; дать вам представление о чем-то очень важном, что происходит во внутренней реализации ListBox. Элемент Border, в который заключается каждый вариант списка, определяет обработчик для события Loaded:

<Border BorderBrush="{Binding Path=Foreground,
                              RelativeSource={RelativeSource TemplatedParent}}"
        BorderThickness="1"
        Width="340" Margin="5"
        Loaded="Border_Loaded">

В обработчике Loaded вызов System.Diagnostics.Debug.WriteLine выводит свойство Name объекта NamedColor, заданного свойству DataContext загруженного элемента:

private void Border_Loaded(object sender, RoutedEventArgs e)
{
     System.Diagnostics.Debug.WriteLine("Элемент Item загружен: " +
          (((FrameworkElement)sender).DataContext as NamedColor).Name);
}

Запустите эту программу в отладчике Visual Studio и понаблюдайте за окном Output. При первой загрузке программы вы увидите лишь несколько цветов, а не весь список из 141 пункта. На моем планшете экран с высотой 768 пикселов позволяет вывести 6 полных вариантов (от AliceBlue до Beige) и половину следующего варианта. Список в окне Output показывает, что были загружены визуальные деревья для 11 вариантов, от AliceBlue до BlueViolet.

Теперь начните прокручивать список. Возможно, вы увидите в окне Output еще несколько вариантов (я вижу Brown, BurlyWood и CadetBlue), но на этом список останавливается. Что происходит?

Класс ListBox старается действовать эффективно. Он строит визуальные деревья только для вариантов, выводимых изначально (и еще пары), а затем повторно использует эти визуальные деревья, когда некоторые варианты уходят с экрана в результате прокрутки. А почему бы и нет? Для этого нужно совсем немного - изменить привязки. Виртуализация приобретает особую важность, когда вы начинаете связывать свои элементы управления ListBox с коллекциями из сотен и тысяч объектов. Но это также означает, что ListBox не может определить ширину, необходимую для отображения всех вариантов.

Будьте внимательны: у объектов коллекции могут быть некоторые особенности, которые создают проблемы с виртуализацией. Позже мы рассмотрим один из таких случаев с визуальными деревьями, содержащими ссылки друг на друга. В определенных ситуациях виртуализацию можно отключить.

Списковый элемент управления всегда использует некую панель для вывода. Вы можете указать, какой из классов, производных от Panel, следует использовать, или же задать ваш собственный класс. ItemsControl определяет (a ListBox наследует) свойство с именем ItemsPanel, которому можно задать объект типа ItemsPanelTemplate. Это второй из трех шаблонов, упоминаемых ранее, но он безусловно является самым простым. Шаблон ItemsPanelTemplate состоит из одного элемента: класса, производного от Panel. Заданная панель используется списковым элементом управления для размещения вариантов. Для обычного класса ItemsControl это класс StackPanel, а для ListBox - VirtualizingStackPanel.

В приложении ListBoxWithItemTemplate можно задать свойству ItemsPanel элемента управления ListBox значение но умолчанию при помощи следующей разметки:

<ListBox Name="listbox"
         ItemsSource="{Binding Source={StaticResource namedcolors},
                               Path=All}"
         Width="400"
         HorizontalAlignment="Center">
    <ListBox.ItemTemplate>
        ...
    </ListBox.ItemTemplate>
    
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel />
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
</ListBox>

Теперь попробуйте заменить VirtualizingStackPanel в этом фрагменте обычной панелью StackPanel:

<ListBox.ItemsPanel>
    <ItemsPanelTemplate>
         <StackPanel></StackPanel>
    </ItemsPanelTemplate>
</ListBox.ItemsPanel>

Теперь все объекты создаются при первой загрузке ListBox - в этом легко убедиться в окне Output в Visual Studio. Однако можно поступить и так:

<ListBox.ItemsPanel>
    <ItemsPanelTemplate>
        <VirtualizingStackPanel Orientation="Horizontal" />
    </ItemsPanelTemplate>
</ListBox.ItemsPanel>

Вертикальный список ListBox превращается в горизонтальный. Вернее, почти превращается. Вам также придется внести некоторые изменения в размер ListBox и свойства внутреннего объекта ScrollViewer, как в проекте HorizontalListBox:

<Page ...>

    <Grid>
        
        <Grid.Resources>
            <local:NamedColor x:Key="namedcolors" />
        </Grid.Resources>

        <ListBox Name="listbox"
                 ItemsSource="{Binding Source={StaticResource namedcolors},
                                       Path=All}"
                 Height="140"
                 ScrollViewer.HorizontalScrollBarVisibility="Auto"
                 ScrollViewer.HorizontalScrollMode="Enabled"
                 ScrollViewer.VerticalScrollBarVisibility="Disabled"
                 ScrollViewer.VerticalScrollMode="Disabled">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="{Binding Path=Foreground,
                                    RelativeSource={RelativeSource TemplatedParent}}"
                            BorderThickness="1"
                            Width="340" Margin="5"
                            Loaded="Border_Loaded">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto" />
                                <ColumnDefinition />
                            </Grid.ColumnDefinitions>

                            <Rectangle Width="80" Height="80"
                                       Margin="5,5,10,5">
                                <Rectangle.Fill>
                                    <SolidColorBrush Color="{Binding Color}" />
                                </Rectangle.Fill>
                            </Rectangle>

                            <TextBlock Grid.Column="1" Text="{Binding Name}"
                                       FontSize="32" VerticalAlignment="Center" />
                        </Grid>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
            
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel Orientation="Horizontal" />
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>

        <Grid.Background>
            <SolidColorBrush Color="{Binding ElementName=listbox,
                                             Path=SelectedItem.Color}" />
        </Grid.Background>
    </Grid>
</Page>

Класс ScrollViewer определяет несколько свойств, управляющих внешним видом и функциональностью элемента управления, однако в некоторых ситуациях сам объект ScrollViewer недоступен (например, когда он находится в ListBox). Для таких ситуаций ScrollViewer определяет несколько вложенных свойств, которые можно задать прямо в теге ListBox.

Этот элемент управления ListBox отличается от предыдущей программы только использованием горизонтальной панели VirtualizingStackPanel и некоторыми изменениями в теге ListBox, которые модифицируют размеры элемента и обеспечивают горизонтальную прокрутку. В результате мы получаем полностью работоспособный горизонтальный список ListBox:

Использование горизонтального списка ListBox с виртуализацией

Как ни странно, попытки использования панелей WrapGrid или VariableSizedWrapGrid с ListBox оказались безуспешными. Программа выдает исключение с сообщением о невозможности использования данного типа Panel в качестве значения свойства ItemsPanel. В следующей статье будет представлен аналог этих панелей.

Пройди тесты
Лучший чат для C# программистов