How to get TreeViewItem from HierarchicalDataTemplate item?
Asked Answered
P

12

36

I have a TreeView which uses a HierarchicalDataTemplate to bind its data.

It looks like this:

<TreeView x:Name="mainTreeList" ItemsSource="{Binding MyCollection}>
  <TreeView.Resources>
    <HierarchicalDataTemplate 
     DataType="{x:Type local:MyTreeViewItemViewModel}" 
     ItemsSource="{Binding Children}">
      <!-- code code code -->
    </HierarchicalDataTemplate>
  </TreeView.Resources>
</TreeView>

Now, from the code-behind of say the main window, I want to get the current selected TreeViewItem. However, if I use:

this.mainTreeList.SelectedItem;

The selectedItem is of type MyTreeViewItemViewModel. But I want to get the 'parent' or 'bound' TreeViewItem. I do not pass that to my TreeViewItemModel object (wouldn't even know how).

How can I do this?

Punk answered 5/3, 2009 at 22:27 Comment(5)
@romkyns: What? This question has nothing to do with keeping track of the selected item.Hochman
@romkyns: Well, you shouldn't be getting the TreeViewItem in the first place, so it does not really matter if it's hard to do or not.Hochman
@H.B. That might well be so... maybe you can help me do this without TreeViewItem?Artel
@H.B. I’ve asked the question directly now: is it wrong to access TreeViewItems?, would appreciate your input.Artel
This work for me: select-treeview-node-on-right-click-before-displaying-contextmenuVibes
M
24

From Bea Stollnitz's blog entry about this, try

TreeViewItem item = (TreeViewItem)(mainTreeList
    .ItemContainerGenerator
    .ContainerFromIndex(mainTreeList.Items.CurrentPosition));
Miletus answered 6/3, 2009 at 3:3 Comment(4)
Good enough for me, thanks. (it's a bit different though, CurrentItem returns an object and ContainerFromIndex takes an integer, so I had to use CurrentIndex or something, but oh well)Punk
Ah, I just copied-n-pasted from Bea's blog and then changed a few things. I didn't notice that error :) Still, I'll update my answer.Miletus
CurrentPosition is always 0 for me, regardless of what’s selected. The blog entry you link to is about lists, not trees - perhaps that’s why. There’s no HierarchicalDataTemplate involved in the linked blog post.Artel
It has been worked for a period of time,but now CurrentPosition always be 0.Wirer
W
32
TreeViewItem item = (TreeViewItem)(mainTreeList
    .ItemContainerGenerator
    .ContainerFromIndex(mainTreeList.Items.CurrentPosition));

DOES NOT WORK (for me) as mainTreeList.Items.CurrentPosition in a treeview using a HierarchicalDataTemplate will always be -1.

NEITHER DOES below as as mainTreeList.Items.CurrentItem in a treeview using a HierarchicalDataTemplate will always be null.

TreeViewItem item = (TreeViewItem)mainTreeList
    .ItemContainerGenerator
    .ContainerFromItem(mainTreeList.Items.CurrentItem);

INSTEAD I had to set a the last selected TreeViewItem in the routed TreeViewItem.Selected event which bubbles up to the tree view (the TreeViewItem's themselves do not exist at design time as we are using a HierarchicalDataTemplate).

The event can be captured in XAML as so:

<TreeView TreeViewItem.Selected="TreeViewItemSelected" .../> 

Then the last TreeViewItem selected can be set in the event as so:

    private void TreeViewItemSelected(object sender, RoutedEventArgs e)
    {
        TreeViewItem tvi = e.OriginalSource as TreeViewItem;

        // set the last tree view item selected variable which may be used elsewhere as there is no other way I have found to obtain the TreeViewItem container (may be null)
        this.lastSelectedTreeViewItem = tvi;

        ...
     }
Warship answered 5/7, 2010 at 4:59 Comment(8)
The only way for the CurrentItem to equal null (or CurrentIndex equaling -1) is if there are no items in the tree. Make sure there are items before trying to get the items container.Miletus
The CurrentItem is not the same as the SelectedItem. The ContainerFromItem returns null if it cannot find the container, and null can be cast to a TreeViewItem just fine without needing to use as cast. Did you even run the code?Miletus
The problem I think is you are using mainTreeList.Items which will only work for items in the first level of the treeview - each tree view node (TreeViewItem) has its own Items collection - unlike Bea's example which you quote which is using a ListBox and only has one level of items. It follows that you need to obtain reference currently selected TreeViewItem for which the only way I found to do so when using a HierarchicalDataTemplate is capturing the TreeViewItem.Selected event as in my answer.Warship
The reason CurrentItem returns null is only once a TreeViewItem is expanded are the next level of nodes instantiated. Admittedly this is not clear from the MSDN documentation.Warship
Incidentally Bea does have a solution for a TreeView too - but that is much messier than mine above as involves waiting for the UI thread to finish doing all its tasks then assuming the TreeViewItems have then been created: bea.stollnitz.com/blog/?p=59Warship
How to add the event in code: ctLayersTree.AddHandler(TreeViewItem.SelectedEvent, <your handler>)Artel
That settles it to be honest. WPF is a horrific monstrous behemoth of unitunitive mess. So many smart people around here that can't easily figure out a simple thing like getting the name of an item on a big fat visual control, and if they do find an answer, it's an event.Tori
@LelaDax it takes some getting used to, but I am glad I did, and like XAML in general now :) Note: this scenario is unusual - why are you trying to the Visual? Bindings in XAML should obviate the need.Warship
M
24

From Bea Stollnitz's blog entry about this, try

TreeViewItem item = (TreeViewItem)(mainTreeList
    .ItemContainerGenerator
    .ContainerFromIndex(mainTreeList.Items.CurrentPosition));
Miletus answered 6/3, 2009 at 3:3 Comment(4)
Good enough for me, thanks. (it's a bit different though, CurrentItem returns an object and ContainerFromIndex takes an integer, so I had to use CurrentIndex or something, but oh well)Punk
Ah, I just copied-n-pasted from Bea's blog and then changed a few things. I didn't notice that error :) Still, I'll update my answer.Miletus
CurrentPosition is always 0 for me, regardless of what’s selected. The blog entry you link to is about lists, not trees - perhaps that’s why. There’s no HierarchicalDataTemplate involved in the linked blog post.Artel
It has been worked for a period of time,but now CurrentPosition always be 0.Wirer
C
10

I ran into this same problem. I needed to get to the TreeViewItem so that I could have it be selected. I then realized that I could just add a property IsSelected to my ViewModel, which I then bound to the TreeViewItems IsSelectedProperty. This can be achieved with the ItemContainerStyle:

<TreeView>
            <TreeView.ItemContainerStyle>
                <Style TargetType="{x:Type TreeViewItem}">
                    <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
                    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
                </Style>
            </TreeView.ItemContainerStyle>
</TreeView>

Now if I want to have an item in the treeview selected, I just call IsSelected on my ViewModel class directly.

Hope it helps someone.

Cudbear answered 21/9, 2011 at 17:20 Comment(3)
In my opinion this is the best way to use treeviews. The items need to be viewmodels so you can work with them property. Otherwise you end up having to do all sorts of shenanigans in order to get what should be simple behaviour to work correctly.Intervalometer
I agree, however the question was how to get the TreeViewItem not the object it wraps.Warship
Yeah, this is certainly the way you're supposed to do it. But having a ViewModel for evvverything is quite a PITA, especially for small basic UIs.Artel
G
7
TreeViewItem item = (TreeViewItem)(mainTreeList
    .ItemContainerGenerator
    .ContainerFromIndex(mainTreeList.Items.CurrentPosition)); gives first item in the TreeView because CurrentPosition is always 0.

How about

TreeViewItem item = (TreeViewItem)(mainTreeList
    .ItemContainerGenerator
    .ContainerFromItem(mainTreeList.SelectedItem)));

This works better for me.

Gibbous answered 9/1, 2013 at 10:39 Comment(2)
You can actually simplify the last example. Just use ...ItemGenerator.ContainerFromItem(mainTreeList.SelectedItem). This also works if you're using one or more HierarchicalDataTemplate's, whereas the first example won't. I allowed myself to edit your answer. I hope that's alright with you.Antionetteantioxidant
Not work, mainTreeList.SelectedItemis null is redundant, mainTreeList.SelectedItem and TreeViewItem item is the same.Vibes
A
4

Inspired by Fëanor’s answer, I’ve attempted to make the TreeViewItem easily accessible for every data item that a TreeViewItem has been created for.

The idea is to add a TreeViewItem-typed field to the view model, also exposed via an interface, and have the TreeView automatically populate it whenever the TreeViewItem container is created.

This is done by subclassing TreeView and attaching an event to the ItemContainerGenerator, which records the TreeViewItem whenever it’s created. Gotchas include the fact that TreeViewItems are created lazily, so there might genuinely not be one available at certain times.

Since posting this answer, I've developed this further and used it for a long time in one project. No issues so far, other than the fact that this violates MVVM (but also saves you a ton of boilerplate for the simple cases). Source here.

Usage

Select the parent of the selected item and collapse it, ensuring that it’s in the view:

...
var selected = myTreeView.SelectedItem as MyItem;
selected.Parent.TreeViewItem.IsSelected = true;
selected.Parent.TreeViewItem.IsExpanded = false;
selected.Parent.TreeViewItem.BringIntoView();
...

Declarations:

<Window ...
        xmlns:tvi="clr-namespace:TreeViewItems"
        ...>
    ...
    <tvi:TreeViewWithItem x:Name="myTreeView">
        <HierarchicalDataTemplate DataType = "{x:Type src:MyItem}"
                                  ItemsSource = "{Binding Children}">
            <TextBlock Text="{Binding Path=Name}"/>
        </HierarchicalDataTemplate>
    </tvi:TreeViewWithItem>
    ...
</Window>
class MyItem : IHasTreeViewItem
{
    public string Name { get; set; }
    public ObservableCollection<MyItem> Children { get; set; }
    public MyItem Parent;
    public TreeViewItem TreeViewItem { get; set; }
    ...
}

Code

public class TreeViewWithItem : TreeView
{
    public TreeViewWithItem()
    {
        ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
    }

    private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
    {
        var generator = sender as ItemContainerGenerator;
        if (generator.Status == GeneratorStatus.ContainersGenerated)
        {
            int i = 0;
            while (true)
            {
                var container = generator.ContainerFromIndex(i);
                if (container == null)
                    break;

                var tvi = container as TreeViewItem;
                if (tvi != null)
                    tvi.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;

                var item = generator.ItemFromContainer(container) as IHasTreeViewItem;
                if (item != null)
                    item.TreeViewItem = tvi;

                i++;
            }
        }
    }
}

interface IHasTreeViewItem
{
    TreeViewItem TreeViewItem { get; set; }
}
Artel answered 26/3, 2012 at 13:33 Comment(3)
Inspecting this road has been very intresting, and formative on how wpf works (aka black magic), but the answer with the bounty led me on how to do it correctly. This is not a good way of doing it (with respect) because the items get regenerated, and u keep adding handers, also you are never going to remove those handlers, because there isn't an generator status "removed". We have routed events and we are supposed to learn and use them.Vigilant
@Vigilant I would certainly agree that my approach is hacky. TBH, for complex UIs the best bet is almost certainly to abandon the idea of accessing TreeViewItems altogether; that's just not how WPF is supposed to be used. We're supposed to be accessing our ViewModel instances instead. I don't remember why the bountied answer didn't cut it for me, but it does look less hacky than mine.Artel
Sure, another good way of doing this is using viewmodels, but it depends on what we are trying to achieve, for me the routed events worked better because i was trying to extend the treeview to achieve a multiselect treeview, but i wanted it to be less code as possible, because that could lead to maintenance and bugs, and wanted it to work independently on what model is used, i mostly did it, i have some flaws right now, exposing the selected items to be accessed from others controls ... i wish i know more :) thanks anyway!Vigilant
M
2

Try something like this:

    public bool UpdateSelectedTreeViewItem(PropertyNode dateItem, ItemsControl itemsControl)
    {
        if (itemsControl == null || itemsControl.Items == null || itemsControl.Items.Count == 0)
        {
            return false;
        }
        foreach (var item in itemsControl.Items.Cast<PropertyNode>())
        {
            var treeViewItem = itemsControl.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
            if (treeViewItem == null)
            {
                continue;
            }
            if (item == dateItem)
            {
                treeViewItem.IsSelected = true;
                return true;
            }
            if (treeViewItem.Items.Count > 0 && UpdateSelectedTreeViewItem(dateItem, treeViewItem))
            {
                return true;
            }
        }
        return false;
    }
Millstone answered 7/2, 2015 at 5:4 Comment(0)
F
2

I modified William's recursive search to a more compact version:

public TreeViewItem GetTreeViewItemFromObject(ItemContainerGenerator container, object targetObject) {
    if (container.ContainerFromItem(targetObject) is TreeViewItem target) return target;
    for (int i = 0; i < container.Items.Count; i++)
        if ((container.ContainerFromIndex(i) as TreeViewItem)?.ItemContainerGenerator is ItemContainerGenerator childContainer)
            if (GetTreeViewItemFromObject(childContainer, targetObject) is TreeViewItem childTarget) return childTarget;
    return null;
}

One would call it by providing the TreeView instance's ItemContainerGenerator and the target data object:

TreeViewItem tvi = GetTreeViewItemFromObject(treeView.ItemContainerGenerator, targetDataObject);
Forgiving answered 11/1, 2019 at 19:46 Comment(0)
I
1

A single 'ItemContainerGenerator.ContainerFromItem' or 'ItemContainerGenerator.ItemContainerGenerator' call cannot find the TreeViewItem associated by tree view object, since the separation of tree view item controller and tree view item data. It is necessary to create a recursive function to use 'ItemContainerGenerator.ContainerFromItem' and 'ItemContainerGenerator.ItemContainerGenerator', to locate TreeViewItem in tree view. Example recursive function could look like:

private TreeViewItem GetTreeViewItemFromObject(ItemContainerGenerator container, object tvio)
{
    var item = container.ContainerFromItem(tvio) as TreeViewItem;
    if (item != null)
    {
        return item;
    }

    for (int i = 0; i < container.Items.Count; i++)
    {
        var subContainer = (TreeViewItem)container.ContainerFromIndex(i);
        if (subContainer != null)
        {
            item = GetTreeViewItemFromObject(subContainer.ItemContainerGenerator, tvio);
            if (item != null)
            {
                return item;
            }
        }
    }

    return null;
}

called by var target = GetTreeViewItemFromObject(treeView.ItemContainerGenerator, item);

The recursive function only works after tree view's 'ItemContainerGenerator.Status' is 'ContainersGenerated'. So during the initialization view period, 'GetTreeViewItemFromObject' does not work.

Indolence answered 8/6, 2018 at 19:36 Comment(0)
O
0

Do you need the TreeViewItem because you're going to modify what is being displayed? If that is the case, I'd recommend using a Style to change how the item is being displayed instead of using code-behind instead of directly modifying the TreeViewItem. It should hopefully be cleaner.

Outbuilding answered 22/9, 2009 at 2:47 Comment(0)
H
0

If you have to find item in children of children you may have to use recursion like this

public bool Select(TreeViewItem item, object select) // recursive function to set item selection in treeview
{
    if (item == null)
        return false;
    TreeViewItem child = item.ItemContainerGenerator.ContainerFromItem(select) as TreeViewItem;
    if (child != null)
    {
        child.IsSelected = true;
        return true;
    }
    foreach (object c in item.Items)
    {
        bool result = Select(item.ItemContainerGenerator.ContainerFromItem(c) as TreeViewItem, select);
        if (result == true)
            return true;
    }
    return false;
}
Hayne answered 6/9, 2013 at 11:2 Comment(0)
V
0

Here is solution. rtvEsa is treeview. HierarchicalDataTemplate is treeview template Tag make real consumation of current item. This is not selected item it is current item in tree control that use HierarchicalDataTemplate.

Items.CurrentItem is part of internal tree colection. You cant get many and diferent data. For example Items.ParenItem too.

  <HierarchicalDataTemplate ItemsSource="{Binding ChildItems}">

  <TextBox Tag="{Binding ElementName=rtvEsa, Path=Items.CurrentItem }" />

Valet answered 9/10, 2014 at 7:28 Comment(0)
T
0

My TreeView template has only one level of headers, so I found success with this:

        foreach (CollectionViewGroup grp in treeView.Items)
        {
            string strHeader = (string)grp.Name;
            TreeViewItem treeViewItem = (TreeViewItem)treeView.ItemContainerGenerator.ContainerFromItem(grp);
Titustityus answered 2/7, 2022 at 16:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.