Data Binding to Selecteditem in a Wpf Treeview

Data binding to SelectedItem in a WPF Treeview

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!

How to bind to SelectedItem property of WPF TreeView?

So, at least you're starting to get used to your daily MVVM-WTF... 'Why do I have to post on SO for stuff as basic as this'. One day, you'll love MVVM, I promise ;)

That being said: As you know, the TreeView doesn't support synchronizing the SelectedItem property. It does exist, though, but it is readonly. What you want to do, is to extend the behavior of the TreeView to synchronize it's selected item with a property on it's ViewModel.

This problem description points you in the right direction: Behaviors. Behaviors (or, to be precise, System.Windows.Interactivity.Behavior<>s) allow you to extend the functionality of any DependencyObject. (Good introduction)

An approach to synchronize your TreeView with a selected item via behaviors, can be found here:

SO Thread

This should do for you already. You can just copy and paste the code of Steve GreatRex and go for it. Please comment, if you need help with the approach. Have fun learning!

WPF MVVM TreeView SelectedItem

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));
}
}

Get Selected TreeViewItem Using MVVM

To do what you want you can modify the ItemContainerStyle of the TreeView:

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

Your view-model (the view-model for each item in the tree) then has to expose a boolean IsSelected property.

If you want to be able to control if a particular TreeViewItem is expanded you can use a setter for that property too:

<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>

Your view-model then has to expose a boolean IsExpanded property.

Note that these properties work both ways so if the user selects a node in the tree the IsSelected property of the view-model will be set to true. On the other hand if you set IsSelected to true on a view-model the node in the tree for that view-model will be selected. And likewise with expanded.

If you don't have a view-model for each item in the tree, well, then you should get one. Not having a view-model means that you are using your model objects as view-models, but for this to work these objects require an IsSelected property.

To expose an SelectedItem property on your parent view-model (the one you bind to the TreeView and that has a collection of child view-models) you can implement it like this:

public ChildViewModel SelectedItem {
get { return Items.FirstOrDefault(i => i.IsSelected); }
}

If you don't want to track selection on each individual item on the tree you can still use the SelectedItem property on the TreeView. However, to be able to do it "MVVM style" you need to use a Blend behavior (available as various NuGet packages - search for "blend interactivity").

Here I have added an EventTrigger that will invoke a command each time the selected item changes in the tree:

<TreeView x:Name="treeView">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<i:InvokeCommandAction
Command="{Binding SetSelectedItemCommand}"
CommandParameter="{Binding SelectedItem, ElementName=treeView}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeView>

You will have to add a property SetSelectedItemCommand on the DataContext of the TreeView returning an ICommand. When the selected item of the tree view changes the Execute method on the command is called with the selected item as the parameter. The easiest way to create a command is probably to use a DelegateCommand (google it to get an implementation as it is not part of WPF).

A perhaps better alternative that allows two-way binding without the clunky command is to use BindableSelectedItemBehavior provided by Steve Greatrex here on Stack Overflow.

SelectedItem in TreeView

Further to the commentary on your question, the WPF TreeView gives some unique challenges to the MVVM developer, and among these is detecting the currently selected item. For this you can use attached behaviour. To begin, write a static class to contain the behaviour...

   public static class TvBehaviour
{
#region TvSelectedItemChangedBehaviour (Attached DependencyProperty)
public static readonly DependencyProperty TvSelectedItemChangedBehaviourProperty =
DependencyProperty.RegisterAttached("TvSelectedItemChangedBehaviour",
typeof (ICommand),
typeof (TvBehaviour),
new PropertyMetadata(
OnTvSelectedItemChangedBehaviourChanged));

public static void SetTvSelectedItemChangedBehaviour(DependencyObject o, ICommand value)
{
o.SetValue(TvSelectedItemChangedBehaviourProperty, value);
}
public static ICommand GetTvSelectedItemChangedBehaviour(DependencyObject o)
{
return (ICommand) o.GetValue(TvSelectedItemChangedBehaviourProperty);
}
private static void OnTvSelectedItemChangedBehaviourChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
TreeView tv = d as TreeView;
if (tv != null)
{
tv.SelectedItemChanged += (s, a) =>
{
GetTvSelectedItemChangedBehaviour(tv).Execute(a.NewValue);
a.Handled = true;
};
}
}
#endregion

}

Then import the class's namespace into your Xaml (using xmlns). You can then declare a TreeView along these lines...

    <TreeView ItemsSource="{Binding MyList}" 
ItemTemplate="{StaticResource My_data_template}"
tvBinding:TvBehaviour.TvSelectedItemChangedBehaviour="{Binding
SelectedItemCommand}"
SelectedValuePath="Name"
>
</TreeView>

This 'wires' the TV behaviour to an ICommand in your VM. Finally, declare the ICommand in your VM...

public ICommand SelectedItemCommand { get; set; }

And initialize it...

 SelectedItemCommand = new RelayCommand(ExecuteSelectedItemCommand, 
CanExecuteSelectedItemCommand);

And then implement your delegates...

    private void ExecuteSelectedItemCommand(object obj)
{
// downcast 'obj' to get the instance of the selected item
}
private bool CanExecuteSelectedItemCommand(object obj)
{
return true;
}

When the user selects a TV item, your 'execute' delegate will get a boxed instance of the item, and you can unbox it and etc etc etc.

Note that the attached behaviour in this example assumes that the TV's lifetime is the same as the app, otherwise you have to unwire the attached behaviour. It also assumes that the TV ItemsSource is binding to something sensible.

That will solve the problem of getting the TV SelectedItem while remaining MVVM compliant (if such a thing as MVVM compliant exists).

The Relay Command class I used was taken from the linked article in MSDN. For reference purposes, here it is...

public class RelayCommand : ICommand
{ //http://msdn.microsoft.com/en-us/magazine/dd419663.aspx
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_execute(parameter);
}
private readonly Action<object> _execute;
private readonly Predicate<object> _canExecute;
}

Use the MSDN link above to get more info about this class.

Set selectedItem in WPF TreeView with DataContext bound to XDocument

After sleeping a weekend about this I found an answer myself. There were some more problems as only the return type.

The solution is, to get the matching ItemContainer for the Item. This Container is of type TreeViewItem, where I can set isSelected an the like.

As an example is most of the time better for understanding, I give one. The following function will search for a XML- node in a TreeView- object and return a TreeViewItem, that contains the matching Node. If no Match is found null is returned.
The matching node is expanded.
The XML is handled in a System.Xml.linq.XDocument which is bound to the System.Windows.Controls.TreeView DataContext.
When calling the function The TreeView is set as ic.

public static TreeViewItem SearchNodeInTreeView(ItemsControl ic, XElement NodeDescription, string indent = "")
{
TreeViewItem ret = null;
foreach (object o in ic.Items)
{
if (o is XElement)
{
var x = ic.ItemContainerGenerator.ContainerFromItem(o);
if (XNode.DeepEquals((o as XElement), NodeDescription))
{
ret = x as TreeViewItem;
}
else if ((o as XElement).HasElements){
if (x != null)
{
bool expanded = (x as TreeViewItem).IsExpanded;
(x as TreeViewItem).IsExpanded = true;
(x as TreeViewItem).UpdateLayout();
ret = SearchNodeInTreeView (x as TreeViewItem, NodeDescription, indent + " ");
if (ret == null)
{
(x as TreeViewItem).IsExpanded = expanded;
(x as TreeViewItem).UpdateLayout();
}
}
}
}
if (ret != null)
{
break;
}
}
return ret;
}

I hope I can help anybody with this.

TreeView SelectedItem Behavior - Two Way Binding does not work in One Direction

Just for the sake of the convenience, here's the final solution combined of the OP and ghrod's answer:

namespace MyPoject.Behaviors
{
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

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(
nameof(SelectedItem),
typeof(object),
typeof(BindableSelectedItemBehavior),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnSelectedItemChanged));

static void OnSelectedItemChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
var behavior = (BindableSelectedItemBehavior)sender;
var generator = behavior.AssociatedObject.ItemContainerGenerator;
if (generator.ContainerFromItem(e.NewValue) is TreeViewItem item)
item.SetValue(TreeViewItem.IsSelectedProperty, true);
}
#endregion

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

AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
}

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

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

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

Binding SelectedItem in a HierarchicalDataTemplate-applied WPF TreeView

Here is an improved version of the above mentioned attached behavior. It fully supports twoway binding and also works with HeriarchicalDataTemplate and TreeViews where its items are virtualized. Please note though that to find the 'TreeViewItem' that needs to be selected, it will realize (i.e. create) the virtualized TreeViewItems until it finds the right one. This could potentially be a performance problem with big virtualized trees.

/// <summary>
/// Behavior that makes the <see cref="System.Windows.Controls.TreeView.SelectedItem" /> bindable.
/// </summary>
public class BindableSelectedItemBehavior : Behavior<TreeView>
{
/// <summary>
/// Identifies the <see cref="SelectedItem" /> dependency property.
/// </summary>
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register(
"SelectedItem",
typeof(object),
typeof(BindableSelectedItemBehavior),
new UIPropertyMetadata(null, OnSelectedItemChanged));

/// <summary>
/// Gets or sets the selected item of the <see cref="TreeView" /> that this behavior is attached
/// to.
/// </summary>
public object SelectedItem
{
get
{
return this.GetValue(SelectedItemProperty);
}

set
{
this.SetValue(SelectedItemProperty, value);
}
}

/// <summary>
/// Called after the behavior is attached to an AssociatedObject.
/// </summary>
/// <remarks>
/// Override this to hook up functionality to the AssociatedObject.
/// </remarks>
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.SelectedItemChanged += this.OnTreeViewSelectedItemChanged;
}

/// <summary>
/// Called when the behavior is being detached from its AssociatedObject, but before it has
/// actually occurred.
/// </summary>
/// <remarks>
/// Override this to unhook functionality from the AssociatedObject.
/// </remarks>
protected override void OnDetaching()
{
base.OnDetaching();
if (this.AssociatedObject != null)
{
this.AssociatedObject.SelectedItemChanged -= this.OnTreeViewSelectedItemChanged;
}
}

private static Action<int> GetBringIndexIntoView(Panel itemsHostPanel)
{
var virtualizingPanel = itemsHostPanel as VirtualizingStackPanel;
if (virtualizingPanel == null)
{
return null;
}

var method = virtualizingPanel.GetType().GetMethod(
"BringIndexIntoView",
BindingFlags.Instance | BindingFlags.NonPublic,
Type.DefaultBinder,
new[] { typeof(int) },
null);
if (method == null)
{
return null;
}

return i => method.Invoke(virtualizingPanel, new object[] { i });
}

/// <summary>
/// Recursively search for an item in this subtree.
/// </summary>
/// <param name="container">
/// The parent ItemsControl. This can be a TreeView or a TreeViewItem.
/// </param>
/// <param name="item">
/// The item to search for.
/// </param>
/// <returns>
/// The TreeViewItem that contains the specified item.
/// </returns>
private static TreeViewItem GetTreeViewItem(ItemsControl container, object item)
{
if (container != null)
{
if (container.DataContext == item)
{
return container as TreeViewItem;
}

// Expand the current container
if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)
{
container.SetValue(TreeViewItem.IsExpandedProperty, true);
}

// Try to generate the ItemsPresenter and the ItemsPanel.
// by calling ApplyTemplate. Note that in the
// virtualizing case even if the item is marked
// expanded we still need to do this step in order to
// regenerate the visuals because they may have been virtualized away.
container.ApplyTemplate();
var itemsPresenter =
(ItemsPresenter)container.Template.FindName("ItemsHost", container);
if (itemsPresenter != null)
{
itemsPresenter.ApplyTemplate();
}
else
{
// The Tree template has not named the ItemsPresenter,
// so walk the descendents and find the child.
itemsPresenter = container.GetVisualDescendant<ItemsPresenter>();
if (itemsPresenter == null)
{
container.UpdateLayout();
itemsPresenter = container.GetVisualDescendant<ItemsPresenter>();
}
}

var itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);

// Ensure that the generator for this panel has been created.
#pragma warning disable 168
var children = itemsHostPanel.Children;
#pragma warning restore 168

var bringIndexIntoView = GetBringIndexIntoView(itemsHostPanel);
for (int i = 0, count = container.Items.Count; i < count; i++)
{
TreeViewItem subContainer;
if (bringIndexIntoView != null)
{
// Bring the item into view so
// that the container will be generated.
bringIndexIntoView(i);
subContainer =
(TreeViewItem)container.ItemContainerGenerator.
ContainerFromIndex(i);
}
else
{
subContainer =
(TreeViewItem)container.ItemContainerGenerator.
ContainerFromIndex(i);

// Bring the item into view to maintain the
// same behavior as with a virtualizing panel.
subContainer.BringIntoView();
}

if (subContainer == null)
{
continue;
}

// Search the next level for the object.
var resultContainer = GetTreeViewItem(subContainer, item);
if (resultContainer != null)
{
return resultContainer;
}

// The object is not under this TreeViewItem
// so collapse it.
subContainer.IsExpanded = false;
}
}

return null;
}

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

var behavior = (BindableSelectedItemBehavior)sender;
var treeView = behavior.AssociatedObject;
if (treeView == null)
{
// at designtime the AssociatedObject sometimes seems to be null
return;
}

item = GetTreeViewItem(treeView, e.NewValue);
if (item != null)
{
item.IsSelected = true;
}
}

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

And for the sake of completeness hier is the implementation of GetVisualDescentants:

/// <summary>
/// Extension methods for the <see cref="DependencyObject" /> type.
/// </summary>
public static class DependencyObjectExtensions
{
/// <summary>
/// Gets the first child of the specified visual that is of tyoe <typeparamref name="T" />
/// in the visual tree recursively.
/// </summary>
/// <param name="visual">The visual to get the visual children for.</param>
/// <returns>
/// The first child of the specified visual that is of tyoe <typeparamref name="T" /> of the
/// specified visual in the visual tree recursively or <c>null</c> if none was found.
/// </returns>
public static T GetVisualDescendant<T>(this DependencyObject visual) where T : DependencyObject
{
return (T)visual.GetVisualDescendants().FirstOrDefault(d => d is T);
}

/// <summary>
/// Gets all children of the specified visual in the visual tree recursively.
/// </summary>
/// <param name="visual">The visual to get the visual children for.</param>
/// <returns>All children of the specified visual in the visual tree recursively.</returns>
public static IEnumerable<DependencyObject> GetVisualDescendants(this DependencyObject visual)
{
if (visual == null)
{
yield break;
}

for (var i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
{
var child = VisualTreeHelper.GetChild(visual, i);
yield return child;
foreach (var subChild in GetVisualDescendants(child))
{
yield return subChild;
}
}
}
}


Related Topics



Leave a reply



Submit