36
votes

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?

11
@romkyns: What? This question has nothing to do with keeping track of the selected item.H.B.
@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.H.B.
@H.B. That might well be so... maybe you can help me do this without TreeViewItem?Roman Starkov
@H.B. I’ve asked the question directly now: is it wrong to access TreeViewItems?, would appreciate your input.Roman Starkov

11 Answers

32
votes
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;

        ...
     }
24
votes

From Bea Stollnitz's blog entry about this, try

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

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.

6
votes
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.

4
votes

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; }
}
2
votes

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;
    }
1
votes

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.

1
votes

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);
0
votes

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.

0
votes

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;
}
0
votes

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 }" />