Below is an update with a working example for getting the selected item for a TreeView.
I've separated the hierarchical collection from the data collection by using
CollectionViewSource
. Any changes to the collection data will be reflected in the hierarchical collection used by the view.
I have also included an example of how to sort the branches using a Converter class. It could also be done in the CollectionViewSource. If you want to use the latter, I'll leave that exercise up to you. ;)
Here is a base class to wrap
INotifyPropertyChanged
:
public abstract class ObservableBase : INotifyPropertyChanged
{
public void Set<TValue>(ref TValue field, TValue newValue, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<TValue>.Default.Equals(field, default(TValue)) || !field.Equals(newValue))
{
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
The data model:
public class EmployeeModel : ObservableBase
{
private int id;
public int Id
{
get => id;
set => Set(ref id, value);
}
private string name;
public string Name
{
get => name;
set => Set(ref name, value);
}
private string role;
public string Role
{
get => role;
set => Set(ref role, value);
}
private int managerId;
public int ManagerId
{
get => managerId;
set => Set(ref managerId, value);
}
}
A wrapper ViewModel for each data model. This handles the hierarchical collection and TreeViewItem selection :
public class EmployeeViewModel : ObservableBase
{
public EmployeeModel Employee { get; set; }
public CollectionViewSource Subordinates { get; set; }
private bool isSelected;
public bool IsSelected
{
get => isSelected;
set => Set(ref isSelected, value);
}
}
Now for the main view model for the view/window - binds the collection to the UI and handles the SelectedItem:
public class MainViewModel : ObservableBase
{
public MainViewModel()
{
MockData();
}
private EmployeeViewModel selectedEmployee;
public EmployeeViewModel SelectedEmployee
{
get => selectedEmployee;
set => Set(ref selectedEmployee, value);
}
private ObservableCollection<EmployeeViewModel> employeesData
= new ObservableCollection<EmployeeViewModel>();
public CollectionViewSource Employees { get; set; }
private void MockData()
{
employeesData.CollectionChanged += EmployyeeDataCollectionChanged;
employeesData.Add(new EmployeeViewModel
{
Employee = new EmployeeModel
{
Id = 1,
Name = "Bob"
}
});
employeesData.Add(new EmployeeViewModel
{
Employee = new EmployeeModel
{
Id = 2,
Name = "Paul",
ManagerId = 3
}
});
employeesData.Add(new EmployeeViewModel
{
Employee = new EmployeeModel
{
Id = 3,
Name = "Mary",
ManagerId = 1
}
});
employeesData.Add(new EmployeeViewModel
{
Employee = new EmployeeModel
{
Id = 4,
Name = "Joe",
ManagerId = 1
}
});
employeesData.Add(new EmployeeViewModel
{
Employee = new EmployeeModel
{
Id = 5,
Name = "Jane",
ManagerId = 2
}
});
Employees = new CollectionViewSource { Source = employeesData };
Employees.View.Filter = new Predicate<object>((o)
=> (o as EmployeeViewModel)?.Employee.ManagerId == 0);
foreach (var employee in employeesData)
{
employee.Subordinates = new CollectionViewSource
{ Source = employeesData };
employee.Subordinates.View.Filter = new Predicate<object>((o)
=> (o as EmployeeViewModel)?.Employee.ManagerId
== employee.Employee.Id);
}
}
private void EmployyeeDataCollectionChanged(object sender,
NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
for (int i = 0; i < e.NewItems.Count; i++)
{
var employee = e.NewItems[i] as EmployeeViewModel;
employee.PropertyChanged += EmployeePropertyChanged;
}
break;
case NotifyCollectionChangedAction.Remove:
for (int i = 0; i < e.OldItems.Count; i++)
{
var employee = e.NewItems[i] as EmployeeViewModel;
employee.PropertyChanged -= EmployeePropertyChanged;
}
break;
}
}
private void EmployeePropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(EmployeeViewModel.IsSelected))
{
SelectedEmployee = sender as EmployeeViewModel;
}
}
}
Note: We are listening to all the data ViewModels (
EmployeeViewModel
) to identify which item is being selected.
Now that the Data is ready, we can build & bind the UI:
<Window
x:Class="TreeViewSelectedItem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TreeViewSelectedItem"
Title="CodeProject - TREEVIEW SELECTED ITEM"
WindowStartupLocation="CenterScreen" Height="500" Width="300">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TreeView ItemsSource="{Binding Employees.View}">
<TreeView.Resources>
<local:TvBranchSortPropertyConverter x:Key="SortConverter"/>
<HierarchicalDataTemplate DataType="{x:Type local:EmployeeViewModel}"
ItemsSource="{Binding Subordinates.View,
Converter={StaticResource SortConverter},
ConverterParameter=Employee.Name}">
<TextBlock Text="{Binding Employee.Name}"
VerticalAlignment="Center"/>
</HierarchicalDataTemplate>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
<TextBlock Text="{Binding SelectedEmployee.Employee.Name, FallbackValue=None}"
Grid.Row="1" Margin="10"/>
</Grid>
</Window>
NOTE: As we are binding with a
CollectionViewSource
, we need to bind to it's
View
property.
Lastly, here is the converter for custom sorting of the TreeView nodes:
public class TvBranchSortPropertyConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var view = value as ListCollectionView;
view.SortDescriptions.Add(new SortDescription(parameter.ToString(), ListSortDirection.Ascending));
return view;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}