43

我有一些奇怪的行为,我似乎无法解决。当我遍历 ListBox.ItemsSource 属性中的项目时,我似乎无法获取容器?我期待看到返回的 ListBoxItem,但我只得到空值。

有任何想法吗?

这是我正在使用的代码:

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

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

ItemsSource 当前设置为 Dictionary 并且确实包含许多 KVP。

4

10 回答 10

56

在这个 StackOverflow 问题中,我发现了一些更适合我的情况:

获取数据网格中的行

通过在调用 ContainerFromItem 或 ContainerFromIndex 之前放入 UpdateLayout 和 ScrollIntoView 调用,您可以实现 DataGrid 的该部分,这使得它可以返回 ContainerFromItem/ContainerFromIndex 的值:

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

如果您不希望 DataGrid 中的当前位置发生变化,这对您来说可能不是一个好的解决方案,但如果没问题,它无需关闭虚拟化即可工作。

于 2013-01-27T19:19:29.390 回答
21
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");
}
于 2016-02-01T23:10:35.287 回答
20

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

不利的一面是,我错过了虚拟化的所有性能优势,因此我将负载路由更改为异步并在加载时将“微调器”添加到我的列表框中...

于 2011-07-18T16:43:00.923 回答
7

使用调试器单步执行代码,看看是否实际上没有返回任何内容,或者as-cast 是否只是错误的并因此将其转换为null(您可以只使用正常的强制转换来获得正确的异常)。

经常发生的一个问题是,当ItemsControl对大多数项目进行虚拟化时,任何时间点都不存在容器。

此外,我不建议直接处理项目容器,而是绑定属性并订阅事件(通过ItemsControl.ItemContainerStyle)。

于 2011-07-17T02:49:01.757 回答
5

使用此订阅:

TheListBox.ItemContainerGenerator.StatusChanged += (sender, e) =>
{
  TheListBox.Dispatcher.Invoke(() =>
  {
     var TheOne = TheListBox.ItemContainerGenerator.ContainerFromIndex(0);
     if (TheOne != null)
       // Use The One
  });
};
于 2014-04-26T13:18:44.200 回答
4

我参加聚会有点晚了,但在我的情况下,这是另一个防故障的解决方案,

在尝试了许多建议添加IsExpanded和添加IsSelected到底层对象并以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

到目前为止效果很好,

  • 无需使用新属性污染您的类型
  • 根本不需要禁用虚拟化
于 2016-01-05T20:21:17.840 回答
3

虽然从 XAML 禁用虚拟化有效,但我认为最好从使用的 .cs 文件中禁用它ContainerFromItem

 VirtualizingStackPanel.SetIsVirtualizing(listBox, false);

这样,您可以减少 XAML 和代码之间的耦合;这样您就可以避免有人通过触摸 XAML 来破坏代码的风险。

于 2013-09-02T13:09:18.390 回答
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 之间的区别是什么) - 这应该不会从本质上影响您的用例。

于 2016-01-13T17:07:06.837 回答
2

VirtualizingStackPanel.IsVirtualizing="False" 使控制变得模糊。请参阅下面的实现。这有助于我避免同样的问题。始终设置您的应用程序 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;
}
于 2013-07-23T08:34:45.713 回答
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}

编辑 10/29/2014:您实际上甚至不需要线程调度程序代码。您可以将所需的任何内容设置为 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
        }
于 2014-10-28T14:38:40.400 回答