WPF ListBox со списком ListBox - Виртуализация и прокрутка пользовательского интерфейса

Мой прототип отображает "документы", содержащие "страницы", которые представленных миниатюрами. Каждый документ может иметь любое количество страниц. Например, может быть 1000 документов по 5 страниц каждая или 5 документов на 1000 страниц каждый, или где-то между ними. Документы не содержат других документов. В моей разметке xaml у меня есть ListBox, чья ItemsTemplate ссылается на innerItemsTemplate, который также имеет ListBox. Я хочу 2 уровня выбранных элементов, чтобы я мог выполнять различные операции на документы или страницы (удалить, слить, перейти в новое место и т.д.). Внутренний элемент ItemsTemplate ListBox использует WrapPanel как ItemsPanelTemplate.

Для сценария, где у меня есть большое количество документов с несколькими страниц (скажем, 10000 документов по 5 страниц каждый), прокрутка отлично работает благодаря виртуализации пользовательского интерфейса с помощью VirtualizingStackPanel. Однако у меня проблемы, если у меня большое количество страниц. Документ с 1000 страниц будет отображаться только около 50 за раз (независимо от того, что подходит на экране), а когда я прокручиваю вниз, внешний ListBox переходит к следующему документу, пропуская 950 страниц или так, чтобы они не были видны. Наряду с этим нет VirtualzingWrapPanel, поэтому память приложения действительно увеличивается.

Мне интересно, правильно ли я это сделаю, особенно так как это сложно объяснить! Я хотел бы иметь возможность отображать 10000 документов по 1000 страниц каждый (только показывая все, что подходит на экране), используя виртуализацию пользовательского интерфейса, а также плавную прокрутку.

Как я могу убедиться, что прокрутка перемещается по всем страницам документа прежде чем он отобразит следующий документ и все еще сохранит виртуализацию пользовательского интерфейса? Полоса прокрутки, похоже, перемещается только к следующему документу.

Кажется ли логичным представлять "документы" и "страницы" - с моим текущим методом использования ListBox в пределах ListBox?

Я бы очень признателен за любые ваши идеи. Спасибо.

Ответ 1

Ответ здесь удивителен:

  • Если вы используете ItemsControl или ListBox, вы получите поведение, которое вы испытываете, когда элемент управления прокручивается "по элементу", поэтому вы переходите через целый документ одновременно, НО
  • Если вместо этого вы используете TreeView, элемент управления будет плавно прокручиваться, чтобы вы могли прокручивать документ и в следующий, но он все равно сможет виртуализоваться.

Я думаю, что команда WPF выбрала такое поведение: TreeView обычно имеет элементы, которые больше видимой области, тогда как обычно ListBox es нет.

В любом случае в WPF тривиально сделать вид TreeView и действовать как ListBox или ItemsControl, просто изменив ItemContainerStyle. Это очень просто. Вы можете перевернуть свой собственный или просто скопировать соответствующий шаблон из файла системных тем.

Итак, у вас будет что-то вроде этого:

<TreeView ItemsSource="{Binding documents}">
  <TreeView.ItemsPanel>
    <ItemsPanelTemplate>
      <VirtualizingStackPanel />
    </ItemsPanelTemplate>
  </TreeView.ItemsPanel>
  <TreeView.ItemContainerStyle>
    <Style TargetType="{x:Type TreeViewItem}">
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="{x:Type TreeViewItem}">
            <ContentPresenter /> <!-- put your desired container style here  with a ContentPresenter inside -->
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
  </TreeView.ItemContainerStyle>
  <TreeView.ItemTemplate>
    <DataTemplate TargetType="{x:Type my:Document}">
      <Border BorderThickness="2"> <!-- your document frame will be more complicated than this -->
        <ItemsControl ItemsSource="{Binding pages}">
          ...
        </ItemsControl>
      </Border>
    </DataTemplate>
  </TreeView.ItemTemplate>
</TreeView>

Получение прокрутки на основе пикселов и многоэлемента стиля ListBox

Если вы используете эту технику для получения прокрутки на основе пикселей, внешний элемент ItemsControl, который показывает документы, не может быть ListBox (поскольку ListBox не является подклассом TreeView или TreeViewItem). Таким образом, вы потеряете всю поддержку Multiselect от ListBox. Насколько я могу судить, нет возможности использовать эти две функции вместе, не включая некоторые из ваших собственных кодов для одной функции или другой.

Если вам нужны оба набора функций в одном элементе управления, у вас есть в основном несколько вариантов:

  • Внедрить мультивыбор самостоятельно в подкласс TreeViewItem. Используйте TreeViewItem вместо TreeView для внешнего управления, поскольку он позволяет выбирать несколько дочерних элементов. В шаблоне внутри ItemsContainerStyle: добавьте CheckBox вокруг ContentPresenter, привяжите шаблон CheckBox к IsSelected и создайте CheckBox с помощью шаблона управления, чтобы получить нужный вам вид. Затем добавьте собственные обработчики событий мыши, чтобы обрабатывать Ctrl-Click и Shift-Click для мультиселектора.

  • Внедрите виртуализацию с прокруткой по пикселям самостоятельно в подкласс VirtualizingPanel. Это относительно просто, поскольку большая часть сложности VirtualizingStackPanel связана с непиксельной прокруткой и переработкой контейнеров. Блог Дэна Кравье содержит полезную информацию для понимания VirtualizingPanel.

Ответ 2

В WPF 4.0 можно добиться гладкой прокрутки VirtualizingStackPanels, не жертвуя виртуализацией, если вы готовы использовать отражение для доступа к частной функциональности VirtualizingStackPanel. Все, что вам нужно сделать, это установить для частного свойства IsPixelBased свойство VirtualizingStackPanel значение true.

Обратите внимание, что в .Net 4.5 нет необходимости в этом взломе, поскольку вы можете установить VirtualizingPanel.ScrollUnit = "Pixel".

Чтобы сделать это очень просто, вот код:

public static class PixelBasedScrollingBehavior 
{
    public static bool GetIsEnabled(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsEnabledProperty);
    }

    public static void SetIsEnabled(DependencyObject obj, bool value)
    {
        obj.SetValue(IsEnabledProperty, value);
    }

    public static readonly DependencyProperty IsEnabledProperty =
        DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(PixelBasedScrollingBehavior), new UIPropertyMetadata(false, HandleIsEnabledChanged));

    private static void HandleIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var vsp = d as VirtualizingStackPanel;
        if (vsp == null)
        {
            return;
        }

        var property = typeof(VirtualizingStackPanel).GetProperty("IsPixelBased",
                                                                     BindingFlags.NonPublic | BindingFlags.Instance);

        if (property == null)
        {
            throw new InvalidOperationException("Pixel-based scrolling behaviour hack no longer works!");
        }

        if ((bool)e.NewValue == true)
        {
            property.SetValue(vsp, true, new object[0]);
        }
        else
        {
            property.SetValue(vsp, false, new object[0]);
        }
    }
}

Чтобы использовать это в ListBox, например, вы бы сделали:

<ListBox>
   <ListBox.ItemsPanel>
      <ItemsPanelTemplate>
         <VirtualizingStackPanel PixelBasedScrollingBehavior.IsEnabled="True">
          </VirtualizingStackPanel>
       </ItemsPanelTemplate>
   </ListBox.ItemsPanel>
</ListBox>

Ответ 4

Это сработало для меня. Кажется, это сделает несколько простых атрибутов (.NET 4.5)

<ListBox            
    ItemsSource="{Binding MyItems}"
    VirtualizingStackPanel.IsVirtualizing="True"
    VirtualizingStackPanel.ScrollUnit="Pixel"/>

Ответ 5

Пожалуйста, позвольте мне предисловие к этому вопросу с вопросом: должен ли пользователь всегда видеть каждый миниатюру в каждом элементе списка?

Если ответ на этот вопрос "нет", возможно, было бы возможно ограничить количество видимых страниц внутри внутреннего шаблона элемента (при условии, что вы указали, что прокрутка работает хорошо, скажем, на 5 страницах) и используйте отдельный шаблон "выбранного элемента", который больше, и отображает все страницы для этого документа? Билли Холлис объясняет, как "вытащить" выделенный элемент в списке на dnrtv эпизод 115