ItemContainerGenerator.ContainerFromItem()返回null?

52

我遇到了一些奇怪的行为,似乎无法解决。当我迭代我的ListBox.ItemsSource属性中的项目时,我无法获取容器?我期望看到返回一个ListBoxItem,但只得到null。

有什么想法吗?

这是我使用的代码:

this.lstResults.ItemsSource.ForEach(t =>
    {
        ListBoxItem lbi = this.lstResults.ItemContainerGenerator.ContainerFromItem(t) as ListBoxItem;

        if (lbi != null)
        {
            this.AddToolTip(lbi);
        }
    });

ItemsSource目前设置为字典,并包含许多键值对。


你不能只是遍历Items,它将是一个只读集合(但其内容不是只读的)吗? - Ed Bayiates
我也尝试了。使用 .ContainerFromIndex() 也返回 null。 - Sonny Boy
请查看以下链接以获取答案:点击这里 - itzmebibin
以下链接包含了它的答案。点击这里 - itzmebibin
10个回答

61

我在StackOverflow上找到了一个对我的情况更有效的解决方案:

获取DataGrid中行

在调用ContainerFromItem或ContainerFromIndex之前,通过添加UpdateLayout和ScrollIntoView调用,可以使DataGrid的那一部分得以实现,这使得它能够为ContainerFromItem/ContainerFromIndex返回一个值:

dataGrid.UpdateLayout();
dataGrid.ScrollIntoView(dataGrid.Items[index]);
var row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromIndex(index);

如果你不想让DataGrid中当前位置改变,那么这可能不是一个好的解决方案,但如果可以接受,它可以在不关闭虚拟化的情况下工作。


1
谢谢,这解决了我的问题。我试图将焦点设置到一个尚不可用的项目上。UpdateLayout()非常好用。 - Tejo
1
我也在做同样的事情,但是在109个项目后它返回null :( - Yawar
哦,我的天啊...关键是UpdateLayout。行了,谢谢! - empax

24

最终解决了问题... 通过在我的XAML中添加VirtualizingStackPanel.IsVirtualizing="False",现在一切都像预期的那样工作。

不过,我错过了虚拟化的所有性能优势,所以我将我的加载路由更改为异步,并在我的列表框中添加了一个“旋转器”来进行加载...


PS - 感谢 H.B. 的提示。 你对虚拟化的评论引导了我走上了这条路。 - Sonny Boy
2
正如你所说,关闭虚拟化不建议,因为会导致性能下降。我曾经有几千行长数据,在关闭虚拟化后,内存消耗超过了1GB。令人害怕。 - Jānis Gruzis
这解决了我的空值问题,但像之前的评论者一样,它使之前瞬间加载2000行数据表的速度变得非常缓慢。 - Charles Clayton

23
object viewItem = list.ItemContainerGenerator.ContainerFromItem(item);
if (viewItem == null)
{
    list.UpdateLayout();
    viewItem = list.ItemContainerGenerator.ContainerFromItem(item);
    Debug.Assert(viewItem != null, "list.ItemContainerGenerator.ContainerFromItem(item) is null, even after UpdateLayout");
}

2
这对我的情况非常有效!你救了我的一天。;) 在我的情况下,这个问题发生在使用CustomSort之后,而Misa的解决方案解决了这个问题。 - Aki24x
你能解释一下这个答案吗?而不是只粘贴代码?什么是列表? - Stealth Rabbi
@Stealth Rabbi:一个ListBox或ListView。否则你需要一个项容器来做什么? - Gábor
7
@Gabor,这个答案没有解释任何内容,只是粘贴了代码。它还开始使用名为itemlist的变量,而没有说明它们是什么。 - Stealth Rabbi
2
我认为这没有任何问题。问题涉及到列表框及其项目。因此,在这种情况下,两个变量都应该是不言自明的。 - Gábor
哇,这太明显了。谢谢你指出来 xD - Leon Bohmann

8
通过调试器逐步执行代码,看看是否实际上没有返回任何内容,或者as转换只是错误的,因此将其转换为null(您可以使用普通转换来获取正确的异常)。
经常出现的一个问题是,当ItemsControl为大多数项目进行虚拟化时,任何时候都不会存在容器。
此外,我不建议直接处理项目容器,而是绑定属性并订阅事件(通过ItemsControl.ItemContainerStyle)。

6

使用此订阅:

TheListBox.ItemContainerGenerator.StatusChanged += (sender, e) =>
{
  TheListBox.Dispatcher.Invoke(() =>
  {
     var TheOne = TheListBox.ItemContainerGenerator.ContainerFromIndex(0);
     if (TheOne != null)
       // Use The One
  });
};

你为什么也要使用调度程序? - MiiChiel
这应该是正确的答案——它是唯一直接回答问题的答案。 - ReflexiveCode

4
我有点晚参加派对,但这里提供另一种解决方案,在我的情况下是无敌的。在尝试了许多解决方案后,建议将IsExpandedIsSelected添加到基础对象中,并在TreeViewItem样式中绑定它们,虽然在某些情况下这个方法大多数情况下可行,但仍然会失败...注意:我的目标是编写一个迷你/自定义资源管理器视图,在右侧窗格中单击文件夹时,它会在TreeView上被选中,就像在资源管理器中一样。
private void ListViewItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
    var item = sender as ListViewItem;
    var node = item?.Content as DirectoryNode;
    if (node == null) return;

    var nodes = (IEnumerable<DirectoryNode>)TreeView.ItemsSource;
    if (nodes == null) return;

    var queue = new Stack<Node>();
    queue.Push(node);
    var parent = node.Parent;
    while (parent != null)
    {
        queue.Push(parent);
        parent = parent.Parent;
    }

    var generator = TreeView.ItemContainerGenerator;
    while (queue.Count > 0)
    {
        var dequeue = queue.Pop();
        TreeView.UpdateLayout();
        var treeViewItem = (TreeViewItem)generator.ContainerFromItem(dequeue);
        if (queue.Count > 0) treeViewItem.IsExpanded = true;
        else treeViewItem.IsSelected = true;
        generator = treeViewItem.ItemContainerGenerator;
    }
}

在这里使用了多个技巧:

  • 使用栈从上到下展开每个项目
  • 确保使用当前级别的生成器来查找该项目(非常重要)
  • 顶层项目的生成器永远不会返回null

到目前为止,它运行得非常好,

  • 无需向类型添加新属性
  • 完全不需要禁用虚拟化

运作良好。也许更好的做法是将这个问题迁移到一个关于TreeView的问题(可能是一个新的问题),因为这个问题是关于ListBox的,以便让这个令人惊叹的答案更加可见。 - Lauraducky
4
有时候它能很好地工作,但是出现异常情况。由于某种无法解释的原因,即使在调试生成器并查看项时可以看到节点尝试选中的情况下,treeViewItem 有可能为空。它会遍历堆栈,但是 ContainerFromItem() 有时会返回空值,我不知道为什么。有时候即使容器不可见,它也能正常工作。 - Mgamerz

3
最可能是与虚拟化相关的问题,因此仅为当前可见项生成ListBoxItem容器(请参阅https://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizingstackpanel(v=vs.110).aspx#Anchor_9)。
如果您正在使用ListBox,我建议改用ListView - 它继承自ListBox并支持ScrollIntoView()方法,您可以利用该方法来控制虚拟化;
targetListView.ScrollIntoView(itemVM);
DoEvents();
ListViewItem itemContainer = targetListView.ItemContainerGenerator.ContainerFromItem(itemVM) as ListViewItem;

上面的示例还使用了在此处更详细解释的DoEvents()静态方法; WPF如何等待绑定更新发生后再处理更多代码?

ListBoxListView控件之间还有一些其他细微差别(ListBox和ListView之间的区别是什么),但这不应该对您的使用情况产生实质影响。


DoEvents()和ScrollIntoView都没有帮助。我在.NET 4.7.2上测试了ListView。控件只是“白色”,但当你滚动项目时,它们会出现,仍然有一些尚未生成容器的项目。我对愚蠢的微软“优化”感到厌倦!... - Vincent

3
虽然从XAML禁用虚拟化是可行的,但我认为最好从使用ContainerFromItem的.cs文件中禁用它。
 VirtualizingStackPanel.SetIsVirtualizing(listBox, false);

这样做可以减少XAML和代码之间的耦合度,从而避免因触碰XAML而破坏代码的风险。


你需要在什么时候做这个?我正在使用 DataGrid。当我在 XAML 中设置 IsVirtualising(false) 时,性能会急剧下降,而当我在 GUI 完全显示后才这样做时,似乎已经太晚了。 - Dominique

2

将VirtualizingStackPanel.IsVirtualizing="False"设置为“True”可以避免控件模糊。请参见下面的实现方法,这有助于我避免同样的问题。

始终将应用程序的VirtualizingStackPanel.IsVirtualizing设置为“True”。详细信息请参见链接

/// <summary>
/// Recursively search for an item in this subtree.
/// </summary>
/// <param name="container">
/// The parent ItemsControl. This can be a TreeView or a TreeViewItem.
/// </param>
/// <param name="item">
/// The item to search for.
/// </param>
/// <returns>
/// The TreeViewItem that contains the specified item.
/// </returns>
private TreeViewItem GetTreeViewItem(ItemsControl container, object item)
{
    if (container != null)
    {
        if (container.DataContext == item)
        {
            return container as TreeViewItem;
        }

        // Expand the current container
        if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)
        {
            container.SetValue(TreeViewItem.IsExpandedProperty, true);
        }

        // Try to generate the ItemsPresenter and the ItemsPanel.
        // by calling ApplyTemplate.  Note that in the 
        // virtualizing case even if the item is marked 
        // expanded we still need to do this step in order to 
        // regenerate the visuals because they may have been virtualized away.

        container.ApplyTemplate();
        ItemsPresenter itemsPresenter = 
            (ItemsPresenter)container.Template.FindName("ItemsHost", container);
        if (itemsPresenter != null)
        {
            itemsPresenter.ApplyTemplate();
        }
        else
        {
            // The Tree template has not named the ItemsPresenter, 
            // so walk the descendents and find the child.
            itemsPresenter = FindVisualChild<ItemsPresenter>(container);
            if (itemsPresenter == null)
            {
                container.UpdateLayout();

                itemsPresenter = FindVisualChild<ItemsPresenter>(container);
            }
        }

        Panel itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);


        // Ensure that the generator for this panel has been created.
        UIElementCollection children = itemsHostPanel.Children; 

        MyVirtualizingStackPanel virtualizingPanel = 
            itemsHostPanel as MyVirtualizingStackPanel;

        for (int i = 0, count = container.Items.Count; i < count; i++)
        {
            TreeViewItem subContainer;
            if (virtualizingPanel != null)
            {
                // Bring the item into view so 
                // that the container will be generated.
                virtualizingPanel.BringIntoView(i);

                subContainer = 
                    (TreeViewItem)container.ItemContainerGenerator.
                    ContainerFromIndex(i);
            }
            else
            {
                subContainer = 
                    (TreeViewItem)container.ItemContainerGenerator.
                    ContainerFromIndex(i);

                // Bring the item into view to maintain the 
                // same behavior as with a virtualizing panel.
                subContainer.BringIntoView();
            }

            if (subContainer != null)
            {
                // Search the next level for the object.
                TreeViewItem resultContainer = GetTreeViewItem(subContainer, item);
                if (resultContainer != null)
                {
                    return resultContainer;
                }
                else
                {
                    // The object is not under this TreeViewItem
                    // so collapse it.
                    subContainer.IsExpanded = false;
                }
            }
        }
    }

    return null;
}

1

如果你仍然遇到这个问题,我通过忽略第一个选择更改事件并使用线程来重复调用来解决了这个问题。这是我最终做的:

private int _hackyfix = 0;
    private void OnMediaSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        //HACKYFIX:Hacky workaround for an api issue
        //Microsoft's api for getting item controls for the flipview item fail on the very first media selection change for some reason.  Basically we ignore the
        //first media selection changed event but spawn a thread to redo the ignored selection changed, hopefully allowing time for whatever is going on
        //with the api to get things sorted out so we can call the "ContainerFromItem" function and actually get the control we need I ignore the event twice just in case but I think you can get away with ignoring only the first one.
        if (_hackyfix == 0 || _hackyfix == 1)
        {
            _hackyfix++;
            Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            OnMediaSelectionChanged(sender, e);
        });
        }
        //END OF HACKY FIX//Actual code you need to run goes here}

编辑于2014年10月29日:实际上,您甚至不需要线程调度程序代码。您可以将所需内容设置为 null 以触发第一个选择更改事件,然后退出该事件,以便未来的事件按预期工作。

        private int _hackyfix = 0;
    private void OnMediaSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        //HACKYFIX: Daniel note:  Very hacky workaround for an api issue
        //Microsoft's api for getting item controls for the flipview item fail on the very first media selection change for some reason.  Basically we ignore the
        //first media selection changed event but spawn a thread to redo the ignored selection changed, hopefully allowing time for whatever is going on
        //with the api to get things sorted out so we can call the "ContainerFromItem" function and actually get the control we need
        if (_hackyfix == 0)
        {
            _hackyfix++;
            /*
            Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            OnMediaSelectionChanged(sender, e);
        });*/
            return;
        }
        //END OF HACKY FIX
        //Your selection_changed code here
        }

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