ItemContainerGenerator.ContainerFromItem如何在分组列表中运作?

31

我有一个ListBox,最近它一直显示一个平面列表。 我之前用myList.ItemContainerGenerator.ConainerFromItem(thing)检索了列表中承载“thing”的ListBoxItem。

本周,我稍微修改了ListBox,使其绑定的CollectionViewSource启用分组。 现在,ListBox中的项目被分组在漂亮的标题下面。

但是,自从这样做以来,ItemContainerGenerator.ContainerFromItem已经停止工作 - 即使对于我知道在ListBox中的项目,它也返回null。 甚至是:当ListBox中填充了许多项目时,ContainerFromIndex(0)也会返回null!

如何从显示分组项的ListBox中检索ListBoxItem?

编辑:这是一个简化示例的XAML和代码后台。 这引发了NullReferenceException,因为ContainerFromIndex(1)返回null,即使列表中有四个项目。

XAML:

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
    Title="Window1">

    <Window.Resources>
        <XmlDataProvider x:Key="myTasks" XPath="Tasks/Task">
            <x:XData>
                <Tasks xmlns="">
                    <Task Name="Groceries" Type="Home"/>
                    <Task Name="Cleaning" Type="Home"/>
                    <Task Name="Coding" Type="Work"/>
                    <Task Name="Meetings" Type="Work"/>
                </Tasks>
            </x:XData>
        </XmlDataProvider>

        <CollectionViewSource x:Key="mySortedTasks" Source="{StaticResource myTasks}">
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="@Type" />
                <scm:SortDescription PropertyName="@Name" />
            </CollectionViewSource.SortDescriptions>

            <CollectionViewSource.GroupDescriptions>
                <PropertyGroupDescription PropertyName="@Type" />
            </CollectionViewSource.GroupDescriptions>
        </CollectionViewSource>
    </Window.Resources>

    <ListBox 
        x:Name="listBox1" 
        ItemsSource="{Binding Source={StaticResource mySortedTasks}}" 
        DisplayMemberPath="@Name"
        >
        <ListBox.GroupStyle>
            <GroupStyle>
                <GroupStyle.HeaderTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Name}"/>
                    </DataTemplate>
                </GroupStyle.HeaderTemplate>
            </GroupStyle>
        </ListBox.GroupStyle>
    </ListBox>
</Window>

计算机科学:

public Window1()
{
    InitializeComponent();
    listBox1.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}

void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
    if (listBox1.ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
    {
        listBox1.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;

        var i = listBox1.ItemContainerGenerator.ContainerFromIndex(1) as ListBoxItem;

        // select and keyboard-focus the second item
        i.IsSelected = true;
        i.Focus();
    }
}

你对容器在做什么?你能否详细说明一下之前的代码是如何工作的?有几种方法可以获取容器...这取决于你想要做什么。 - rudigrobler
3个回答

41

在可以通过 ContainerFromElement 访问 ItemContainers 之前,您需要侦听并响应 ItemsGenerator.StatusChanged 事件,并等待生成 ItemContainers。


进一步搜索,我在 MSDN 论坛的帖子 中找到了一个有同样问题的人。这似乎是 WPF 中的一个 bug,当设置 GroupStyle 时会出现此问题。解决方案是将访问 ItemGenerator 推迟到呈现过程之后。以下是针对您问题的代码。我尝试过这个方法,它有效:

    void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
    {
        if (listBox1.ItemContainerGenerator.Status
            == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
        {
            listBox1.ItemContainerGenerator.StatusChanged
                -= ItemContainerGenerator_StatusChanged;
            Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input,
                new Action(DelayedAction));
        }
    }

    void DelayedAction()
    {
        var i = listBox1.ItemContainerGenerator.ContainerFromIndex(1) as ListBoxItem;

        // select and keyboard-focus the second item
        i.IsSelected = true;
        i.Focus();
    }

@justin:我已经移除了VoidDelegate。 - David Schmitt
1
我不相信这个问题仅限于设置了GroupStyle的情况。另请参阅:https://dev59.com/1Gs05IYBdhLWcg3wPfcR - Cheeso
@Cheeso:可能是这样。我只探索了这个特定的用例。 - David Schmitt
1
+1 感谢您的回答David,它有助于解决类似的问题。我想指出一个更改,即首先检查ItemContainerGenerator是否就绪,如果是,则调度操作,而不是等待状态改变。这解决了第一次无法工作的错误。https://dev59.com/A2s05IYBdhLWcg3wPPeB#7414200 - Dennis
关于这个问题的更新(你可能想在你的回答中包含这个评论,David)。我有另一个分组的ListBox也遇到了这个问题,在上面的代码中删除了“取消订阅事件”的行之后,我才使它正常工作。即使容器首次生成后,事件仍然会被多次触发。希望这能帮助其他人。 - Matt Hamilton
显示剩余5条评论

2
如果上述代码对您不起作用,请尝试以下方法。
public class ListBoxExtenders : DependencyObject
{
    public static readonly DependencyProperty AutoScrollToCurrentItemProperty = DependencyProperty.RegisterAttached("AutoScrollToCurrentItem", typeof(bool), typeof(ListBoxExtenders), new UIPropertyMetadata(default(bool), OnAutoScrollToCurrentItemChanged));

    public static bool GetAutoScrollToCurrentItem(DependencyObject obj)
    {
        return (bool)obj.GetValue(AutoScrollToSelectedItemProperty);
    }

    public static void SetAutoScrollToCurrentItem(DependencyObject obj, bool value)
    {
        obj.SetValue(AutoScrollToSelectedItemProperty, value);
    }

    public static void OnAutoScrollToCurrentItemChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
    {
        var listBox = s as ListBox;
        if (listBox != null)
        {
            var listBoxItems = listBox.Items;
            if (listBoxItems != null)
            {
                var newValue = (bool)e.NewValue;

                var autoScrollToCurrentItemWorker = new EventHandler((s1, e2) => OnAutoScrollToCurrentItem(listBox, listBox.Items.CurrentPosition));

                if (newValue)
                    listBoxItems.CurrentChanged += autoScrollToCurrentItemWorker;
                else
                    listBoxItems.CurrentChanged -= autoScrollToCurrentItemWorker;
            }
        }
    }

    public static void OnAutoScrollToCurrentItem(ListBox listBox, int index)
    {
        if (listBox != null && listBox.Items != null && listBox.Items.Count > index && index >= 0)
            listBox.ScrollIntoView(listBox.Items[index]);
    }

}

XAML中的用法

<ListBox IsSynchronizedWithCurrentItem="True" extenders:ListBoxExtenders.AutoScrollToCurrentItem="True" ..../>

这对我解决了问题。谢谢!现在要理解为什么... ;-) - Anthony Nichols

0
尝试从“thing”开始解析VisualTree,直到达到ListBoxItem类型。

Jobi - 在这种情况下,“thing”不是视觉元素-它是一个业务对象的实例。它是绑定到ListBox的ItemsSource的IList<Thing>的一部分。在这种情况下,我可以按照你描述的方式进行操作吗? - Matt Hamilton
明白了,感谢您的澄清。您是从代码后台的哪个部分尝试进行操作?是在任何DataTemplate点击事件中还是在其他事件处理程序中? - Jobi Joy
当页面(这是一个导航式应用程序)加载时,我想将键盘焦点设置到特定元素。因此,在Loaded事件处理程序中。 - Matt Hamilton
实际上这并不完全正确。我是在 ListBox 上的 ItemContainerGenerator.StatusChanged 事件处理程序中执行此操作的,而我是在 Loaded 事件中连接它的。 - Matt Hamilton

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