使用CollectionViewSource和GroupDescriptions(即IsGrouping == True)时,如何使ListBox滚动到视图中?

11

简短版

当选择改变时,我想将ListBox项目滚动到视图中。

详细版

我有一个 ListBox,其 ItemsSource 绑定到带有 GroupDescriptionCollectionViewSource,如下面的示例所示。

<Window.Resources>
    <CollectionViewSource x:Key="AnimalsView" Source="{Binding Source={StaticResource Animals}, Path=AnimalList}">
        <CollectionViewSource.GroupDescriptions>
            <PropertyGroupDescription PropertyName="Category"/>
        </CollectionViewSource.GroupDescriptions>
    </CollectionViewSource>  
</Window.Resources>

<ListBox x:Name="AnimalsListBox"ItemsSource="{Binding Source={StaticResource AnimalsView}}" ItemTemplate="{StaticResource AnimalTemplate}" SelectionChanged="ListBox_SelectionChanged">
    <ListBox.GroupStyle>
        <GroupStyle HeaderTemplate="{StaticResource CategoryTemplate}" />
    </ListBox.GroupStyle>
</ListBox>

在代码后台文件中有一个SelectionChanged事件。

public List<Animal> Animals { get; set; }

private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ListBox control = (ListBox)sender;
    control.ScrollIntoView(control.SelectedItem);
}
现在,如果我将AnimalsListBox.SelectedItem设置为当前不可见的项目,我希望它能够滚动到视图中。这就是困难所在,因为ListBox正在进行分组(IsGrouped属性为true),调用ScrollIntoView会失败。
通过Reflector查看System.Windows.Controls.ListBox。请注意,在OnBringItemIntoView中的base.IsGrouping
public void ScrollIntoView(object item)
{
    if (base.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
    {
        this.OnBringItemIntoView(item);
    }
    else
    {
        base.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new DispatcherOperationCallback(this.OnBringItemIntoView), item);
    }
}

private object OnBringItemIntoView(object arg)
{
    FrameworkElement element = base.ItemContainerGenerator.ContainerFromItem(arg) as FrameworkElement;
    if (element != null)
    {
        element.BringIntoView();
    }
    else if (!base.IsGrouping && base.Items.Contains(arg))
    {
        VirtualizingPanel itemsHost = base.ItemsHost as VirtualizingPanel;
        if (itemsHost != null)
        {
            itemsHost.BringIndexIntoView(base.Items.IndexOf(arg));
        }
    }
    return null;
}

问题

  1. 有人能解释一下为什么使用分组时它不起作用吗?
    • ItemContainerGenerator.ContainerFromItem总是返回 null,尽管其状态表明所有容器都已生成。
  2. 我如何在使用分组时实现滚动到视图?
2个回答

10
我已经找到解决方案。我确信我不是第一个遇到这个问题的人,所以继续在StackOverflow上寻找解决方案时,我偶然发现了David提供的答案有关分组列表中ItemContainerGenerator的工作原理
David的解决方案是在渲染过程之后延迟访问ItemContainerGenerator
我已经实施了这个解决方案,并进行了一些更改,我将在后面详细说明。
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ListBox control = (ListBox)sender;

    if (control.IsGrouping)
    {
         if (control.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
              Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(DelayedBringIntoView));
         else
              control.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
    }
    else
        control.ScrollIntoView(control.SelectedItem);
}

private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
    if (ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
        return;

    ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
    Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(DelayedBringIntoView));
}

private void DelayedBringIntoView()
{
    var item = ItemContainerGenerator.ContainerFromItem(SelectedItem) as ListBoxItem;
    if (item != null)
        item.BringIntoView();
}

更新:

  • 仅在 IsGroupingtrue 时使用 ItemContainerGenerator 方法,否则继续使用默认的 ScrollIntoView 方法。
  • 检查 ItemContainerGenerator 是否准备就绪,如果是,则分派操作,否则监听 ItemContainerGenerator 状态的更改。这很重要,因为如果它准备就绪,则不会触发 StatusChanged 事件。

你应该将你的答案更改为正确的,而不是上面的那个。 - Valentein
@Valentein:我已经更改了标记答案。然而,正如crazyarabian的建议帮助我诊断问题,如果您使用了我的最终解决方案,那么为两个答案投票是很好的。 - Dennis
使用.NET 4.5.1和MVVM,您可以使用行为来实现这一点。该行为在两种情况下都能正常工作,因为它已经延迟触发。 - Kelly

3
  1. 默认的VirtualizingStackPanel不支持虚拟化分组集合视图。当分组集合在ItemsControl中呈现时,每个组作为整体是一个项目,而不是集合中的每个项目,这导致滚动到每个组标题时出现“抖动”,而不是每个项目。

  2. 您可能需要自己编写VirtualizingStackPanel或ItemContainerGenerator,以便跟踪显示在组中的容器。听起来很荒谬,但WPF中带有分组的默认虚拟化至少是不足的。


这正是我所想的,但我希望不必这样做,因为编写虚拟化面板可能会很棘手。由于每个组作为整体是一个项,这就是为什么当传递“SelectedItem”时,“ItemContainerGenerator”总是返回null的原因吗? - Dennis
1
我相信是的。你应该查看Bea Stollnitz的博客,她在分组和虚拟化方面有很多好的文章: http://www.beacosta.com/ - sellmeadog
谢谢。我已经阅读了Bea Stollnitz在WPF和CollectionViewSource以及分组方面的几篇文章 - 实际上,我在我的问题中使用了她的分组示例。 - Dennis
更新:我已经找到了一个解决方案,并将其发布为答案。 - Dennis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接