Data binding to SelectedItem in a WPF Treeview
Asked Answered
S

21

264

How can I retrieve the item that is selected in a WPF-treeview? I want to do this in XAML, because I want to bind it.

You might think that it is SelectedItem but apparently that does not exist is readonly and therefore unusable.

This is what I want to do:

<TreeView ItemsSource="{Binding Path=Model.Clusters}" 
            ItemTemplate="{StaticResource ClusterTemplate}"
            SelectedItem="{Binding Path=Model.SelectedCluster}" />

I want to bind the SelectedItem to a property on my Model.

But this gives me the error:

'SelectedItem' property is read-only and cannot be set from markup.

Edit: Ok, this is the way that I solved this:

<TreeView
          ItemsSource="{Binding Path=Model.Clusters}" 
          ItemTemplate="{StaticResource HoofdCLusterTemplate}"
          SelectedItemChanged="TreeView_OnSelectedItemChanged" />

and in the codebehindfile of my xaml:

private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
    Model.SelectedCluster = (Cluster)e.NewValue;
}
Seasonal answered 16/6, 2009 at 7:41 Comment(7)
Man this sucks. It just hit me too. I came here hoping to find that there is a decent way and I'm just an idiot. This is the first time that I am sad that I am not an idiot..Hexachlorophene
this really sucks and mess up the binding conceptCanter
Hope this could help some one to bind to a tree view item selected changed call back on Icommand jacobaloysious.wordpress.com/2012/02/19/…Covington
In terms of binding and MVVM, code behind is not "banned", rather code behind should support the view. In my opinion from all the other solutions I've seen, the code behind is a vastly better option since it's still dealing with "binding" the view to the viewmodel. The only negative is that if you have a team with a designer on it working only in XAML, the code behind could get broken/neglected. It's a small price to pay for a solution that takes 10 seconds to implement.Avow
One of the easiest solutions probably: #1238804Undersheriff
The easiest way for me was this.Junkman
I think it is sad and astonishing that nearly a dozen years later, Microsoft has still not fixed this horrible developer experience. It's... unbelievable, really.Grubman
D
279

I realise this has already had an answer accepted, but I put this together to solve the problem. It uses a similar idea to Delta's solution, but without the need to subclass the TreeView:

public class BindableSelectedItemBehavior : Behavior<TreeView>
{
    #region SelectedItem Property

    public object SelectedItem
    {
        get { return (object)GetValue(SelectedItemProperty); }
        set { SetValue(SelectedItemProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(BindableSelectedItemBehavior), new UIPropertyMetadata(null, OnSelectedItemChanged));

    private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var item = e.NewValue as TreeViewItem;
        if (item != null)
        {
            item.SetValue(TreeViewItem.IsSelectedProperty, true);
        }
    }

    #endregion

    protected override void OnAttached()
    {
        base.OnAttached();

        this.AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        if (this.AssociatedObject != null)
        {
            this.AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
        }
    }

    private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        this.SelectedItem = e.NewValue;
    }
}

You can then use this in your XAML as:

<TreeView>
    <e:Interaction.Behaviors>
        <behaviours:BindableSelectedItemBehavior SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
    </e:Interaction.Behaviors>
</TreeView>

Hopefully it will help someone!

Deferential answered 25/2, 2011 at 14:29 Comment(16)
As Brent pointed out I also needed to add Mode=TwoWay to the binding. I'm not a "Blender" so wasn't familiar withe the Behavior<> class from System.Windows.Interactivity. The assembly is part of Expression Blend. For those who don't want to buy/install trial to get this assembly you can download the BlendSDK which includes System.Windows.Interactivity. BlendSDK 3 for 3.5... I think it is BlendSDK 4 for 4.0. Note: This only allows you to get what item is selected, does not allow you to set the selected itemWoodworking
See my answer below for a solution without dependencies to external assemblies. And also allows setting the selected item.Moniquemonism
You can also replace UIPropertyMetadata by FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));Bicknell
Does it work on data-bound treeview? It doesn't work for me, changing SelectedItem on the view model is not reflected on the UI. Mode=TwoWay is set.Canady
@user986080 yes, it should work on data bound lists. If it isn't, put the details and some sample code in a new questionDeferential
This would be an approach to solve the problem: https://mcmap.net/q/110976/-binding-selecteditem-in-a-hierarchicaldatatemplate-applied-wpf-treeviewDenounce
But how could I bind SelectedItem to my ViewModel field?Burnsides
@Burnsides exactly as shown in the XAML code snippet above. Just replace {Binding SelectedItem, Mode=TwoWay} with {Binding MyViewModelField, Mode=TwoWay}Deferential
For me it just works as one-way-binding. When I select an item in the gui everything is fine, but on Setting the selecteditem in code behind nothing happens - what do I miss?Prophylactic
What a namespace is this? <e:Interaction.Behaviors> Can you show your xaml declaration please? I can not get the behavior to compile!Hammerfest
@Hammerfest it's xmlns:e="http://schemas.microsoft.com/expression/2010/interactivity"Deferential
@Steve I have added your xmlns and get this: Error 1 The tag 'Interaction.Behaviors' does not exist in XML namespace 'schemas.microsoft.com/expression/2010/interactivity'. Line 53 Position 26. I have the System.Windows.Interactivity assembly referenced , do I have the wrong one?Hammerfest
Rock star response. Nice and clean, thank you. Question now is how to use this programmatically? I tried: myTreeView.SetBinding(TreeView.SelectedItemProperty, selectedItemBinding) and still run into exception. I also tried tv.SetBinding(BindableSelectedItemBehavior.SelectedItemProperty, selectedItemBinding) but change is not propagated to SelectedItem property in my view model.Ealdorman
Nice answer, I used it in my WPF but I didn't have Blend and didn't want to install the SDK (cause all developers would need to install it then). I prevent installing the Blend SDK by installing this NuGet package: nuget.org/packages/System.Windows.Interactivity.WPF Now all developers just get the NuGet package when they build and they don't need to install some SDK.Iritis
This only works if your SelectedItem is a TreeViewItem, which in MVVM world is a no-no... in order to make this work with any data in SelectedItem, we need to get the container of the data, which is tricky because each treeview parent node has its own containerSubtract
Purely from the code's point of view, in the OnDetaching function, shall we call "base.OnDetaching();" at the end of the function? (After our customization.)Teets
S
46

This property exists : TreeView.SelectedItem

But it is readonly, so you cannot assign it through a binding, only retrieve it

Synecious answered 16/6, 2009 at 8:4 Comment(2)
I accept this answer, because there I found this link, which let to my own answer: msdn.microsoft.com/en-us/library/ms788714.aspxSeasonal
So can I have this TreeView.SelectedItem affect a property on the model when the user selects an item (aka OneWayToSource)?Bhili
C
40

Well, I found a solution. It moves the mess, so that MVVM works.

First add this class:

public class ExtendedTreeView : TreeView
{
    public ExtendedTreeView()
        : base()
    {
        this.SelectedItemChanged += new RoutedPropertyChangedEventHandler<object>(___ICH);
    }

    void ___ICH(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        if (SelectedItem != null)
        {
            SetValue(SelectedItem_Property, SelectedItem);
        }
    }

    public object SelectedItem_
    {
        get { return (object)GetValue(SelectedItem_Property); }
        set { SetValue(SelectedItem_Property, value); }
    }
    public static readonly DependencyProperty SelectedItem_Property = DependencyProperty.Register("SelectedItem_", typeof(object), typeof(ExtendedTreeView), new UIPropertyMetadata(null));
}

and add this to your xaml:

 <local:ExtendedTreeView ItemsSource="{Binding Items}" SelectedItem_="{Binding Item, Mode=TwoWay}">
 .....
 </local:ExtendedTreeView>
Canter answered 20/8, 2010 at 21:43 Comment(5)
This is the ONLY thing that has come close to working for me so far. I really like this solution.Hallah
Don't know why but it did not work for me :( I succeeded to get the selected item from the tree but not the vice versa - to change the selected item from outside the tree.Wesleywesleyan
It would be slightly neater to set the dependency property as BindsTwoWayByDefault then you wouldn't need to specify TwoWay in the XAMLRattling
This is the best approach. It does not use Interactivity reference, it does not use code behind, it does not have a memory leak like some behaviors have. Thank you.Farceur
As mentioned, this solution doesn't work with 2 way binding. If you set the value in the viewmodel, the change doesn't propagate to the TreeView.Teasley
O
35

It answers a little more than the OP is expecting... But I hope it could help some one at least.

If you want to execute a ICommand whenever the SelectedItem changed, you can bind a command on an event and the use of a property SelectedItem in the ViewModel isn't needed anymore.

To do so:

1- Add reference to System.Windows.Interactivity

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

2- Bind the command to the event SelectedItemChanged

<TreeView x:Name="myTreeView" Margin="1"
            ItemsSource="{Binding Directories}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectedItemChanged">
            <i:InvokeCommandAction Command="{Binding SomeCommand}"
                                   CommandParameter="
                                            {Binding ElementName=myTreeView
                                             ,Path=SelectedItem}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <TreeView.ItemTemplate>
           <!-- ... -->
    </TreeView.ItemTemplate>
</TreeView>
Ody answered 9/3, 2016 at 9:57 Comment(3)
The reference System.Windows.Interactivity can be installed from NuGet: nuget.org/packages/System.Windows.Interactivity.WPFCrossfade
I've been trying to this solve this problems for hours, I've implemented this but my command is not working, please could you help me?Oink
There was introduced the XAML Behaviors for WPF by Microsoft at the end of 2018. It can be used instead of of the System.Windows.Interactivity. It was worked for me (tried with the .NET Core project). To set the things up, just add the Microsoft.Xaml.Behaviors.Wpf nuget package, change the namespace to xmlns:i="http://schemas.microsoft.com/xaml/behaviors". To get more info - please see the blogBiebel
V
21

This can be accomplished in a 'nicer' way using only binding and the GalaSoft MVVM Light library's EventToCommand. In your VM add a command which will be called when the selected item is changed, and initialize the command to perform whatever action is necessary. In this example I used a RelayCommand and will just set the SelectedCluster property.

public class ViewModel
{
    public ViewModel()
    {
        SelectedClusterChanged = new RelayCommand<Cluster>( c => SelectedCluster = c );
    }

    public RelayCommand<Cluster> SelectedClusterChanged { get; private set; } 

    public Cluster SelectedCluster { get; private set; }
}

Then add the EventToCommand behavior in your xaml. This is really easy using blend.

<TreeView
      x:Name="lstClusters"
      ItemsSource="{Binding Path=Model.Clusters}" 
      ItemTemplate="{StaticResource HoofdCLusterTemplate}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectedItemChanged">
            <GalaSoft_MvvmLight_Command:EventToCommand Command="{Binding SelectedClusterChanged}" CommandParameter="{Binding ElementName=lstClusters,Path=SelectedValue}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TreeView>
Visual answered 4/2, 2011 at 14:15 Comment(1)
This is a nice solution especially if you are already using MvvmLight toolkit. It does however not solve the problem of setting the selected node and have the treeview update the selection.Nevlin
P
12

All to complicated... Go with Caliburn Micro (http://caliburnmicro.codeplex.com/)

View:

<TreeView Micro:Message.Attach="[Event SelectedItemChanged] = [Action SetSelectedItem($this.SelectedItem)]" />

ViewModel:

public void SetSelectedItem(YourNodeViewModel item) {}; 
Pavid answered 20/7, 2011 at 20:27 Comment(2)
Yes... and where's the part that sets SelectedItem on TreeView?Bev
Caliburn is nice and elegant. Works quite easily for nested hierarchiesIntuitivism
C
9

I came across this page looking for the same answer as the original author, and proving there's always more than one way to do it, the solution for me was even easier than the answers provided here so far, so I figured I might as well add to the pile.

The motivation for the binding is to keep it nice & MVVM. The probable usage of the ViewModel is to have a property w/ a name such as "CurrentThingy", and somewhere else, the DataContext on some other thing is bound to "CurrentThingy".

Rather than going through additional steps required (eg: custom behavior, 3rd party control) to support a nice binding from the TreeView to my Model, and then from something else to my Model, my solution was to use simple Element binding the other thing to TreeView.SelectedItem, rather than binding the other thing to my ViewModel, thereby skipping the extra work required.

XAML:

<TreeView x:Name="myTreeView" ItemsSource="{Binding MyThingyCollection}">
.... stuff
</TreeView>

<!-- then.. somewhere else where I want to see the currently selected TreeView item: -->

<local:MyThingyDetailsView 
       DataContext="{Binding ElementName=myTreeView, Path=SelectedItem}" />

Of course, this is great for reading the currently selected item, but not setting it, which is all I needed.

Carew answered 13/6, 2011 at 4:48 Comment(2)
What is local:MyThingyDetailsView? I get that local:MyThingyDetailsView holds the selected item, but how does your view model get this info? This looks like a nice, clean way to do this, but I need just a little more info...Abednego
local:MyThingyDetailsView is simply a UserControl full of XAML making up a details view about one "thingy" instance. It's embedded in the middle of another view as content, w/ the DataContext of this view is the currently selected tree view item , using Element binding.Carew
M
6

You might also be able to use TreeViewItem.IsSelected property

Mashe answered 10/8, 2010 at 9:36 Comment(2)
I think this might be the correct answer. But I'd like to see an example or best-practice recommandation on how the IsSelected property of the Items is passed to the TreeView.Alfredalfreda
How is this done? I don't have "TreeViewItem" anywhere, I have <TreeView.ItemTemplate><HierarchicalDataTemplate>...Tanishatanitansy
A
4

My requirement was for PRISM-MVVM based solution where a TreeView was needed and the bound object is of type Collection<> and hence needs HierarchicalDataTemplate. The default BindableSelectedItemBehavior wont be able to identify the child TreeViewItem. To make it to work in this scenario.

public class BindableSelectedItemBehavior : Behavior<TreeView>
{
    #region SelectedItem Property

    public object SelectedItem
    {
        get { return (object)GetValue(SelectedItemProperty); }
        set { SetValue(SelectedItemProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(BindableSelectedItemBehavior), new UIPropertyMetadata(null, OnSelectedItemChanged));

    private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var behavior = sender as BindableSelectedItemBehavior;
        if (behavior == null) return;
        var tree = behavior.AssociatedObject;
        if (tree == null) return;
        if (e.NewValue == null)
            foreach (var item in tree.Items.OfType<TreeViewItem>())
                item.SetValue(TreeViewItem.IsSelectedProperty, false);
        var treeViewItem = e.NewValue as TreeViewItem;
        if (treeViewItem != null)
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
        else
        {
            var itemsHostProperty = tree.GetType().GetProperty("ItemsHost", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            if (itemsHostProperty == null) return;
            var itemsHost = itemsHostProperty.GetValue(tree, null) as Panel;
            if (itemsHost == null) return;
            foreach (var item in itemsHost.Children.OfType<TreeViewItem>())
            {
                if (WalkTreeViewItem(item, e.NewValue)) 
                    break;
            }
        }
    }

    public static bool WalkTreeViewItem(TreeViewItem treeViewItem, object selectedValue)
    {
        if (treeViewItem.DataContext == selectedValue)
        {
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
            treeViewItem.Focus();
            return true;
        }
        var itemsHostProperty = treeViewItem.GetType().GetProperty("ItemsHost", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        if (itemsHostProperty == null) return false;
        var itemsHost = itemsHostProperty.GetValue(treeViewItem, null) as Panel;
        if (itemsHost == null) return false;
        foreach (var item in itemsHost.Children.OfType<TreeViewItem>())
        {
            if (WalkTreeViewItem(item, selectedValue))
                break;
        }
        return false;
    }
    #endregion

    protected override void OnAttached()
    {
        base.OnAttached();
        this.AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        if (this.AssociatedObject != null)
        {
            this.AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
        }
    }

    private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        this.SelectedItem = e.NewValue;
    }
}

This enables to iterate through all the elements irrespective of the level.

Astrophysics answered 20/10, 2015 at 9:39 Comment(0)
C
3

I suggest an addition to the behavior provided by Steve Greatrex. His behavior doesn't reflects changes from the source because it may not be a collection of TreeViewItems. So it is a matter of finding the TreeViewItem in the tree which datacontext is the selectedValue from the source. The TreeView has a protected property called "ItemsHost", which holds the TreeViewItem collection. We can get it through reflection and walk the tree searching for the selected item.

private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var behavior = sender as BindableSelectedItemBehaviour;

        if (behavior == null) return;

        var tree = behavior.AssociatedObject;

        if (tree == null) return;

        if (e.NewValue == null) 
            foreach (var item in tree.Items.OfType<TreeViewItem>())
                item.SetValue(TreeViewItem.IsSelectedProperty, false);

        var treeViewItem = e.NewValue as TreeViewItem; 
        if (treeViewItem != null)
        {
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
        }
        else
        {
            var itemsHostProperty = tree.GetType().GetProperty("ItemsHost", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

            if (itemsHostProperty == null) return;

            var itemsHost = itemsHostProperty.GetValue(tree, null) as Panel;

            if (itemsHost == null) return;

            foreach (var item in itemsHost.Children.OfType<TreeViewItem>())
                if (WalkTreeViewItem(item, e.NewValue)) break;
        }
    }

    public static bool WalkTreeViewItem(TreeViewItem treeViewItem, object selectedValue) {
        if (treeViewItem.DataContext == selectedValue)
        {
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
            treeViewItem.Focus();
            return true;
        }

        foreach (var item in treeViewItem.Items.OfType<TreeViewItem>())
            if (WalkTreeViewItem(item, selectedValue)) return true;

        return false;
    }

This way the behavior works for two-way bindings. Alternatively, it is possible to move the ItemsHost acquisition to the Behavior's OnAttached method, saving the overhead of using reflection every time the binding updates.

Countershading answered 12/7, 2011 at 1:38 Comment(0)
R
3

There is also a way to create XAML bindable SelectedItem property without using Interaction.Behaviors.

public static class BindableSelectedItemHelper
{
    #region Properties

    public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(BindableSelectedItemHelper),
        new FrameworkPropertyMetadata(null, OnSelectedItemPropertyChanged));

    public static readonly DependencyProperty AttachProperty = DependencyProperty.RegisterAttached("Attach", typeof(bool), typeof(BindableSelectedItemHelper), new PropertyMetadata(false, Attach));

    private static readonly DependencyProperty IsUpdatingProperty = DependencyProperty.RegisterAttached("IsUpdating", typeof(bool), typeof(BindableSelectedItemHelper));

    #endregion

    #region Implementation

    public static void SetAttach(DependencyObject dp, bool value)
    {
        dp.SetValue(AttachProperty, value);
    }

    public static bool GetAttach(DependencyObject dp)
    {
        return (bool)dp.GetValue(AttachProperty);
    }

    public static string GetSelectedItem(DependencyObject dp)
    {
        return (string)dp.GetValue(SelectedItemProperty);
    }

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

    private static bool GetIsUpdating(DependencyObject dp)
    {
        return (bool)dp.GetValue(IsUpdatingProperty);
    }

    private static void SetIsUpdating(DependencyObject dp, bool value)
    {
        dp.SetValue(IsUpdatingProperty, value);
    }

    private static void Attach(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        TreeListView treeListView = sender as TreeListView;
        if (treeListView != null)
        {
            if ((bool)e.OldValue)
                treeListView.SelectedItemChanged -= SelectedItemChanged;

            if ((bool)e.NewValue)
                treeListView.SelectedItemChanged += SelectedItemChanged;
        }
    }

    private static void OnSelectedItemPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        TreeListView treeListView = sender as TreeListView;
        if (treeListView != null)
        {
            treeListView.SelectedItemChanged -= SelectedItemChanged;

            if (!(bool)GetIsUpdating(treeListView))
            {
                foreach (TreeViewItem item in treeListView.Items)
                {
                    if (item == e.NewValue)
                    {
                        item.IsSelected = true;
                        break;
                    }
                    else
                       item.IsSelected = false;                        
                }
            }

            treeListView.SelectedItemChanged += SelectedItemChanged;
        }
    }

    private static void SelectedItemChanged(object sender, RoutedEventArgs e)
    {
        TreeListView treeListView = sender as TreeListView;
        if (treeListView != null)
        {
            SetIsUpdating(treeListView, true);
            SetSelectedItem(treeListView, treeListView.SelectedItem);
            SetIsUpdating(treeListView, false);
        }
    }
    #endregion
}

You can then use this in your XAML as:

<TreeView  helper:BindableSelectedItemHelper.Attach="True" 
           helper:BindableSelectedItemHelper.SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
Radiopaque answered 30/5, 2013 at 15:5 Comment(0)
F
3

I tried all solutions of this questions. No one solved my problem fully. So I think it's better to use such inherited class with redefined property SelectedItem. It will work perfectly if you choose tree element from GUI and if you set this property value in your code

public class TreeViewEx : TreeView
{
    public TreeViewEx()
    {
        this.SelectedItemChanged += new RoutedPropertyChangedEventHandler<object>(TreeViewEx_SelectedItemChanged);
    }

    void TreeViewEx_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        this.SelectedItem = e.NewValue;
    }

    #region SelectedItem

    /// <summary>
    /// Gets or Sets the SelectedItem possible Value of the TreeViewItem object.
    /// </summary>
    public new object SelectedItem
    {
        get { return this.GetValue(TreeViewEx.SelectedItemProperty); }
        set { this.SetValue(TreeViewEx.SelectedItemProperty, value); }
    }

    // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
    public new static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(TreeViewEx),
        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, SelectedItemProperty_Changed));

    static void SelectedItemProperty_Changed(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        TreeViewEx targetObject = dependencyObject as TreeViewEx;
        if (targetObject != null)
        {
            TreeViewItem tvi = targetObject.FindItemNode(targetObject.SelectedItem) as TreeViewItem;
            if (tvi != null)
                tvi.IsSelected = true;
        }
    }                                               
    #endregion SelectedItem   

    public TreeViewItem FindItemNode(object item)
    {
        TreeViewItem node = null;
        foreach (object data in this.Items)
        {
            node = this.ItemContainerGenerator.ContainerFromItem(data) as TreeViewItem;
            if (node != null)
            {
                if (data == item)
                    break;
                node = FindItemNodeInChildren(node, item);
                if (node != null)
                    break;
            }
        }
        return node;
    }

    protected TreeViewItem FindItemNodeInChildren(TreeViewItem parent, object item)
    {
        TreeViewItem node = null;
        bool isExpanded = parent.IsExpanded;
        if (!isExpanded) //Can't find child container unless the parent node is Expanded once
        {
            parent.IsExpanded = true;
            parent.UpdateLayout();
        }
        foreach (object data in parent.Items)
        {
            node = parent.ItemContainerGenerator.ContainerFromItem(data) as TreeViewItem;
            if (data == item && node != null)
                break;
            node = FindItemNodeInChildren(node, item);
            if (node != null)
                break;
        }
        if (node == null && parent.IsExpanded != isExpanded)
            parent.IsExpanded = isExpanded;
        if (node != null)
            parent.IsExpanded = true;
        return node;
    }
} 
Farceuse answered 16/8, 2013 at 3:59 Comment(1)
It would be much faster if UpdateLayout() and IsExpanded is not called for some nodes. When it is not needed to call UpdateLayout() and IsExpanded? When tree item was visited previously. How to know that? ContainerFromItem() returns null for not visited nodes. So we can expand the parent node only when ContainerFromItem() returns null for children.Halfway
T
2

WPF MVVM TreeView SelectedItem

... is a better answer, but does not mention a way to get/set the SelectedItem in the ViewModel.

  1. Add a IsSelected boolean property to your ItemViewModel, and bind to it in a Style Setter for the TreeViewItem.
  2. Add a SelectedItem property to your ViewModel used as the DataContext for the TreeView. This is the missing piece in the solution above.
    ' ItemVM...
    Public Property IsSelected As Boolean
        Get
            Return _func.SelectedNode Is Me
        End Get
        Set(value As Boolean)
            If IsSelected  value Then
                _func.SelectedNode = If(value, Me, Nothing)
            End If
            RaisePropertyChange()
        End Set
    End Property
    ' TreeVM...
    Public Property SelectedItem As ItemVM
        Get
            Return _selectedItem
        End Get
        Set(value As ItemVM)
            If _selectedItem Is value Then
                Return
            End If
            Dim prev = _selectedItem
            _selectedItem = value
            If prev IsNot Nothing Then
                prev.IsSelected = False
            End If
            If _selectedItem IsNot Nothing Then
                _selectedItem.IsSelected = True
            End If
        End Set
    End Property
<TreeView ItemsSource="{Binding Path=TreeVM}" 
          BorderBrush="Transparent">
    <TreeView.ItemContainerStyle>
        <Style TargetType="TreeViewItem">
            <Setter Property="IsExpanded" Value="{Binding IsExpanded}"/>
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
        </Style>
    </TreeView.ItemContainerStyle>
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Children}">
            <TextBlock Text="{Binding Name}"/>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>
Tsarevitch answered 26/5, 2015 at 14:34 Comment(0)
N
1

After studying the Internet for a day I found my own solution for selecting an item after create a normal treeview in a normal WPF/C# environment

private void BuildSortTree(int sel)
        {
            MergeSort.Items.Clear();
            TreeViewItem itTemp = new TreeViewItem();
            itTemp.Header = SortList[0];
            MergeSort.Items.Add(itTemp);
            TreeViewItem prev;
            itTemp.IsExpanded = true;
            if (0 == sel) itTemp.IsSelected= true;
            prev = itTemp;
            for(int i = 1; i<SortList.Count; i++)
            {

                TreeViewItem itTempNEW = new TreeViewItem();
                itTempNEW.Header = SortList[i];
                prev.Items.Add(itTempNEW);
                itTempNEW.IsExpanded = true;
                if (i == sel) itTempNEW.IsSelected = true;
                prev = itTempNEW ;
            }
        }
Noblesse answered 19/4, 2013 at 14:38 Comment(0)
M
1

It can also be done using the IsSelected property of the TreeView item. Here's how I managed it,

public delegate void TreeviewItemSelectedHandler(TreeViewItem item);
public class TreeViewItem
{      
  public static event TreeviewItemSelectedHandler OnItemSelected = delegate { };
  public bool IsSelected 
  {
    get { return isSelected; }
    set 
    { 
      isSelected = value;
      if (value)
        OnItemSelected(this);
    }
  }
}

Then in the ViewModel that contains the data your TreeView is bound to, just subscribe to the event in the TreeViewItem class.

TreeViewItem.OnItemSelected += TreeViewItemSelected;

And finally, implement this handler in the same ViewModel,

private void TreeViewItemSelected(TreeViewItem item)
{
  //Do something
}

And the binding of course,

<Setter Property="IsSelected" Value="{Binding IsSelected}" />    
Marinelli answered 13/2, 2015 at 16:10 Comment(1)
This is actually an under-rated solution. By changing your way of thinking and binding the IsSelected property of each treeview element, and bubbling up the IsSelected events you get to use built in functionality which works well with two way binding. I've tried many proposed solutions to this problem, and this is the first that has worked. Just a little complex to wire up. Thanks.Teasley
H
1

I know this thread is 10 years old but the problem still exists....

The original question was 'to retrieve' the selected item. I also needed to "get" the selected item in my viewmodel (not set it). Of all the answers in this thread, the one by 'Wes' is the only one that approaches the problem differently: If you can use the 'Selected Item' as a target for databinding use it as a source for databinding. Wes did it to another view property, I will do it to a viewmodel property:

We need two things:

  • Create a dependency property in the viewmodel (in my case of type 'MyObject' as my treeview is bound to object of the 'MyObject' type)
  • Bind from the Treeview.SelectedItem to this property in the View's constructor (yes that is code behind but, it's likely that you will init your datacontext there as well)

Viewmodel:

public static readonly DependencyProperty SelectedTreeViewItemProperty = DependencyProperty.Register("SelectedTreeViewItem", typeof(MyObject), typeof(MyViewModel), new PropertyMetadata(OnSelectedTreeViewItemChanged));

    private static void OnSelectedTreeViewItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        (d as MyViewModel).OnSelectedTreeViewItemChanged(e);
    }

    private void OnSelectedTreeViewItemChanged(DependencyPropertyChangedEventArgs e)
    {
        //do your stuff here
    }

    public MyObject SelectedWorkOrderTreeViewItem
    {
        get { return (MyObject)GetValue(SelectedTreeViewItemProperty); }
        set { SetValue(SelectedTreeViewItemProperty, value); }
    }

View constructor:

Binding binding = new Binding("SelectedItem")
        {
            Source = treeView, //name of tree view in xaml
            Mode = BindingMode.OneWay
        };

        BindingOperations.SetBinding(DataContext, MyViewModel.SelectedTreeViewItemProperty, binding);
Himeji answered 5/7, 2019 at 11:30 Comment(0)
M
1

I propose this solution (which I consider the easiest and memory leaks free) which works perfectly for updating the ViewModel's selected item from the View's selected item.

Please note that changing the selected item from the ViewModel won't update the selected item of the View.

public class TreeViewEx : TreeView
{
    public static readonly DependencyProperty SelectedItemExProperty = DependencyProperty.Register("SelectedItemEx", typeof(object), typeof(TreeViewEx), new FrameworkPropertyMetadata(default(object))
    {
        BindsTwoWayByDefault = true // Required in order to avoid setting the "BindingMode" from the XAML
    });

    public object SelectedItemEx
    {
        get => GetValue(SelectedItemExProperty);
        set => SetValue(SelectedItemExProperty, value);
    }

    protected override void OnSelectedItemChanged(RoutedPropertyChangedEventArgs<object> e)
    {
        SelectedItemEx = e.NewValue;
    }
}

XAML usage

<l:TreeViewEx ItemsSource="{Binding Path=Items}" SelectedItemEx="{Binding Path=SelectedItem}" >
Mcclees answered 4/1, 2020 at 1:7 Comment(0)
P
0

(Let's just all agree that TreeView is obviously busted in respect to this problem. Binding to SelectedItem would have been obvious. Sigh)

I needed the solution to interact properly with the IsSelected property of TreeViewItem, so here's how I did it:

// the Type CustomThing needs to implement IsSelected with notification
// for this to work.
public class CustomTreeView : TreeView
{
    public CustomThing SelectedCustomThing
    {
        get
        {
            return (CustomThing)GetValue(SelectedNode_Property);
        }
        set
        {
            SetValue(SelectedNode_Property, value);
            if(value != null) value.IsSelected = true;
        }
    }

    public static DependencyProperty SelectedNode_Property =
        DependencyProperty.Register(
            "SelectedCustomThing",
            typeof(CustomThing),
            typeof(CustomTreeView),
            new FrameworkPropertyMetadata(
                null,
                FrameworkPropertyMetadataOptions.None,
                SelectedNodeChanged));

    public CustomTreeView(): base()
    {
        this.SelectedItemChanged += new RoutedPropertyChangedEventHandler<object>(SelectedItemChanged_CustomHandler);
    }

    void SelectedItemChanged_CustomHandler(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        SetValue(SelectedNode_Property, SelectedItem);
    }

    private static void SelectedNodeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var treeView = d as CustomTreeView;
        var newNode = e.NewValue as CustomThing;

        treeView.SelectedCustomThing = (CustomThing)e.NewValue;
    }
}

With this XAML:

<local:CustonTreeView ItemsSource="{Binding TreeRoot}" 
    SelectedCustomThing="{Binding SelectedNode,Mode=TwoWay}">
    <TreeView.ItemContainerStyle>
        <Style TargetType="TreeViewItem">
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
        </Style>
    </TreeView.ItemContainerStyle>
</local:CustonTreeView>
Picturize answered 15/2, 2018 at 2:36 Comment(0)
M
0

I bring you my solution which offers the following features:

  • Supports 2 ways binding

  • Auto updates the TreeViewItem.IsSelected properties (according to the SelectedItem)

  • No TreeView subclassing

  • Items bound to ViewModel can be of any type (even null)

1/ Paste the following code in your CS:

public class BindableSelectedItem
{
    public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.RegisterAttached(
        "SelectedItem", typeof(object), typeof(BindableSelectedItem), new PropertyMetadata(default(object), OnSelectedItemPropertyChangedCallback));

    private static void OnSelectedItemPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var treeView = d as TreeView;
        if (treeView != null)
        {
            BrowseTreeViewItems(treeView, tvi =>
            {
                tvi.IsSelected = tvi.DataContext == e.NewValue;
            });
        }
        else
        {
            throw new Exception("Attached property supports only TreeView");
        }
    }

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

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

    public static void BrowseTreeViewItems(TreeView treeView, Action<TreeViewItem> onBrowsedTreeViewItem)
    {
        var collectionsToVisit = new System.Collections.Generic.List<Tuple<ItemContainerGenerator, ItemCollection>> { new Tuple<ItemContainerGenerator, ItemCollection>(treeView.ItemContainerGenerator, treeView.Items) };
        var collectionIndex = 0;
        while (collectionIndex < collectionsToVisit.Count)
        {
            var itemContainerGenerator = collectionsToVisit[collectionIndex].Item1;
            var itemCollection = collectionsToVisit[collectionIndex].Item2;
            for (var i = 0; i < itemCollection.Count; i++)
            {
                var tvi = itemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
                if (tvi == null)
                {
                    continue;
                }

                if (tvi.ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
                {
                    collectionsToVisit.Add(new Tuple<ItemContainerGenerator, ItemCollection>(tvi.ItemContainerGenerator, tvi.Items));
                }

                onBrowsedTreeViewItem(tvi);
            }

            collectionIndex++;
        }
    }

}

2/ Example of use in your XAML file

<TreeView myNS:BindableSelectedItem.SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}" />  
Mcclees answered 24/5, 2018 at 14:27 Comment(0)
C
0

When clicking on some item list you'll get the data in "Selected" property.

ViewModel:

public class ShellViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void NotifyPropertyChanged(String info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }

    private ObservableCollection<Books> _books;

    private List<Books> bookList = new List<Books>();

    public ObservableCollection<Books> Books
    {
        get { return _books; }
        set { _books = value; NotifyPropertyChanged("Books"); }
    }

    private Books books;

    public Books Selected
    {
        get { return books; }
        set { books = value; }
    }


    public ShellViewModel()
    {
        bookList = new List<Books>()
        {
            new Books{BookName = "Harry Poter",Price ="15$"},
            new Books{BookName = "Harry Poter 2 ",Price ="14.95$"},
            new Books{BookName = "Harry Poter 3",Price ="18.50$"},
            new Books{BookName = "Harry Poter 4",Price ="32.90$"},
        };

        Books = new ObservableCollection<Books>(bookList);
    }
}

public class Books
{
    public string BookName { get; set; }
    public string Price { get; set; }
}

XAML:

 <ListView x:Name="lst" Grid.Row="2" ItemsSource="{Binding Books}" SelectedItem="{Binding Selected}">
        <ListView.View>
            <GridView >
                <GridViewColumn Header="Name" DisplayMemberBinding="{Binding BookName}"  />
                <GridViewColumn Header="Price" Width="100" DisplayMemberBinding="{Binding Price}"/>
            </GridView>
        </ListView.View>
    </ListView>
Codification answered 10/11, 2022 at 8:29 Comment(0)
C
-1

I realise it's been a while since this was posted, but FWIW I'm using Telerik's RadTreeView, and SelectedItem seems to work fine - either the problem has been fixed in the meantime, or Telerik have worked round it for us.

Christchurch answered 9/11, 2021 at 15:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.