Treeview ContainerFromItem总是返回null

本文关键字:返回 null ContainerFromItem Treeview | 更新日期: 2023-09-27 18:02:08

我已经阅读了一些关于这个主题的线程,但找不到任何可以做我想做的事情。我有一个绑定到一组分层对象的树视图。这些物体中的每一个都代表地图上的一个图标。当用户单击地图上的一个图标时,我希望在树视图中选择项目,关注它,并将其滚动到视图中。map对象有一个绑定到treeview的对象列表。在这个例子中,Thing是绑定到树的对象类型。

public void ScrollIntoView(Thing t)
{
  if (t != null)
  {
    t.IsSelected = true;
    t.IsExpanded = true;
    TreeViewItem container = (TreeViewItem)(masterTreeView
      .ItemContainerGenerator.ContainerFromItem(t));
    if (container != null)
    {
      container.Focus();
      LogicalTreeHelper.BringIntoView(container);
    }
  }
}

到目前为止,无论我尝试什么,容器总是空的。什么好主意吗?

Treeview ContainerFromItem总是返回null

项目实际上是masterTreeView的子项目吗?

这可能实际上是相当困难的,因为TreeViewItemsItemsControls与他们自己的ItemContainerGenerator,这意味着你应该只能从直接父的ItemContainerGenerator而不是从根获得容器。

一些递归函数,它首先在层次结构中向上到根,然后在ui层反转这条路径,总是得到项目的容器,但你的数据项需要引用它们的逻辑父数据对象。

问题是,每个TreeViewItem本身就是一个ItemsControl,所以他们每个管理自己的容器为他们的孩子。

你有三个选择:

  • 禁用项目:<TreeView VirtualizingStackPanel.IsVirtualizing="False">的虚拟化,但这可能对性能有影响

  • 管理每个项目的ItemContainerGenerator状态(提供一些代码作为示例)。很复杂。

  • 你添加一个层次视图模型到你的层次结构,并为每个节点级别实现一个IsExpanded属性。最好的解决方案。

禁用虚拟化:

<TreeView VirtualizingStackPanel.IsVirtualizing="False">

好运……

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Threading;
using HQ.Util.General;
namespace HQ.Util.Wpf.WpfUtil
{
    public static class TreeViewExtensions
    {
        // ******************************************************************
        public delegate void OnTreeViewVisible(TreeViewItem tvi);
        public delegate void OnItemExpanded(TreeViewItem tvi, object item);
        public delegate void OnAllItemExpanded();
        // ******************************************************************
        private static void SetItemHierarchyVisible(ItemContainerGenerator icg, IList listOfRootToNodeItemPath, OnTreeViewVisible onTreeViewVisible = null)
        {
            Debug.Assert(icg != null);
            if (icg != null)
            {
                if (listOfRootToNodeItemPath.Count == 0) // nothing to do
                    return;
                TreeViewItem tvi = icg.ContainerFromItem(listOfRootToNodeItemPath[0]) as TreeViewItem;
                if (tvi != null) // Due to threading, always better to verify
                {
                    listOfRootToNodeItemPath.RemoveAt(0);
                    if (listOfRootToNodeItemPath.Count == 0)
                    {
                        if (onTreeViewVisible != null)
                            onTreeViewVisible(tvi);
                    }
                    else
                    {
                        if (!tvi.IsExpanded)
                            tvi.IsExpanded = true;
                        SetItemHierarchyVisible(tvi.ItemContainerGenerator, listOfRootToNodeItemPath, onTreeViewVisible);
                    }
                }
                else
                {
                    ActionHolder actionHolder = new ActionHolder();
                    EventHandler itemCreated = delegate(object sender, EventArgs eventArgs)
                        {
                            var icgSender = sender as ItemContainerGenerator;
                            tvi = icgSender.ContainerFromItem(listOfRootToNodeItemPath[0]) as TreeViewItem;
                            if (tvi != null) // Due to threading, it is always better to verify
                            {
                                SetItemHierarchyVisible(icg, listOfRootToNodeItemPath, onTreeViewVisible);
                                actionHolder.Execute();
                            }
                        };
                    actionHolder.Action = new Action(() => icg.StatusChanged -= itemCreated);
                    icg.StatusChanged += itemCreated;
                    return;
                }
            }
        }
        // ******************************************************************
        /// <summary>
        /// You cannot rely on this method to be synchronous. If you have any action that depend on the TreeViewItem 
        /// (last item of collectionOfRootToNodePath) to be visible, you should set it in the 'onTreeViewItemVisible' method.
        /// This method should work for Virtualized and non virtualized tree.
        /// The difference with ExpandItem is that this one open up the tree up to the target but will not expand the target itself,
        /// while ExpandItem expand the target itself.
        /// </summary>
        /// <param name="treeView">TreeView where  an item has to be set visible</param>
        /// <param name="listOfRootToNodePath">Any collectionic List. The collection should have every objet of the path to the targeted item from the root
        /// to the target. For example for an apple tree: AppleTree (index 0), Branch4, SubBranch3, Leaf2 (index 3)</param>
        /// <param name="onTreeViewVisible">Optionnal</param>
        public static void SetItemHierarchyVisible(this TreeView treeView, IEnumerable<object> listOfRootToNodePath, OnTreeViewVisible onTreeViewVisible = null)
        {
            ItemContainerGenerator icg = treeView.ItemContainerGenerator;
            if (icg == null)
                return; // Is tree loaded and initialized ???
            SetItemHierarchyVisible(icg, new List<object>(listOfRootToNodePath), onTreeViewVisible);
        }
        // ******************************************************************
        private static void ExpandItem(ItemContainerGenerator icg, IList listOfRootToNodePath, OnTreeViewVisible onTreeViewVisible = null)
        {
            Debug.Assert(icg != null);
            if (icg != null)
            {
                if (listOfRootToNodePath.Count == 0) // nothing to do
                    return;
                TreeViewItem tvi = icg.ContainerFromItem(listOfRootToNodePath[0]) as TreeViewItem;
                if (tvi != null) // Due to threading, always better to verify
                {
                    listOfRootToNodePath.RemoveAt(0);
                    if (!tvi.IsExpanded)
                        tvi.IsExpanded = true;
                    if (listOfRootToNodePath.Count == 0)
                    {
                        if (onTreeViewVisible != null)
                            onTreeViewVisible(tvi);
                    }
                    else
                    {
                        SetItemHierarchyVisible(tvi.ItemContainerGenerator, listOfRootToNodePath, onTreeViewVisible);
                    }
                }
                else
                {
                    ActionHolder actionHolder = new ActionHolder();
                    EventHandler itemCreated = delegate(object sender, EventArgs eventArgs)
                        {
                            var icgSender = sender as ItemContainerGenerator;
                            tvi = icgSender.ContainerFromItem(listOfRootToNodePath[0]) as TreeViewItem;
                            if (tvi != null) // Due to threading, it is always better to verify
                            {
                                SetItemHierarchyVisible(icg, listOfRootToNodePath, onTreeViewVisible);
                                actionHolder.Execute();
                            }
                        };
                    actionHolder.Action = new Action(() => icg.StatusChanged -= itemCreated);
                    icg.StatusChanged += itemCreated;
                    return;
                }
            }
        }
        // ******************************************************************
        /// <summary>
        /// You cannot rely on this method to be synchronous. If you have any action that depend on the TreeViewItem 
        /// (last item of collectionOfRootToNodePath) to be visible, you should set it in the 'onTreeViewItemVisible' method.
        /// This method should work for Virtualized and non virtualized tree.
        /// The difference with SetItemHierarchyVisible is that this one open the target while SetItemHierarchyVisible does not try to expand the target.
        /// (SetItemHierarchyVisible just ensure the target will be visible)
        /// </summary>
        /// <param name="treeView">TreeView where  an item has to be set visible</param>
        /// <param name="listOfRootToNodePath">The collection should have every objet of the path, from the root to the targeted item.
        /// For example for an apple tree: AppleTree (index 0), Branch4, SubBranch3, Leaf2</param>
        /// <param name="onTreeViewVisible">Optionnal</param>
        public static void ExpandItem(this TreeView treeView, IEnumerable<object> listOfRootToNodePath, OnTreeViewVisible onTreeViewVisible = null)
        {
            ItemContainerGenerator icg = treeView.ItemContainerGenerator;
            if (icg == null)
                return; // Is tree loaded and initialized ???
            ExpandItem(icg, new List<object>(listOfRootToNodePath), onTreeViewVisible);
        }
        // ******************************************************************
        private static void ExpandSubWithContainersGenerated(ItemsControl ic, Action<TreeViewItem, object> actionItemExpanded, ReferenceCounterTracker referenceCounterTracker)
        {
            ItemContainerGenerator icg = ic.ItemContainerGenerator;
            foreach (object item in ic.Items)
            {
                var tvi = icg.ContainerFromItem(item) as TreeViewItem;
                actionItemExpanded(tvi, item);
                tvi.IsExpanded = true;
                ExpandSubContainers(tvi, actionItemExpanded, referenceCounterTracker);
            }
        }
        // ******************************************************************
        /// <summary>
        /// Expand any ItemsControl (TreeView, TreeViewItem, ListBox, ComboBox, ...) and their childs if any (TreeView)
        /// </summary>
        /// <param name="ic"></param>
        /// <param name="actionItemExpanded"></param>
        /// <param name="referenceCounterTracker"></param>
        public static void ExpandSubContainers(ItemsControl ic, Action<TreeViewItem, object> actionItemExpanded, ReferenceCounterTracker referenceCounterTracker)
        {
            ItemContainerGenerator icg = ic.ItemContainerGenerator;
            {
                if (icg.Status == GeneratorStatus.ContainersGenerated)
                {
                    ExpandSubWithContainersGenerated(ic, actionItemExpanded, referenceCounterTracker);
                }
                else if (icg.Status == GeneratorStatus.NotStarted)
                {
                    ActionHolder actionHolder = new ActionHolder();
                    EventHandler itemCreated = delegate(object sender, EventArgs eventArgs)
                        {
                            var icgSender = sender as ItemContainerGenerator;
                            if (icgSender.Status == GeneratorStatus.ContainersGenerated)
                            {
                                ExpandSubWithContainersGenerated(ic, actionItemExpanded, referenceCounterTracker);
                                // Never use the following method in BeginInvoke due to ICG recycling. The same icg could be 
                                // used and will keep more than one subscribers which is far from being intended
                                //  ic.Dispatcher.BeginInvoke(actionHolder.Action, DispatcherPriority.Background);
                                // Very important to unsubscribe as soon we've done due to ICG recycling.
                                actionHolder.Execute();
                                referenceCounterTracker.ReleaseRef();
                            }
                        };
                    referenceCounterTracker.AddRef();
                    actionHolder.Action = new Action(() => icg.StatusChanged -= itemCreated);
                    icg.StatusChanged += itemCreated;
                    // Next block is only intended to protect against any race condition (I don't know if it is possible ? How Microsoft implemented it)
                    // I mean the status changed before I subscribe to StatusChanged but after I made the check about its state.
                    if (icg.Status == GeneratorStatus.ContainersGenerated)
                    {
                        ExpandSubWithContainersGenerated(ic, actionItemExpanded, referenceCounterTracker);
                    }
                }
            }
        }
        // ******************************************************************
        /// <summary>
        /// This method is asynchronous.
        /// Expand all items and subs recursively if any. Does support virtualization (item recycling).
        /// But honestly, make you a favor, make your life easier en create a model view around your hierarchy with
        /// a IsExpanded property for each node level and bind it to each TreeView node level.
        /// </summary>
        /// <param name="treeView"></param>
        /// <param name="actionItemExpanded"></param>
        /// <param name="actionAllItemExpanded"></param>
        public static void ExpandAll(this TreeView treeView, Action<TreeViewItem, object> actionItemExpanded = null, Action actionAllItemExpanded = null)
        {
            var referenceCounterTracker = new ReferenceCounterTracker(actionAllItemExpanded);
            referenceCounterTracker.AddRef();
            treeView.Dispatcher.BeginInvoke(new Action(() => ExpandSubContainers(treeView, actionItemExpanded, referenceCounterTracker)), DispatcherPriority.Background);
            referenceCounterTracker.ReleaseRef();
        }
        // ******************************************************************
    }
}

using System;
using System.Threading;
namespace HQ.Util.General
{
    public delegate void CountToZeroAction();
    public class ReferenceCounterTracker
    {
        private Action _actionOnCountReachZero = null;
        private int _count = 0;
        public ReferenceCounterTracker(Action actionOnCountReachZero)
        {
            _actionOnCountReachZero = actionOnCountReachZero;
        }
        public void AddRef()
        {
            Interlocked.Increment(ref _count);
        }
        
        public void ReleaseRef()
        {
            int count = Interlocked.Decrement(ref _count);
            if (count == 0)
            {
                if (_actionOnCountReachZero != null)
                {
                    _actionOnCountReachZero();
                }
            }
        }
    }
}