Treeview ContainerFromItem always returns null
Asked Answered
D

3

10

I've read a few threads on this subject but couldn't find anything to do what I'm trying to do. I have a treeview that is bound to a hierarchical set of objects. Each of these objects represents an icon on a map. When the user clicks one of the icons on the map, I want to select the item in the tree view, focus on it, and scroll it into view. The map object has a list of the objects that are bound to the treeview. In the example, Thing is the type of object bound to the tree.

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);
    }
  }
}

So far, no matter what I've tried, container is always null. Any ideas?

Diahann answered 15/7, 2011 at 19:40 Comment(0)
W
11

Is the item actually a child of the masterTreeView?

This might actually be quite difficult since TreeViewItems are ItemsControls with their own ItemContainerGenerator which means you should only be able to get the container from the immediate parent's ItemContainerGenerator and not from the root.

Some recursive function which first goes up the hierarchy to the root and then reverses this route on the ui level always getting the container of the items might work out but your data-items need a reference to their logical parent data object.

Wheresoever answered 15/7, 2011 at 20:56 Comment(5)
Nope, only some of them are children of the treeview. The treeview is many levels deep. Each Thing, has a reference to it's parent thing, but not to it's TreeViewItem Container. That is what I'm trying to figure out. I can easily climb up and down the tree from within the bound objects, I'm just not sure how to get the TreeViewItem container for a specific object.Diahann
Push each object on a stack till you reach a root item, you know the root's corresponding ItemContainerGenerator, so you can use that to get the container for the next object on your stack, then you can use this TreeViewItem's ItemContainerGenerator to get the container for yet the next object on the stack and so on until your stack is empty and you have the container for your initial thing.Wheresoever
Is there a snippet of how to get the actual container?Viera
@JobaDiniz: No.Wheresoever
If anyone wants an example of how this could be implemented, this answer is really good.Highhanded
D
1

You have 3 choices:

  • You disable the virtualization of items: <TreeView VirtualizingStackPanel.IsVirtualizing="False">, but this could have impact on performance

  • You manage ItemContainerGenerator status for each item (some code provided as examples). Pretty complex.

  • You add a hierarchical view model to your hierarchy and implement a IsExpanded property to it for each node level. The best solution.

Disable virtualization:

<TreeView VirtualizingStackPanel.IsVirtualizing="False">

Good Luck...

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();
        }

        // ******************************************************************
    }
}

And

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();
                }
            }
        }
    }
}
Decare answered 19/11, 2012 at 21:38 Comment(0)
A
0

The issue is that each TreeViewItem is itself an ItemsControl so they each manage their own containers for their children.

Armentrout answered 13/4, 2016 at 11:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.