30

I have a three level treeview. How do I select any item in third level from code? I tried a method mentioned in many blogs and on stackoverflow but it seems to work only for first level (dbObject is null for items on below first level).

Here is the code I'm using to select TreeViewItem. Do I miss something?

public static void SetSelectedItem(this TreeView control, object item)
{
    try
    {
        var dObject = control.ItemContainerGenerator.ContainerFromItem(item);

        //uncomment the following line if UI updates are unnecessary
        ((TreeViewItem)dObject).IsSelected = true;

        MethodInfo selectMethod = typeof(TreeViewItem).GetMethod("Select",
            BindingFlags.NonPublic | BindingFlags.Instance);

        selectMethod.Invoke(dObject, new object[] { true });
    }
    catch { }
}
Sergej Andrejev
  • 9,091
  • 11
  • 71
  • 108
  • 14
    WPF’s TreeView sacrifices *every* last bit of usability to achieve features that most people don’t seem to need most of the time... – Roman Starkov Mar 26 '12 at 11:02

6 Answers6

36

Another option would be to use binding. If you have an object that you are using binding with to get the text of each TreeViewItem (for example), you can create a style that also binds the IsSelected property:

<TreeView>
    <TreeView.Resources>
        <Style TargetType="TreeViewItem">
            <Setter Property="IsSelected"
                    Value="{Binding Path=IsSelected, Mode=TwoWay}" />
        </Style>
    </TreeView.Resources>
</TreeView>

This assumes that the bound object has an IsSelected property of type bool. You can then select a TreeViewItem by setting IsSelected to true for its corresponding object.

The same approach can be used with the IsExpanded property to control when a TreeViewItem is expanded or collapsed.

Andy
  • 30,088
  • 6
  • 78
  • 89
  • Yes, I was aware of this. But it looks like it introduces code coupling. Anyway it's good that you have that answer here. People who will come to this page may prefer your way over mine – Sergej Andrejev Jun 03 '09 at 18:40
  • @Andy: How can i do this in Silverlight? I try this code get error `Cannot set read-only property ''.`. – Navid Farhadi Jul 21 '11 at 07:56
  • 1
    @Navid I'm not sure if you can. I haven't done much with Silverlight, but I don't think that TreeViewItem.IsSelected is a DependencyProperty in Silverlight. You can't use binding for properties that aren't a DependencyProperty. – Andy Jul 21 '11 at 17:33
  • Doesn't seem to work for me; the programmatical selection is always automatically reset to what was selected before, unless nothing was selected before. I've created a [related question](http://stackoverflow.com/questions/13778515/synchronizing-a-selectedpath-property-with-the-selecteditem-in-wpfs-treeview) with sample code. – O. R. Mapper Dec 09 '12 at 14:05
  • In order for this code to work properly whether an item is already selected or not, `Mode=TwoWay` is important. – hfann Oct 25 '20 at 16:10
9

You can use the following TreeView extension, which I find is a simpler solution:

public static class TreeViewExtension
{
    public static bool SetSelectedItem(this TreeView treeView, object item)
    {
        return SetSelected(treeView, item);
    }

    private static bool SetSelected(ItemsControl parent, object child)
    {
       if (parent == null || child == null)
          return false;

       TreeViewItem childNode = parent.ItemContainerGenerator
       .ContainerFromItem(child) as TreeViewItem;

       if (childNode != null)
       {
          childNode.Focus();
          return childNode.IsSelected = true;
       }

       if (parent.Items.Count > 0) 
       {
          foreach (object childItem in parent.Items)
          {
             ItemsControl childControl = parent
               .ItemContainerGenerator
               .ContainerFromItem(childItem) 
               as ItemsControl;

             if (SetSelected(childControl, child))
               return true;
          }
       }

      return false;
   }
}

For more info, read this blog article; http://decompile.it/blog/2008/12/11/selecting-an-item-in-a-treeview-in-wpf/

Matt
  • 74,352
  • 26
  • 153
  • 180
kbisang
  • 558
  • 4
  • 13
4

After trying different sollutions I came to this site. Zhou Yong shows how to programatically expand all nodes of TreeView. There are two main ideas in his method:

  • ContainerFromItem will return container only if item is direct child of the element. In TreeView that means that only first level child container will be returned and you have to call ContainerFromItem on child TreeViewItem to get container from next level
  • For ContainerFromItem to work TreeViewItem visual children should be created and this happens only when TreeViewItem is expanded. That means that to select TreeViewItem all items preceding required item must be expanded. In practice that means that we will have to provide path to the item we want to select instead of just the item.

Here is the code I ended up with

public static void SelectItem(this ItemsControl parentContainer, List<object> path)
{
    var head = path.First();
    var tail = path.GetRange(1, path.Count - 1);
    var itemContainer = parentContainer.ItemContainerGenerator.ContainerFromItem(head) as TreeViewItem;

    if (itemContainer != null && itemContainer.Items.Count == 0)
    {
        itemContainer.IsSelected = true;

        var selectMethod = typeof(TreeViewItem).GetMethod("Select", BindingFlags.NonPublic | BindingFlags.Instance);
        selectMethod.Invoke(itemContainer, new object[] { true });
    }
    else if (itemContainer != null)
    {
        itemContainer.IsExpanded = true;

        if (itemContainer.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
        {
            itemContainer.ItemContainerGenerator.StatusChanged += delegate
            {
                SelectItem(itemContainer, tail);
            };
        }
        else
        {
            SelectItem(itemContainer, tail);
        }
    }
}
Sergej Andrejev
  • 9,091
  • 11
  • 71
  • 108
  • It seems that I have to *have* the item I want to select. However, usually that very object is not available; you just have some ID of that object, right? I'm not sure how to call `ContainerFromItem` without having the actual item instance. – O. R. Mapper Dec 06 '12 at 21:27
2

In my case (I had the same problem) but it was unappropriate to use binding to IsSelected property of the Data object and also I couldnt easily get the path to the tree item, so the following code did the job perfectly:

  private void SelectTreeViewItem(object item)
    {
        try
        {
            var tvi = GetContainerFromItem(this.MainRegion, item);

            tvi.Focus();
            tvi.IsSelected = true;

            var selectMethod =
                typeof(TreeViewItem).GetMethod("Select",
                System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

            selectMethod.Invoke(tvi, new object[] { true });
        }
        catch { }
    }

  private TreeViewItem GetContainerFromItem(ItemsControl parent, object item)
    {
        var found = parent.ItemContainerGenerator.ContainerFromItem(item);
        if (found == null)
        {
            for (int i = 0; i < parent.Items.Count; i++)
            {
                var childContainer = parent.ItemContainerGenerator.ContainerFromIndex(i) as ItemsControl;
                TreeViewItem childFound = null;
                if (childContainer != null && childContainer.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
                {
                    childContainer.ItemContainerGenerator.StatusChanged += (o, e) =>
                        {
                             childFound = GetContainerFromItem(childContainer, item);
                        };
                }
                else
                {
                     childFound = GetContainerFromItem(childContainer, item);                            
                }
                if (childFound != null)
                    return childFound;                 
            }
        }
        return found as TreeViewItem;
    }
Mladen Nikolov
  • 191
  • 1
  • 7
0

Very late to the party with my answer but for those wanting a pure MVVM solution this can be done with an Event Trigger (to update the binding when the user selects a new item) and a Data Trigger (to update the selected item when the value of the binding changes).

For this to work the main ViewModel needs the items, a property for the currently selected item and a command property that will be called when the currently selected item changes:

public class MainViewModel : ViewModelBase
{
    // the currently selected node, can be changed programmatically
    private Node _CurrentNode;
    public Node CurrentNode
    {
        get { return this._CurrentNode; }
        set { this._CurrentNode = value; RaisePropertyChanged(() => this.CurrentNode); }
    }

    // called when the user selects a new node in the tree view
    public ICommand SelectedNodeChangedCommand { get { return new RelayCommand<Node>(OnSelectedNodeChanged); } }
    private void OnSelectedNodeChanged(Node node)
    {
        this.CurrentNode = node;
    }

    // list of items to display in the tree view
    private ObservableCollection<Node> _Items;
    public ObservableCollection<Node> Items
    {
        get { return this._Items; }
        set { this._Items = value; RaisePropertyChanged(() => this.Items); }
    }
}

The TreeView needs an event trigger to call SelectedNodeChangedCommand when the selection changes, and a DataTrigger in the TreeViewItem style so that the control items get selected when the value of CurrentNode is changed programatically in the code:

<TreeView x:Name="treeView" ItemsSource="{Binding Items}"
            xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
            xmlns:cmd ="http://www.galasoft.ch/mvvmlight">
        <TreeView.Resources>

            <conv:EqualityConverter x:Key="EqualityConverter" />

            <Style TargetType="TreeViewItem">
                <Setter Property="IsExpanded" Value="True" />
                <Setter Property="IsSelected" Value="False" />
                <Style.Triggers>
                    <!-- DataTrigger updates TreeViewItem selection when vm code changes CurrentNode -->
                    <DataTrigger Value="True">
                        <DataTrigger.Binding>
                            <MultiBinding Converter="{StaticResource EqualityConverter}">
                                <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type TreeView}}" Path="DataContext.CurrentNode" />
                                <Binding />
                            </MultiBinding>
                        </DataTrigger.Binding>
                        <Setter Property="IsSelected" Value="True" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>


            <!-- *** HierarchicalDataTemplates go here ***  -->

        </TreeView.Resources>

        <!-- EventTrigger invokes SelectedNodeChangedCommand when selection is changed by user interaction -->
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="SelectedItemChanged">
                <cmd:EventToCommand Command="{Binding SelectedNodeChangedCommand}" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=TreeView}, Path=SelectedItem}"  />
            </i:EventTrigger>
        </i:Interaction.Triggers>

    </TreeView>

The DataTrigger works by detecting when the value of CurrentNode matches the Node for the current list item. Unfortunately DataTriggers can't bind their Value, so it has to test with an EqualityConverter instead which just does a simple comparison:

    public class EqualityConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        return values[0] == values[1];
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
Mark Feldman
  • 15,731
  • 3
  • 31
  • 58
0

Yeah, the ContainerFromItem method isn't giving back anything, even when you call it from the direct parent TreeViewItem.

You may need to do a bit of redesign. If you create everything as an explicit TreeViewItem you should be able to keep a reference to it and set IsSelected on it.

RandomEngy
  • 14,931
  • 5
  • 70
  • 113