Best loop to remove children with bottom-up deletion in a treeview? - c#

I have a TreeView organized as:
Level1
level 1.1
level 1.1.1
level 1.1.2
level 1.1.3
level 1.1.3.1
level 1.1.3.2
level 1.2
level 1.3
level2
level 2.1
.............
Each level is a ViewModel that inherits from TreeViewModelBase.
Given a viewmodel of say level 1.1.3.2, this code will remove it from the TreeView:
var y = SelectedItem as TreeViewModelBase;
var z = y.Parent;
z.Children.Remove(y);
if (z.Children.Count == 0)
{
var g = z.Parent;
g.Children.Remove(z);
}
What would be a single loop that starts from the selected item and remove it from its parent. If the parent no longer has any children, then remove the parent from its parent and likewise work up the tree structure.
What is the best looping construct to do this?
Thanks for any help.

Here's what I came up with:
an item model that can be constructed with a friendly syntax (no need to set its parent)
a pruning tool that will remove an item and cleanup the hierarchy as much as it can
Example
(pruning item 1.4.1.1)
before:
after:
Objects:
Item : your item
it watches for childrens you add to it and (un)sets its Parent property
provides Prune method which removes it from its parent, climbs up and repeat until not possible
Code:
internal class Item
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)] private ItemCollection _children;
public Item()
{
Children = new ItemCollection();
}
public Item Parent { get; private set; }
public ItemCollection Children
{
get { return _children; }
set
{
if (_children != null)
{
_children.CollectionChanged -= Children_CollectionChanged;
}
if (value != null)
{
value.CollectionChanged += Children_CollectionChanged;
// Notify about previously (never notified) added items
if (value.Count > 0)
{
value.RaiseCollectionChanged(
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, value));
}
}
_children = value;
}
}
public string Name { get; set; }
private void Children_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e != null)
{
if (e.NewItems != null)
{
IEnumerable<Item> newItems = e.NewItems.OfType<Item>();
foreach (Item item in newItems)
{
item.Parent = this;
}
}
if (e.OldItems != null)
{
IEnumerable<Item> oldItems = e.OldItems.OfType<Item>();
foreach (Item item in oldItems)
{
item.Parent = null;
}
}
}
}
public override string ToString()
{
return string.Format("{0}", Name);
}
public void Prune()
{
Item parent = Parent;
if (parent != null)
{
parent.Children.Remove(this);
while (parent.Children.Count <= 0)
{
Item grandParent = parent.Parent;
if (grandParent != null)
{
grandParent.Children.Remove(parent);
parent = grandParent;
}
}
}
}
}
ItemCollection :
a collection of items using ObservableCollection, very useful in alleviating the need to specify parent as you will see on the example below.
internal class ItemCollection : ObservableCollection<Item>
{
public ItemCollection()
{
}
public ItemCollection(IEnumerable<Item> items)
{
foreach (Item item in items)
{
Add(item);
}
}
internal void RaiseCollectionChanged(NotifyCollectionChangedEventArgs e)
{
OnCollectionChanged(e);
}
}
Demo code
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var root = new Item
{
Name = "root",
Children = new ItemCollection(new[]
{
new Item
{
Name = "item1.1",
Children = new ItemCollection(new[]
{
new Item {Name = "item1.1.1"},
new Item {Name = "item1.1.2"}
})
},
new Item
{
Name = "item1.2",
Children = new ItemCollection(new[]
{
new Item {Name = "item1.2.1"}
})
},
new Item {Name = "item1.3"}
})
};
var item1411 = new Item
{
Name = "item1.4.1.1"
};
var item141 = new Item
{
Name = "item1.4.1",
Children = new ItemCollection(new[]
{
item1411
})
};
var item14 = new Item
{
Name = "item1.4",
Children = new ItemCollection(new[]
{
item141
})
};
root.Children.Add(item14);
Console.WriteLine("-----------------");
Console.WriteLine("before pruning");
Console.WriteLine("-----------------");
PrintHierarchy(root);
Console.WriteLine("-----------------");
Console.WriteLine("after pruning");
Console.WriteLine("-----------------");
item1411.Prune();
PrintHierarchy(root);
DataContext = root;
}
private void PrintHierarchy(Item root, int level = 0)
{
Console.WriteLine("{0}{1}", string.Concat(Enumerable.Repeat(" ", level)), root);
if (root.Children.Count > 0)
{
foreach (Item child in root.Children)
{
PrintHierarchy(child, level + 1);
}
}
}
}
XAML:
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfApplication3="clr-namespace:WpfApplication3"
Title="MainWindow"
Width="525"
Height="350">
<Grid>
<Grid.Resources>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="True" />
</Style>
</Grid.Resources>
<TreeView ItemsSource="{Binding (wpfApplication3:Item.Children)}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="wpfApplication3:Item" ItemsSource="{Binding Children}">
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</Window>
EDIT
The system will obviously reflect changes in the tree such as if you do :
private void Button_Click(object sender, RoutedEventArgs e)
{
_item1411.Prune();
}
The TreeView will be updated seamlessly.

I concur with Aybe's answer. In my specific case where all the ViewModel's are held within ObservableCollections, and all derive from the same TreeViewModelBase, the below code seems much simplier. The TreeView will automatically be updated.
var z = SelectedItem as TreeViewModelBase;
TreeViewModelBase y;
do
{
y = z;
if (y.Parent == null) break;
z = y.Parent;
z.Children.Remove(y);
} while(z.Children.Count==0);

Related

Multi selection listbox with bindable selected items DP not binding back to source

I adapted some code from this answer (https://stackoverflow.com/a/51254960/3797778) in the hopes of adding SelectedValuePath functionality since the collection i want to store is actually a collection of property values contained in a collection of objects that the listbox is actually bound to. So basically i want to bind the selected items to a list of ID's rather than the complete objects.
My adapted code for the ListBox is as follows:
public class MultipleSelectionListBox : ListBox, INotifyPropertyChanged
{
public static readonly DependencyProperty BindableSelectedItemsProperty =
DependencyProperty.Register("BindableSelectedItems",
typeof(IEnumerable<dynamic>), typeof(MultipleSelectionListBox),
new FrameworkPropertyMetadata(default(IEnumerable<dynamic>),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));
public event PropertyChangedEventHandler PropertyChanged;
public IEnumerable<dynamic> BindableSelectedItems
{
get => (IEnumerable<dynamic>)GetValue(BindableSelectedItemsProperty);
set {
SetValue(BindableSelectedItemsProperty, value);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("BindableSelectedItems"));
}
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
BindableSelectedItems = SelectedItems.Cast<dynamic>();
}
private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MultipleSelectionListBox listBox)
{
List<dynamic> newSelection = new List<dynamic>();
if (!string.IsNullOrWhiteSpace(listBox.SelectedValuePath))
foreach (var item in listBox.BindableSelectedItems)
{
var collectionValue = item.GetType().GetProperty(listBox.SelectedValuePath).GetValue(item, null);
foreach (var lbItem in listBox.Items)
{
if (lbItem.GetType().GetProperty(listBox.SelectedValuePath).GetValue(lbItem, null) == collectionValue)
newSelection.Add(lbItem);
}
}
else
newSelection = listBox.BindableSelectedItems as List<dynamic>;
listBox.SetSelectedItems(listBox.BindableSelectedItems);
}
}
}
the rest you can probibly assume but i'll block out the basics anyway.
My objects in the LB:
public class DeviceChannelInfo
{
public DeviceStateInfo parentDeviceState { get; set; }
public string name { get; set; }
public string displayName { get; set; }
public int id { get; set; }
}
My LB code:
<uc:MultipleSelectionListBox ItemsSource="{Binding Source={x:Static local:SharedProperties.deviceChannelInfos}, Mode=OneWay}" SelectionMode="Extended" SelectedValuePath="name" IsSynchronizedWithCurrentItem="True" BindableSelectedItems="{Binding MyCollectionOfSelectedIDs, Mode=TwoWay}">
The binding never seems to communicate with with my 'MyCollectionOfSelectedIDs' prop.
Please refer to the following sample code.
public class MultipleSelectionListBox : ListBox
{
public static readonly DependencyProperty BindableSelectedItemsProperty =
DependencyProperty.Register("BindableSelectedItems",
typeof(IEnumerable<dynamic>), typeof(MultipleSelectionListBox),
new FrameworkPropertyMetadata(default(IEnumerable<dynamic>),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));
public IEnumerable<dynamic> BindableSelectedItems
{
get => (IEnumerable<dynamic>)GetValue(BindableSelectedItemsProperty);
set => SetValue(BindableSelectedItemsProperty, value);
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
BindableSelectedItems = SelectedItems.Cast<dynamic>();
}
private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MultipleSelectionListBox listBox)
{
List<dynamic> newSelection = new List<dynamic>();
if (!string.IsNullOrWhiteSpace(listBox.SelectedValuePath))
foreach (var item in listBox.BindableSelectedItems)
{
var collectionValue = item.GetType().GetProperty(listBox.SelectedValuePath).GetValue(item, null);
foreach (var lbItem in listBox.Items)
{
if (lbItem.GetType().GetProperty(listBox.SelectedValuePath).GetValue(lbItem, null) == collectionValue)
newSelection.Add(lbItem);
}
}
else
newSelection = listBox.BindableSelectedItems as List<dynamic>;
listBox.SetSelectedItems(listBox.BindableSelectedItems);
}
}
}
View Model:
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
//select 1 and 4 initially:
MyCollectionOfSelectedIDs = new List<dynamic> { Items[0], Items[3] };
}
public IList<DeviceChannelInfo> Items { get; } = new List<DeviceChannelInfo>()
{
new DeviceChannelInfo{ name = "1", displayName = "1", id =1 },
new DeviceChannelInfo{ name = "2", displayName = "2", id =2 },
new DeviceChannelInfo{ name = "3", displayName = "3", id =3 },
new DeviceChannelInfo{ name = "4", displayName = "4", id =4 }
};
private IEnumerable<dynamic> _mCollectionOfSelectedIDs;
public IEnumerable<dynamic> MyCollectionOfSelectedIDs
{
get { return _mCollectionOfSelectedIDs; }
set { _mCollectionOfSelectedIDs = value; NotifyPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
View:
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp4"
mc:Ignorable="d"
Title="MainWindow" Height="300" Width="300">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<local:MultipleSelectionListBox ItemsSource="{Binding Items}"
SelectionMode="Extended"
DisplayMemberPath="displayName"
SelectedValuePath="name"
BindableSelectedItems="{Binding MyCollectionOfSelectedIDs}" />
<TextBlock Grid.Row="1" Text="{Binding MyCollectionOfSelectedIDs.Count}" />
</Grid>
</Window>
I cracked this egg.
The most important thing to me was to avoid using behaviors, or code behind, and I believe I have accomplished this (with some likely needed testing but working so far)
public class MultipleSelectionListBox : ListBox
{
internal bool processSelectionChanges = false;
public static readonly DependencyProperty BindableSelectedItemsProperty =
DependencyProperty.Register("BindableSelectedItems",
typeof(object), typeof(MultipleSelectionListBox),
new FrameworkPropertyMetadata(default(ICollection<object>),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));
public dynamic BindableSelectedItems
{
get => GetValue(BindableSelectedItemsProperty);
set => SetValue(BindableSelectedItemsProperty, value);
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
if (BindableSelectedItems == null || !this.IsInitialized) return; //Handle pre initilized calls
if (e.AddedItems.Count > 0)
if (!string.IsNullOrWhiteSpace(SelectedValuePath))
{
foreach (var item in e.AddedItems)
if (!BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
BindableSelectedItems.Add((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
}
else
{
foreach (var item in e.AddedItems)
if (!BindableSelectedItems.Contains((dynamic)item))
BindableSelectedItems.Add((dynamic)item);
}
if (e.RemovedItems.Count > 0)
if (!string.IsNullOrWhiteSpace(SelectedValuePath))
{
foreach (var item in e.RemovedItems)
if (BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
BindableSelectedItems.Remove((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
}
else
{
foreach (var item in e.RemovedItems)
if (BindableSelectedItems.Contains((dynamic)item))
BindableSelectedItems.Remove((dynamic)item);
}
}
private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MultipleSelectionListBox listBox)
{
List<dynamic> newSelection = new List<dynamic>();
if (!string.IsNullOrWhiteSpace(listBox.SelectedValuePath))
foreach (var item in listBox.BindableSelectedItems)
{
foreach (var lbItem in listBox.Items)
{
var lbItemValue = lbItem.GetType().GetProperty(listBox.SelectedValuePath).GetValue(lbItem, null);
if ((dynamic)lbItemValue == (dynamic)item)
newSelection.Add(lbItem);
}
}
else
newSelection = listBox.BindableSelectedItems as List<dynamic>;
listBox.SetSelectedItems(newSelection);
}
}
}
I was originally swapping the property value that held the selecteditems and this is what was breaking my higher level bindings, but this control modifies the collection directly keeping all references intact. I have not fully tested two way binding yet, but it loads the correct values on initial load and raises all the proper collection changed events when edited in the listbox.

WPF TreeView MultiSelect Behavior

I have written a multi select behavior based on this post:WPF TreeView with Multiple Selection
However my behavior does't seem to work as expected..Can you please help?
The following is my xaml code:
<Window x:Class="TreeView.Spike.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Spike="clr-namespace:TreeView.Spike"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".5*"/>
<ColumnDefinition Width=".5*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="40"></RowDefinition>
</Grid.RowDefinitions>
<TreeView ItemsSource="{Binding Nodes}" Grid.Row="0" x:Name="treeView" Grid.Column="0">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Nodes}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}">
</TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="Add"></MenuItem>
<MenuItem Header="Delete"></MenuItem>
</ContextMenu>
</TreeView.ContextMenu>
<i:Interaction.Behaviors>
<Spike:MultipleItemSelectionAttachedBehavior AllSelectedItems="{Binding Path=AllSelectedNodes}"/>
</i:Interaction.Behaviors>
</TreeView>
</Grid>
</Window>
My attached behavior:
public class MultipleItemSelectionAttachedBehavior:Behavior<System.Windows.Controls.TreeView>
{
public static DependencyProperty AllSelectedItemsProperty =
DependencyProperty.RegisterAttached("AllSelectedItems", typeof(object), typeof(MultipleItemSelectionAttachedBehavior),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | FrameworkPropertyMetadataOptions.Inherits));
private static readonly PropertyInfo IsSelectionChangeActiveProperty = typeof(System.Windows.Controls.TreeView).GetProperty("IsSelectionChangeActive",
BindingFlags.NonPublic | BindingFlags.Instance);
public object AllSelectedItems
{
get
{
return (object)GetValue(AllSelectedItemsProperty);
}
set
{
SetValue(AllSelectedItemsProperty, value);
}
}
public static bool GetAllSelectedItems(DependencyObject obj)
{
return (bool)obj.GetValue(AllSelectedItemsProperty);
}
public static void SetAllSelectedItems(DependencyObject obj, bool value)
{
obj.SetValue(AllSelectedItemsProperty, value);
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectedItemChanged += AssociatedObject_SelectedItemChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.SelectedItemChanged -= AssociatedObject_SelectedItemChanged;
}
void AssociatedObject_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (IsSelectionChangeActiveProperty == null) return;
var selectedItems = new List<Node>();
var treeViewItem = AssociatedObject.SelectedItem as Node;
if (treeViewItem == null) return;
// allow multiple selection
// when control key is pressed
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{
var isSelectionChangeActive = IsSelectionChangeActiveProperty.GetValue(AssociatedObject, null);
IsSelectionChangeActiveProperty.SetValue(AssociatedObject, true, null);
selectedItems.ForEach(item => item.IsSelected = true);
IsSelectionChangeActiveProperty.SetValue(AssociatedObject, isSelectionChangeActive, null);
}
else
{
// deselect all selected items except the current one
selectedItems.ForEach(item => item.IsSelected = (item == treeViewItem));
selectedItems.Clear();
}
if (!selectedItems.Contains(treeViewItem))
{
selectedItems.Add(treeViewItem);
}
else
{
// deselect if already selected
treeViewItem.IsSelected = false;
selectedItems.Remove(treeViewItem);
}
AllSelectedItems = selectedItems;
}
}
..and my ViewModel
public class ViewModel :NotificationObject
{
public ViewModel()
{
AllSelectedNodes = new ObservableCollection<Node>();
}
private ObservableCollection<Node> _allSelectedNodes;
public ObservableCollection<Node> AllSelectedNodes
{
get { return _allSelectedNodes; }
set
{
_allSelectedNodes = value;
RaisePropertyChanged(() => AllSelectedNodes);
}
}
}
My Model:
public class Node:NotificationObject
{
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
RaisePropertyChanged(() => Name);
}
}
private bool _isExpanded = true;
public bool IsExpanded
{
get { return _isExpanded; }
set
{
_isExpanded = value;
RaisePropertyChanged(() => IsExpanded);
}
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
RaisePropertyChanged(() => IsSelected);
}
}
private ObservableCollection<Node> _nodes;
public ObservableCollection<Node> Nodes
{
get { return _nodes; }
set
{
_nodes = value;
RaisePropertyChanged(() => Nodes);
}
}
public static IList<Node> Create()
{
return new List<Node>()
{
new Node()
{
Name = "Activity",
Nodes = new ObservableCollection<Node>()
{
new Node() {Name = "Company",Nodes = new ObservableCollection<Node>(){ new Node() {Name = "Company1",Existing = false}}},
new Node() {Name = "Strategy",Nodes = new ObservableCollection<Node>(){ new Node() {Name = "Strategy1"}}},
new Node() {Name = "Vehicle",Nodes = new ObservableCollection<Node>(){ new Node() {Name = "Vehicle1",Existing = false}}}
}
}
};
}
}
..and my initialization clode:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var viewModel = new ViewModel();
this.DataContext = viewModel;
viewModel.Nodes = new ObservableCollection<Node>(Node.Create());
}
}
The issue is that, it still selects only one while it's supposed to select multiple while holding back control key.
In AssociatedObject_SelectedItemChanged, how is the selectedItems count ever anything but zero when you just instantiate it or 1 when you add the treeViewItem to it a bit later? Try setting it to the AllSelectedItems property. Also, shouldn't the AllSelectedItems property be a list or IEnumerable of some sort?
if(AllSelectedItems == null)
{
AllSelectedItems = new List<Node>();
}
List<Node> selectedItems = AllSelectedItems;
you are missing the binding of the IsSelected property:
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</TreeView.ItemContainerStyle>
you need to add Selection mode property to treeview like this
SelectionMode="Multiple"

Getting TreeViewItem for the selected item in a TreeView

I have a TreeView that is bound to a dataset which is having parent child relation. How i will get seleted TreeViewItem from the TreeView? Please help me. My code is below.
xaml:-
<TreeView Name="tvPersonal" Background="Transparent" ItemsSource="{Binding RootNodes}" SelectedItemChanged="tvPersonal_SelectedItemChanged">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding rsParentChild}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
public MainWindow()
{
InitializeComponent();
DataSet ds = new BL.BLMenu().GetAllMenues(new BOModule { Name = Modules.Personnel });
ds.Tables[0].TableName = "Menu";
DataRelation relation = new DataRelation("rsParentChild",
ds.Tables["Menu"].Columns["MenuId"],
ds.Tables["Menu"].Columns["ParentId"]);
relation.Nested = true;
ds.Relations.Add(relation);
BOMenu mnu = new BOMenu();
BOMenu.RootNodes = ds.Tables["Menu"].DefaultView;
BOMenu.RootNodes.RowFilter = "ParentId IS NULL";
this.DataContext = this;
stbiDate.Content = DateTime.Now;
}
public DataView RootNodes
{
get { return BOMenu.RootNodes; }
}
BOMenu Class :-
public class BOMenu
{
public string MenuId
{
get;
set;
}
public string Name
{
get;
set;
}
public string ParentId
{
get;
set;
}
public int Priority
{
get;
set;
}
public static DataView RootNodes
{
get;
set;
}
}
While biju's answer works for flat hierarchies, I had to look for a solution for HierarchicalDataTemplates. This is the extension method that worked for me:
public static TreeViewItem ContainerFromItemRecursive(this ItemContainerGenerator root, object item)
{
var treeViewItem = root.ContainerFromItem(item) as TreeViewItem;
if (treeViewItem != null)
return treeViewItem;
foreach (var subItem in root.Items)
{
treeViewItem = root.ContainerFromItem(subItem) as TreeViewItem;
var search = treeViewItem?.ItemContainerGenerator.ContainerFromItemRecursive(item);
if (search != null)
return search;
}
return null;
}
You can use it with
TreeViewItem tvi = treeView
.ItemContainerGenerator
.ContainerFromItemRecursive(treeView.SelectedItem);
Try
TreeViewItem tvi = myTree.ItemContainerGenerator.ContainerFromItem(SelectedItem) as TreeViewItem;
or go through the below links.Hope this helps
Data binding to SelectedItem in a WPF Treeview
Get SelectedItem from TreeView?
http://social.msdn.microsoft.com/forums/en-US/wpf/thread/36aca7f7-0b47-488b-8e16-840b86addfa3/
This works for trees whose data source is bound via a HierarchicalDataTemplate
Handle TreeViewItem.Selected
<TreeView Name="mTreeView" TreeViewItem.Selected="TreeViewItem_OnItemSelected" />
And set the TreeViewItem as the Tag.
private void TreeViewItem_OnItemSelected(object sender, RoutedEventArgs e)
{
mTreeView.Tag = e.OriginalSource;
}
Which you can retrieve later
TreeViewItem tvi = mTreeView.Tag as TreeViewItem;
The best solution I've found involves a simple helper method and can be used in virtually any of the TreeView's events (i.e., SelectedItemChanged, MouseLeftButtonUp, etc.).
TreeViewItem Item = TreeViewHelper.VisualUpwardSearch(e.OriginalSource as DependencyObject);
I am using multiple hierarchy data templates and this is the only method that worked for me. Now, I'm able to create a new control based on TreeView and can handle all events involving the selected item internally.
public static TreeViewItem VisualUpwardSearch(DependencyObject source)
{
while (source != null && !(source is TreeViewItem)) source = System.Windows.Media.VisualTreeHelper.GetParent(source);
return source as TreeViewItem;
}

Deleting in WPF TreeView jumps to parent

I tried the solution discussed here: WPF treeview itemselected moves incorrectly when deleting an item
However, I'm still seeing the selection jump to that parent when deleting an item. What am I doing wrong?
MainWindow.xaml
<Window x:Class="TreeViewDelete.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Tree View Delete sample" Height="350" Width="525">
<Window.Resources>
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding Selected, Mode=TwoWay}"/>
<EventSetter Event="KeyDown" Handler="OnTreeKeyDown"/>
</Style>
<HierarchicalDataTemplate x:Key="recursiveTemplate" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</HierarchicalDataTemplate>
</Window.Resources>
<Grid>
<TreeView Name="m_tree" ItemsSource="{Binding Children}" ItemTemplate="{StaticResource recursiveTemplate}"/>
</Grid>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
private Container m_root, m_child;
public MainWindow()
{
InitializeComponent();
m_root = new Container("Root");
m_child = new Container("main");
m_child.Add(new Container("k1"));
m_child.Add(new Container("k2"));
m_child.Add(new Container("k3"));
m_child.Add(new Container("k4"));
m_child.Add(new Container("k5"));
m_root.Add(m_child);
m_tree.DataContext = m_root;
}
private IEnumerable<T> GetVisualAncestorsOfType<T>(DependencyObject obj) where T : DependencyObject
{
for (; obj != null; obj = VisualTreeHelper.GetParent(obj))
if (obj is T)
yield return (T)obj;
}
private void OnTreeKeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Delete:
{
TreeViewItem item = sender as TreeViewItem;
if (item != null)
{
Container container = item.Header as Container;
if (container != null)
{
Container parent = container.Parent;
// Find the currently focused element in the TreeView's focus scope
DependencyObject focused =
FocusManager.GetFocusedElement(
FocusManager.GetFocusScope(m_tree)) as DependencyObject;
// Scan up the VisualTree to find the TreeViewItem for the parent
var parentContainer = (
from element in GetVisualAncestorsOfType<FrameworkElement>(focused)
where (element is TreeViewItem && element.DataContext == parent)
|| element is TreeView
select element
).FirstOrDefault();
parent.Remove(container);
if (parentContainer != null)
{
parent.Children[0].Selected = true;
parentContainer.Focus();
}
}
}
e.Handled = true;
}
break;
}
}
}
and finally Container.cs
public class Container : INotifyPropertyChanged
{
private bool m_selected;
private string m_name;
private ObservableCollection<Container> m_children;
private Container m_parent;
public Container(string name)
{
m_name = name;
m_children = new ObservableCollection<Container>();
m_parent = null;
m_selected = false;
}
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
get
{
return m_name;
}
set
{
m_name = value;
OnPropertyChanged("Name");
}
}
public Container Parent
{
get
{
return m_parent;
}
set
{
m_parent = value;
}
}
public bool Selected
{
get
{
return m_selected;
}
set
{
m_selected = value;
OnPropertyChanged("Selected");
}
}
public ObservableCollection<Container> Children
{
get
{
return m_children;
}
}
private void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public void Add(Container child)
{
m_children.Add(child);
child.Parent = this;
}
public void Remove(Container child)
{
m_children.Remove(child);
child.Parent = null;
}
}
I didn't check this myself but i think you can make this happen by using the CollectionChanged event of the ObservableCollections. here you can attach an event to your children collection so that when one of them is deleted you go and set the Selected property of for example the first child of the collection to true. something like the following code:
attach the event:
Children.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Children_CollectionChanged);
event implementation:
void Children_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
{
Children.First().Selected = true;
}
}
I tried the solution supplied here: "TreeView selected item when removing nodes" and in a simple application it works beautifully. Needless to say in the app I'm working on it doesn't...
Looking at what the article is talking about, it seems I need the TreeViewItem that is the container of my item. So I tried:
m_tree.ItemContainerGenerator.ContainerFromItem
Except this always returns null. Then I read the following: TreeView.ItemContainerGenerator.ContainerFromItem returns null, at which point my brain nearly blew a fuse!
It seems to get at the TreeViewItem I want to be selected, I have to start at the top of my hierarchy and work down the tree until I get to where I want. So, my container data has a Parent property, so I built a stack of objects:
Stack<Containerl> stack = new Stack<Container>();
Container toBeSelected = ... my object to be selected after deletion ...
while (toBeSelected != null)
{
stack.Push(toBeSelected);
toBeSelected = toBeSelected.Parent;
}
then I delete all the items from my hierarchy, then do the following:
TreeViewItem item = m_tree.ItemContainerGenerator.ContainerFromItem(stack.Pop()) as TreeViewItem;
while(item != null && (stack.Count > 0))
{
item = item.ItemContainerGenerator.ContainerFromItem(stack.Pop()) as TreeViewItem;
}
// Force this item to be selected, and set focus
item.IsSelected = true;
item.Focus();
It works!!!

Data binding to SelectedItem in a WPF Treeview

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;
}
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!
This property exists : TreeView.SelectedItem
But it is readonly, so you cannot assign it through a binding, only retrieve it
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>
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>
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>
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) {};
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.
You might also be able to use TreeViewItem.IsSelected property
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.
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.
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}">
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;
}
}
WPF MVVM TreeView SelectedItem
... is a better answer, but does not mention a way to get/set the SelectedItem in the ViewModel.
Add a IsSelected boolean property to your ItemViewModel, and bind to it in a Style Setter for the TreeViewItem.
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>
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 ;
}
}
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}" />
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);
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}" >
(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>
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}" />
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>
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.

Categories

Resources