40

This cannot be this difficult. The TreeView in WPF doesn't allow you to set the SelectedItem, saying that the property is ReadOnly. I have the TreeView populating, even updating when it's databound collection changes.

I just need to know what item is selected. I am using MVVM, so there is no codebehind or variable to reference the treeview by. This is the only solution I have found, but it is an obvious hack, it creates another element in XAML that uses ElementName binding to set itself to the treeviews selected item, which you must then bind your Viewmodel too. Several other questions are asked about this, but no other working solutions are given.

I have seen this question, but using the answer given gives me compile errors, for some reason I cannot add a reference to the blend sdk System.Windows.Interactivity to my project. It says "unknown error system.windows has not been preloaded" and I haven't yet figured out how to get past that.

For Bonus Points: why the hell did Microsoft make this element's SelectedItem property ReadOnly?

Community
  • 1
  • 1
Kyeotic
  • 19,697
  • 10
  • 71
  • 128
  • There is a way with attached properties, I'll create an answer – Bas Aug 22 '11 at 21:51
  • 4
    Or you can just use the codebehind to handle this for you. The lack of a bindable SelectedItem in the treeview is a well known defect. And I do call it a defect, because a simple workaround can provide for a solution. –  Aug 23 '11 at 10:26
  • One of the easiest ways probably: https://stackoverflow.com/questions/1238304/get-selecteditem-from-treeview – JoanComasFdz Sep 07 '17 at 08:26
  • Related: https://stackoverflow.com/questions/9880589/bind-to-selecteditems-from-datagrid-or-listbox-in-mvvm – StayOnTarget Jul 20 '20 at 16:59

6 Answers6

56

You should not really need to deal with the SelectedItem property directly, bind IsSelected to a property on your viewmodel and keep track of the selected item there.

A sketch:

<TreeView ItemsSource="{Binding TreeData}">
    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsSelected" Value="{Binding IsSelected}" />
        </Style>
    </TreeView.ItemContainerStyle>
</TreeView>
public class TViewModel : INotifyPropertyChanged
{
    private static object _selectedItem = null;
    // This is public get-only here but you could implement a public setter which
    // also selects the item.
    // Also this should be moved to an instance property on a VM for the whole tree, 
    // otherwise there will be conflicts for more than one tree.
    public static object SelectedItem
    {
        get { return _selectedItem; }
        private set
        {
            if (_selectedItem != value)
            {
                _selectedItem = value;
                OnSelectedItemChanged();
            }
        }
    }

    static virtual void OnSelectedItemChanged()
    {
        // Raise event / do other things
    }

    private bool _isSelected;
    public bool IsSelected
    {
        get { return _isSelected; }
        set
        {
            if (_isSelected != value)
            {
                _isSelected = value;
                OnPropertyChanged("IsSelected");
                if (_isSelected)
                {
                    SelectedItem = this;
                }
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        var handler = this.PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}
kmote
  • 16,095
  • 11
  • 68
  • 91
H.B.
  • 166,899
  • 29
  • 327
  • 400
  • 4
    I don't understand how that would move back up the tree. `SelectedItem = this` Occurs at the TreeViewItem level, setting a property on the TreeViewItem, not the treeview. I would still need to locate the TreeViewItem that has that property set, and I am right back at square one. – Kyeotic Aug 22 '11 at 22:55
  • 1
    @Tyrsius: Indeed, you do not understand it. It is one-way, as you said `I just need to know what item is selected`, the **static** property `SelectedItem` will tell you as all the `TreeViewItems` report to it via the `IsSelected`-binding. – H.B. Aug 22 '11 at 23:03
  • I tried adding that, I get a compile error, "ProperyChangedEventHandled does not contain a definition for Notify" – Kyeotic Aug 22 '11 at 23:15
  • 4
    @Tyrsius: Man, i could rant about people copy-pasting code from SO... Have you never seen a property which raises `PropertyChanged`? You should be able to raise it without using that extension method (which you obviously do not have). – H.B. Aug 22 '11 at 23:20
  • 5
    Yes, I have seen it raised, in about a dozen and a half tutorials. I am sorry I didn't understand the context that you didn't supply. – Kyeotic Aug 22 '11 at 23:28
  • 1
    Gotta say, love the above code snippet, reads like a dream, absolutely love the simplicity. The above or GalaSofts EventToCommand are going to be my chosen methods of tackling what node has been selected. Let's see which one works the quickest ... = ) – IbrarMumtaz Apr 18 '12 at 10:16
  • @H.B. I've been looking over this solution as I was using a similar method to track the my selected item in my list. I have an observablecollection of TreeViewItems on my VM, but do not have a VM for the treeview. Is your above code snipit a VM for the TreeView? Does this approach allow for the selected item to bubble up the tree expanding its parents nodes? That's the issue I am having. I set the TreeViewItem's VM to selected and it's selected but its parents are not expanded. – Mike G Jun 05 '12 at 12:36
  • 1
    @Mike: It's a vm (see the class name), to expand all the items you can bind `IsExpanded` in the same manner, you will also need your vm to have a reference to the parent node, that way you can go up and set `IsExpanded` to `true`. – H.B. Jun 05 '12 at 12:51
  • @H.B. My initial approach was to let the view handle expanding the nodes through the SelectedItemChanged event on the TreeView. This didn't work at first so I ended up going with the route you mentioned. However, while doing some testing I still had a breakpoint in the view event and saw that it started firing. I may switch my code to the event so that this is considered view specific. Thanks! – Mike G Jun 05 '12 at 14:53
  • 5
    @H.B. Because you rely on a static property (`TViewModel.SelectedItem`) this solution does not work when you have more than one TreeView in your current process. – bitbonk Jul 04 '12 at 06:56
  • @bitbonk: Well, you can just refactor the property to a logical parent view model for the tree itself as an instance property. – H.B. Jul 04 '12 at 12:21
  • I believe this is the Right Way to do it, by binding the TreeViewItem's IsSelected property to a property on the view model. And if you use a Two Way binding, you can also update which item is selected in the Tree View, by simply setting IsSelected on your view model object. Pretty elegant! – René May 24 '13 at 07:57
  • 2
    "You should not really need to deal with the SelectedItem property directly" I disagree, I would expect TreeView as a container of items to expose it's selected item in the same way that a ListBox does. That said, +1 for providing the correct answer and a best-practice workaround – darkpbj May 11 '15 at 16:42
16

A very unusual but quite effective way to solve this in a MVVM-acceptable way is the following:

  1. Create a visibility-collapsed ContentControl on the same View the TreeView is. Name it appropriately, and bind its Content to some SelectedSomething property in viewmodel. This ContentControl will "hold" the selected object and handle it's binding, OneWayToSource;
  2. Listen to the SelectedItemChanged in TreeView, and add a handler in code-behind to set your ContentControl.Content to the newly selected item.

XAML:

<ContentControl x:Name="SelectedItemHelper" Content="{Binding SelectedObject, Mode=OneWayToSource}" Visibility="Collapsed"/>
<TreeView ItemsSource="{Binding SomeCollection}"
    SelectedItemChanged="TreeView_SelectedItemChanged">

Code Behind:

    private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        SelectedItemHelper.Content = e.NewValue;
    }

ViewModel:

    public object SelectedObject  // Class is not actually "object"
    {
        get { return _selected_object; }
        set
        {
            _selected_object = value;
            RaisePropertyChanged(() => SelectedObject);
            Console.WriteLine(SelectedObject);
        }
    }
    object _selected_object;
heltonbiker
  • 26,657
  • 28
  • 137
  • 252
12

You can create an attached property that is bindable and has a getter and setter:

public class TreeViewHelper
{
    private static Dictionary<DependencyObject, TreeViewSelectedItemBehavior> behaviors = new Dictionary<DependencyObject, TreeViewSelectedItemBehavior>();

    public static object GetSelectedItem(DependencyObject obj)
    {
        return (object)obj.GetValue(SelectedItemProperty);
    }

    public static void SetSelectedItem(DependencyObject obj, object value)
    {
        obj.SetValue(SelectedItemProperty, value);
    }

    // Using a DependencyProperty as the backing store for SelectedItem.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(TreeViewHelper), new UIPropertyMetadata(null, SelectedItemChanged));

    private static void SelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        if (!(obj is TreeView))
            return;

        if (!behaviors.ContainsKey(obj))
            behaviors.Add(obj, new TreeViewSelectedItemBehavior(obj as TreeView));

        TreeViewSelectedItemBehavior view = behaviors[obj];
        view.ChangeSelectedItem(e.NewValue);
    }

    private class TreeViewSelectedItemBehavior
    {
        TreeView view;
        public TreeViewSelectedItemBehavior(TreeView view)
        {
            this.view = view;
            view.SelectedItemChanged += (sender, e) => SetSelectedItem(view, e.NewValue);
        }

        internal void ChangeSelectedItem(object p)
        {
            TreeViewItem item = (TreeViewItem)view.ItemContainerGenerator.ContainerFromItem(p);
            item.IsSelected = true;
        }
    }
}

Add the namespace declaration containing that class to your XAML and bind as follows (local is how I named the namespace declaration):

<TreeView ItemsSource="{Binding Path=Root.Children}"
          local:TreeViewHelper.SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}"/>

Now you can bind the selected item, and also set it in your view model to change it programmatically, should that requirement ever arise. This is, of course, assuming that you implement INotifyPropertyChanged on that particular property.

H.B.
  • 166,899
  • 29
  • 327
  • 400
Bas
  • 26,772
  • 8
  • 53
  • 86
  • 1
    This looks promising, but I am not able to implement it correctly. The getter fires when the treeview constructs, and when asking for the property, but the setter never fires. – Kyeotic Aug 22 '11 at 22:26
  • 2
    @Bas: Every `TreeViewItem` is itself an `ItemsControl` and you can't be sure that its children have been generated yet. You would have to check if `ItemContainerGenerator` is returning null, expand the current `TreeViewItem`, wait for it to finish generating and then try again recursively. In a big tree that can take alot of time – Fredrik Hedblad Aug 22 '11 at 22:58
  • 1
    Yes, but that is the case for any way you do it, if you want to be able to set the selected item. – Bas Aug 23 '11 at 08:13
  • 12
    There is a **memoryleak** in th this solution. Your static `behaviors` dictionary holds all TreeViews (including all their items they might have) that ever have been attached to in memory forever. – bitbonk Jul 04 '12 at 07:12
5

Use the OneWayToSource binding mode. This doesn't work. See edit.

Edit: Looks like this is a bug or "by design" behavior from Microsoft, according to this question; there are some workarounds posted, though. Do any of those work for your TreeView?

The Microsoft Connect issue: https://connect.microsoft.com/WPF/feedback/details/523865/read-only-dependency-properties-does-not-support-onewaytosource-bindings

Posted by Microsoft on 1/10/2010 at 2:46 PM

We cannot do this in WPF today, for the same reason we cannot support bindings on properties that are not DependencyProperties. The runtime per-instance state of a binding is held in a BindingExpression, which we store in the EffectiveValueTable for the target DependencyObject. When the target property is not a DP or the DP is read-only, there's no place to store the BindingExpression.

It's possible we may some day choose to extend binding functionality to these two scenarios. We get asked about them pretty frequently. In other words, your request is already on our list of features to consider in future releases.

Thanks for your feedback.

Community
  • 1
  • 1
Aphex
  • 7,390
  • 5
  • 33
  • 54
  • This does not work, it gives the same error as any other mode. The SelectedItem property is readonly, you cannot set it AT ALL. – Kyeotic Aug 22 '11 at 21:44
  • I looked into that for you, looks like it's by design for some reason. Check the link in my edit. – Aphex Aug 22 '11 at 21:51
  • How then does microsoft intend we get the selection from a treeview? – Kyeotic Aug 22 '11 at 22:30
  • I would try one of the workarounds posted in the SO question or one of the workarounds posted on the Microsoft Connect issue (there are 2 code examples posted there). While at least now we know why this isn't possible now, and that Microsoft might change the way BindingExpressions are stored in a future release, it's anybody's theory as to why the hell a TreeView's SelectedItem is read only in the first place. – Aphex Aug 22 '11 at 22:44
2

I decided to use a combination of code behind and viewmodel code. the xaml is like this:

<TreeView 
                    Name="tvCountries"
                ItemsSource="{Binding Path=Countries}"
                ItemTemplate="{StaticResource ResourceKey=countryTemplate}"   
                    SelectedValuePath="Name"
                    SelectedItemChanged="tvCountries_SelectedItemChanged">

Code behind

private void tvCountries_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        var vm = this.FindResource("vm") as ViewModels.CoiEditorViewModel;
        if (vm != null)
        {
            var treeItem = sender as TreeView;
            vm.TreeItemSelected = treeItem.SelectedItem;
        }
    }

And in the viewmodel there is a TreeItemSelected object which you can then access in the viewmodel.

allan
  • 949
  • 1
  • 10
  • 17
1

You can always create a DependencyProperty that uses ICommand and listen to the SelectedItemChanged event on the TreeView. This can be a bit easier than binding IsSelected, but I imagine you will wind up binding IsSelected anyway for other reasons. If you just want to bind on IsSelected you can always have your item send a message whenever IsSelected changes. Then you can listen to those messages anyplace in your program.

stricq
  • 798
  • 6
  • 18