How to get TreeViewItem from HierarchicalDataTemplate item?

50,601

Solution 1

From Bea Stollnitz's blog entry about this, try

TreeViewItem item = (TreeViewItem)(mainTreeList
    .ItemContainerGenerator
    .ContainerFromIndex(mainTreeList.Items.CurrentPosition));

Solution 2

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;

        ...
     }

Solution 3

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.

Solution 4

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.

Solution 5

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; }
}
Share:
50,601
Razzie
Author by

Razzie

I graduated at the University of Amsterdam in 2006, in Software Engineering. I primarily develop in C# (since 2003), while I learned to program in Java. I got employed by a Dutch website company in 2006, located in The Hague, where I'm currently a software engineer. In my spare time I like to mess around with WPF a little.

Updated on January 12, 2020

Comments

  • Razzie
    Razzie over 4 years

    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?

  • Razzie
    Razzie about 15 years
    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)
  • Cameron MacFarland
    Cameron MacFarland about 15 years
    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.
  • Cameron MacFarland
    Cameron MacFarland almost 14 years
    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.
  • Cameron MacFarland
    Cameron MacFarland almost 14 years
    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?
  • markmnl
    markmnl almost 14 years
    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.
  • markmnl
    markmnl almost 14 years
    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.
  • markmnl
    markmnl about 13 years
    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=59
  • Roman Starkov
    Roman Starkov about 12 years
    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.
  • Jason Ridge
    Jason Ridge about 12 years
    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.
  • Roman Starkov
    Roman Starkov about 12 years
    How to add the event in code: ctLayersTree.AddHandler(TreeViewItem.SelectedEvent, <your handler>)
  • René
    René almost 11 years
    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.
  • markmnl
    markmnl over 10 years
    I agree, however the question was how to get the TreeViewItem not the object it wraps.
  • zionpi
    zionpi about 10 years
    It has been worked for a period of time,but now CurrentPosition always be 0.
  • L.Trabacchin
    L.Trabacchin over 9 years
    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.
  • Roman Starkov
    Roman Starkov over 9 years
    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.
  • Roman Starkov
    Roman Starkov over 9 years
    @L.Trabacchin 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.
  • L.Trabacchin
    L.Trabacchin over 9 years
    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!
  • j riv
    j riv almost 9 years
    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.
  • markmnl
    markmnl almost 9 years
    @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.
  • Ejrr1085
    Ejrr1085 almost 5 years
    Not work, mainTreeList.SelectedItemis null is redundant, mainTreeList.SelectedItem and TreeViewItem item is the same.