Selection Manager For WPF/MVVM

Introduction

It is a quite common situation when the  UI shows a lot of different kinds of elements (text blocks, images, graphics, etc.) structured in different ways (lists, trees etc.), but only one of these elements could be selected at the same time.

In this article, I will try to create a class which will help to deal with selection. WPF is used here for the demo. However, this approach can be used in UWP, Xamarin, Windows Forms, and maybe some other technologies.
 
Source code can also be found on GitHub.

Interfaces

To be handled by Selection Manager, the object should implement ISelectableElement interface.

  1. /// <summary>  
  2. /// Classes must implement this interface to be handled by <see cref="SelectionManager"/>  
  3. /// <remarks>Property <see cref="Selected"/> have to fire PropertyChanged event./></remarks>  
  4. /// </summary>  
  5. public interface ISelectableElement: INotifyPropertyChanged  
  6. {  
  7.     /// <summary>  
  8.     /// Selection flag.  
  9.     /// </summary>  
  10.     bool Selected { getset; }  
  11. }  
SelectionManager implements ISelectionManager interface to be able to use DependancyInjection pattern.
  1. /// <summary>  
  2. /// Manages SelectedElement in hierarchical collection of elements (only one element selected at the particular moment).  
  3. /// </summary>  
  4. public interface ISelectionManager: INotifyPropertyChanged  
  5. {  
  6.     /// <summary>  
  7.     /// Gets and sets selected element  
  8.     /// </summary>  
  9.     ISelectableElement SelectedElement { getset; }  
  10.   
  11.     /// <summary>  
  12.     /// Adds collection of the objects to manager  
  13.     /// </summary>  
  14.     /// <param name="collection">The collection to be added</param>  
  15.     void AddCollection(INotifyCollectionChanged collection);  
  16.   
  17.     /// <summary>  
  18.     /// Removes collection of the objects from manager  
  19.     /// </summary>  
  20.     /// <param name="collection">The collection to be removed</param>  
  21.     void RemoveCollection(INotifyCollectionChanged collection);  
  22. }   
Helpers

PropertyHelper is used to get the property name.

  1. internal class PropertyHelper  
  2. {  
  3.     public static string GetPropertyName<T>(Expression<Func<T>> propertyLambda)  
  4.     {  
  5.         var me = propertyLambda.Body as MemberExpression;  
  6.   
  7.         if (me == null)  
  8.         {  
  9.             throw new ArgumentException(  
  10.                 "You must pass a lambda of the form: '() => Class.Property' or '() => object.Property'");  
  11.         }  
  12.   
  13.         return me.Member.Name;  
  14.     }  
  15. }  
ObservableCollection does not fire CollectionChanged with the list of removed (old) items after calling Clear(). It is possible to use ObservableCollection and not use Clear() method or use ObservableCollectionEx to be able to use Clear() method.
  1. /// <summary>  
  2. /// Works the same as <see cref="ObservableCollection{T}"/>.   
  3. /// Fires <see cref="ObservableCollection{T}.CollectionChanged"/> event with <see cref="NotifyCollectionChangedEventArgs.Action"/> equal to <see cref="NotifyCollectionChangedAction.Remove"/> after calling <see cref="Collection{T}.Clear"/> methods.  
  4. /// <see cref="ObservableCollection{T}"/> fires event with <see cref="NotifyCollectionChangedEventArgs.Action"/> equal to <see cref="NotifyCollectionChangedAction.Reset"/> and empty list of old items./>  
  5. /// </summary>  
  6. /// <typeparam name="T">The type of elements in the list.</typeparam>  
  7. public class ObservableCollectionEx<T> : ObservableCollection<T>  
  8. {  
  9.     /// <summary>  
  10.     /// Removes all items from the collection and fire CollectionChanged  
  11.     /// </summary>  
  12.     protected override void ClearItems()  
  13.     {  
  14.         var items = new List<T>(Items);  
  15.         base.ClearItems();  
  16.         OnCollectionChanged(  
  17.             new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, items));  
  18.     }  
  19. }   
SelectionManager

AddCollection adds all elements in the collection to the internal list and searchs for subelements using reflection (if some of the element properties implement ObservableCollection<> and elements of this collection implement ISelectableElement this collection will also be managed by SelectionManager).

RemoveCollection removes all elements and subelements from SelectionManager

SelectionManager will handle adding and removing subelements automatically. 

  1. public class SelectionManager : ISelectionManager  
  2. {  
  3.     public event PropertyChangedEventHandler PropertyChanged;  
  4.   
  5.     /// <summary>  
  6.     /// Gets and sets selected element  
  7.     /// </summary>  
  8.     public ISelectableElement SelectedElement  
  9.     {  
  10.         get  
  11.         {  
  12.             return _selectedElement;  
  13.         }  
  14.   
  15.         set  
  16.         {  
  17.             _selectedElement = value;  
  18.             OnPropertyChanged();  
  19.         }  
  20.     }  
  21.   
  22.     private ISelectableElement _selectedElement;  
  23.     private readonly List<ISelectableElement> _elements = new List<ISelectableElement>();  
  24.   
  25.     /// <summary>  
  26.     /// Adds collection of the objects to manager  
  27.     /// </summary>  
  28.     /// <param name="collection">The collection to be added</param>  
  29.     public void AddCollection(INotifyCollectionChanged collection)  
  30.     {  
  31.         collection.CollectionChanged += collection_CollectionChanged;  
  32.         foreach (var element in (ICollection)collection)  
  33.         {  
  34.             var selectableElement = element as ISelectableElement;  
  35.             if (selectableElement != null)  
  36.             {  
  37.                 AddElement(selectableElement);  
  38.             }  
  39.         }  
  40.     }  
  41.   
  42.     /// <summary>  
  43.     /// Removes collection of the objects from manager  
  44.     /// </summary>  
  45.     /// <param name="collection">The collection to be removed</param>  
  46.     public void RemoveCollection(INotifyCollectionChanged collection)  
  47.     {  
  48.         collection.CollectionChanged -= collection_CollectionChanged;  
  49.         foreach (var element in (ICollection)collection)  
  50.         {  
  51.             var selectableElement = element as ISelectableElement;  
  52.             if (selectableElement != null)  
  53.             {  
  54.                 RemoveElement(selectableElement);  
  55.             }  
  56.         }  
  57.     }  
  58.   
  59.     private void OnPropertyChanged([CallerMemberName] string property = null)  
  60.     {  
  61.         PropertyChanged?.Invoke(thisnew PropertyChangedEventArgs(property));  
  62.     }  
  63.   
  64.     private void AddElement(ISelectableElement element)  
  65.     {  
  66.         _elements.Add(element);  
  67.         element.PropertyChanged += element_PropertyChanged;  
  68.         AddSelectableElements(element);  
  69.         if (_elements.Any() && _elements.All(e => !e.Selected))  
  70.         {  
  71.             _elements[0].Selected = true;  
  72.         }  
  73.     }  
  74.   
  75.     private void RemoveElement(ISelectableElement element)  
  76.     {  
  77.         _elements.Remove(element);  
  78.         RemoveSelectableElements(element);  
  79.         element.PropertyChanged -= element_PropertyChanged;  
  80.   
  81.         if (SelectedElement == element)  
  82.         {  
  83.             SelectedElement = null;  
  84.             if (_elements.Count > 0)  
  85.             {  
  86.                 _elements[0].Selected = true;  
  87.             }  
  88.         }  
  89.     }  
  90.   
  91.     private void element_PropertyChanged(object sender, PropertyChangedEventArgs e)  
  92.     {  
  93.         var currentElement = (ISelectableElement)sender;  
  94.         if (e.PropertyName != PropertyHelper.GetPropertyName(() => currentElement.Selected))  
  95.         {  
  96.             return;  
  97.         }  
  98.   
  99.         if (currentElement.Selected)  
  100.         {  
  101.             foreach (var selectedElement in _elements  
  102.                 .Where(element => element != currentElement && element.Selected))  
  103.             {  
  104.                 selectedElement.Selected = false;  
  105.             }  
  106.   
  107.             SelectedElement = currentElement;  
  108.         }  
  109.         else  
  110.         {  
  111.             SelectedElement = null;  
  112.         }  
  113.     }  
  114.   
  115.     private void collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)  
  116.     {  
  117.         if (e.NewItems != null)  
  118.         {  
  119.             foreach (var item in e.NewItems)  
  120.             {  
  121.                 if (e.OldItems == null || !e.OldItems.Contains(item))  
  122.                 {  
  123.                     var element = item as ISelectableElement;  
  124.                     if (element != null)  
  125.                     {  
  126.                         AddElement(element);  
  127.                     }  
  128.                 }  
  129.             }  
  130.         }  
  131.   
  132.         if (e.OldItems != null)  
  133.         {  
  134.             foreach (var item in e.OldItems)  
  135.             {  
  136.                 if (e.NewItems == null || !e.NewItems.Contains(item))  
  137.                 {  
  138.                     var element = item as ISelectableElement;  
  139.                     if (element != null)  
  140.                     {  
  141.                         RemoveElement(element);  
  142.                     }  
  143.                 }  
  144.             }  
  145.         }  
  146.     }  
  147.   
  148.     private void AddSelectableElements(ISelectableElement rootElement)  
  149.     {  
  150.         foreach (var prop in rootElement.GetType().GetProperties().Where(IsPropertyObservable))  
  151.         {  
  152.             var value = (INotifyCollectionChanged)prop.GetValue(rootElement);  
  153.             AddCollection(value);  
  154.         }  
  155.     }  
  156.   
  157.     private void RemoveSelectableElements(ISelectableElement rootElement)  
  158.     {  
  159.         foreach (var prop in rootElement.GetType().GetProperties().Where(IsPropertyObservable))  
  160.         {  
  161.             var value = (INotifyCollectionChanged)prop.GetValue(rootElement);  
  162.             RemoveCollection(value);  
  163.         }  
  164.     }  
  165.   
  166.     private bool IsPropertyObservable(PropertyInfo prop)  
  167.     {  
  168.         if (!prop.PropertyType.IsGenericType)  
  169.         {  
  170.             return false;  
  171.         }  
  172.   
  173.         var observableCollectionType = GetObservableCollectionType(prop.PropertyType);  
  174.         if (observableCollectionType != null &&  
  175.             typeof(ISelectableElement).IsAssignableFrom(observableCollectionType.GenericTypeArguments[0]))  
  176.         {  
  177.             return true;  
  178.         }  
  179.   
  180.         return false;  
  181.     }  
  182.   
  183.     private Type GetObservableCollectionType(Type type)  
  184.     {  
  185.         if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ObservableCollection<>))  
  186.         {  
  187.             return type;  
  188.         }  
  189.   
  190.         if (type.BaseType == null)  
  191.         {  
  192.             return null;  
  193.         }  
  194.   
  195.         return GetObservableCollectionType(type.BaseType);  
  196.     }  
  197. }  
Tests

 
Demo

This demo application contains a list and a tree. Selection is managed by SelectionManager.

MVVM Light framework is used to make the code more compact and clear.

There are two types of the objects which support selection.

  1. class ListElementViewModel : ViewModelBase, ISelectableElement  
  2. {  
  3.     private string _description;  
  4.     public string Description  
  5.     {  
  6.         get { return _description; }  
  7.         set { Set(ref _description, value); }  
  8.     }  
  9.   
  10.     private bool _selected;  
  11.     public bool Selected  
  12.     {  
  13.         get { return _selected; }  
  14.         set { Set(ref _selected, value); }  
  15.     }  
  16. }  
  17.   
  18.   
  19. class HierarchicalElementViewModel: ViewModelBase, ISelectableElement  
  20. {  
  21.     private string _name;  
  22.     public string Name  
  23.     {  
  24.         get { return _name; }  
  25.         set { Set(ref _name, value); }  
  26.     }  
  27.   
  28.     public ObservableCollection<HierarchicalElementViewModel> Subitems { getset; }  
  29.   
  30.     private bool _selected;  
  31.     public bool Selected  
  32.     {  
  33.         get { return _selected; }  
  34.         set { Set(ref _selected, value); }  
  35.     }  
  36.   
  37.     public ICommand AddSubitemCommand { get; }  
  38.     public ICommand RemoveCommand { get; }  
  39.   
  40.     public HierarchicalElementViewModel ParentViewModel { get; }  
  41.   
  42.     public HierarchicalElementViewModel(HierarchicalElementViewModel parentViewModel)  
  43.     {  
  44.         ParentViewModel = parentViewModel;  
  45.         Subitems = new ObservableCollection<HierarchicalElementViewModel>();  
  46.         AddSubitemCommand = new RelayCommand(Add);  
  47.         RemoveCommand = new RelayCommand(Remove, () => ParentViewModel != null);  
  48.     }  
  49.   
  50.     private void Add()  
  51.     {  
  52.         Subitems.Add(new HierarchicalElementViewModel(this) { Name = "Child Element" });  
  53.     }  
  54.     private void Remove()  
  55.     {  
  56.         ParentViewModel.Subitems.Remove(this);  
  57.     }  
  58. }  
MainViewModel contains two collections of these elements.
  1. class MainViewModel : ViewModelBase  
  2. {  
  3.     public ObservableCollection<HierarchicalElementViewModel> HierarchicalElements { get; }  
  4.   
  5.     public ObservableCollection<ListElementViewModel> ListElements { get; }  
  6.   
  7.     public RelayCommand AddHierarchicalElementCommand { get; }  
  8.   
  9.     public RelayCommand RemoveHierarchicalElementCommand { get; }  
  10.   
  11.     public RelayCommand AddListElementCommand { get; }  
  12.   
  13.     public RelayCommand RemoveListElementCommand { get; }  
  14.   
  15.     public ISelectionManager Manager { get; }  
  16.   
  17.     public MainViewModel()  
  18.     {  
  19.         HierarchicalElements = new ObservableCollection<HierarchicalElementViewModel>();  
  20.         ListElements = new ObservableCollection<ListElementViewModel>();  
  21.         AddHierarchicalElementCommand = new RelayCommand(AddHierarchicalElement);  
  22.         RemoveHierarchicalElementCommand = new RelayCommand(  
  23.             RemoveHierarchicalElement,  
  24.             () => Manager.SelectedElement is HierarchicalElementViewModel);  
  25.         AddListElementCommand = new RelayCommand(AddListElement);  
  26.         RemoveListElementCommand = new RelayCommand(  
  27.             RemoveListElement,  
  28.             () => Manager.SelectedElement is ListElementViewModel);  
  29.         Manager = new SelectionManager.SelectionManager();  
  30.         Manager.PropertyChanged += ManagerOnPropertyChanged;  
  31.         Manager.AddCollection(HierarchicalElements);  
  32.         Manager.AddCollection(ListElements);  
  33.     }  
  34.   
  35.     private void AddHierarchicalElement()  
  36.     {  
  37.         var selectedHierarchicalElement = Manager.SelectedElement as HierarchicalElementViewModel;  
  38.         if (selectedHierarchicalElement != null)  
  39.         {  
  40.             var newItem = new HierarchicalElementViewModel(selectedHierarchicalElement) { Name = "Child Element" };  
  41.             selectedHierarchicalElement.Subitems.Add(newItem);  
  42.             newItem.Selected = true;  
  43.         }  
  44.         else  
  45.         {  
  46.             var newItem = new HierarchicalElementViewModel(null) { Name = "Root Element" };  
  47.             HierarchicalElements.Add(newItem);  
  48.             newItem.Selected = true;  
  49.         }  
  50.     }  
  51.   
  52.     private void RemoveHierarchicalElement()  
  53.     {  
  54.         var hierarchicalElement = Manager.SelectedElement as HierarchicalElementViewModel;  
  55.   
  56.         if (hierarchicalElement?.ParentViewModel != null)  
  57.         {  
  58.             hierarchicalElement.ParentViewModel.Subitems.Remove(hierarchicalElement);  
  59.         }  
  60.         else  
  61.         {  
  62.             HierarchicalElements.Remove(hierarchicalElement);  
  63.         }  
  64.     }  
  65.   
  66.     private void AddListElement()  
  67.     {  
  68.         var newItem = new ListElementViewModel { Description = "List Element" };  
  69.         ListElements.Add(newItem);  
  70.         newItem.Selected = true;  
  71.     }  
  72.   
  73.     private void RemoveListElement()  
  74.     {  
  75.         ListElements.Remove((ListElementViewModel)Manager.SelectedElement);  
  76.     }  
  77.     private void ManagerOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)  
  78.     {  
  79.         RemoveHierarchicalElementCommand.RaiseCanExecuteChanged();  
  80.         RemoveListElementCommand.RaiseCanExecuteChanged();  
  81.     }  
  82. }  
MainForm xaml code.
  1. <Window x:Class="SelectionManagerDemo.MainWindow"  
  2.         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
  3.         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
  4.         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"  
  5.         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"  
  6.         xmlns:viewModel="clr-namespace:SelectionManagerDemo.ViewModel"  
  7.         mc:Ignorable="d"  
  8.         Title="Selection Manager Demo"  
  9.         Height="350"  
  10.         Width="525"  
  11.         d:DataContext="{d:DesignInstance IsDesignTimeCreatable=False, d:Type=viewModel:MainViewModel}">  
  12.     <Grid>  
  13.         <Grid.Resources>  
  14.             <DataTemplate DataType="{x:Type viewModel:ListElementViewModel}">  
  15.                 <StackPanel Orientation="Horizontal">  
  16.                     <Ellipse Fill="AliceBlue"  
  17.                              Height="15"  
  18.                              Width="15"  
  19.                              Stroke="Blue"  
  20.                              StrokeThickness="2"  
  21.                              Margin="5"  
  22.                              VerticalAlignment="Center" />  
  23.                     <TextBlock Text="{Binding Description}"  
  24.                                VerticalAlignment="Center"  
  25.                                Margin="5" />  
  26.                 </StackPanel>  
  27.             </DataTemplate>  
  28.             <DataTemplate DataType="{x:Type viewModel:HierarchicalElementViewModel}">  
  29.                 <StackPanel Orientation="Horizontal">  
  30.                     <Polygon Points="0,0 15,0 15,15 0,15"  
  31.                              Stroke="Crimson"  
  32.                              StrokeThickness="2"  
  33.                              Margin="5"  
  34.                              VerticalAlignment="Center"  
  35.                              Fill="AliceBlue" />  
  36.                     <TextBlock Text="{Binding Name}"  
  37.                                VerticalAlignment="Center"  
  38.                                Margin="5" />  
  39.                 </StackPanel>  
  40.             </DataTemplate>  
  41.         </Grid.Resources>  
  42.         <Grid.ColumnDefinitions>  
  43.             <ColumnDefinition />  
  44.             <ColumnDefinition />  
  45.         </Grid.ColumnDefinitions>  
  46.         <Grid.RowDefinitions>  
  47.             <RowDefinition Height="Auto" />  
  48.             <RowDefinition Height="*" />  
  49.             <RowDefinition Height="Auto" />  
  50.         </Grid.RowDefinitions>  
  51.         <StackPanel Orientation="Horizontal">  
  52.             <TextBlock Text="List Elements"  
  53.                        VerticalAlignment="Center"  
  54.                        Margin="3" />  
  55.             <Button Content="+"  
  56.                     Margin="3"  
  57.                     Width="25"  
  58.                     Height="25"  
  59.                     Command="{Binding AddListElementCommand}" />  
  60.             <Button Content="-"  
  61.                     Margin="3"  
  62.                     Width="25"  
  63.                     Height="25"  
  64.                     Command="{Binding RemoveListElementCommand}" />  
  65.         </StackPanel>  
  66.         <StackPanel Grid.Row="0"  
  67.                     Grid.Column="1"  
  68.                     Orientation="Horizontal">  
  69.             <TextBlock Text="Hierarchical Elements"  
  70.                        VerticalAlignment="Center" />  
  71.             <Button Content="+"  
  72.                     Margin="3"  
  73.                     Width="25"  
  74.                     Height="25"  
  75.                     Command="{Binding AddHierarchicalElementCommand}" />  
  76.             <Button Content="-"  
  77.                     Margin="3"  
  78.                     Width="25"  
  79.                     Height="25"  
  80.                     Command="{Binding RemoveHierarchicalElementCommand}" />  
  81.         </StackPanel>  
  82.         <ListBox Grid.Row="1"  
  83.                  Grid.Column="0"  
  84.                  ItemsSource="{Binding ListElements}">  
  85.             <ListBox.ItemContainerStyle>  
  86.                 <Style TargetType="{x:Type ListBoxItem}"  
  87.                        d:DataContext="{d:DesignInstance viewModel:ListElementViewModel}">  
  88.                     <Setter Property="IsSelected"  
  89.                             Value="{Binding Selected, Mode=TwoWay}" />  
  90.                 </Style>  
  91.             </ListBox.ItemContainerStyle>  
  92.         </ListBox>  
  93.         <TreeView Grid.Row="1"  
  94.                   Grid.Column="1"  
  95.                   ItemsSource="{Binding HierarchicalElements}">  
  96.             <TreeView.ItemContainerStyle>  
  97.                 <Style TargetType="{x:Type TreeViewItem}"  
  98.                        d:DataContext="{d:DesignInstance viewModel:HierarchicalElementViewModel}">  
  99.                     <Setter Property="IsSelected"  
  100.                             Value="{Binding Selected, Mode=TwoWay}" />  
  101.                     <Setter Property="IsExpanded"  
  102.                             Value="True" />  
  103.                 </Style>  
  104.             </TreeView.ItemContainerStyle>  
  105.             <TreeView.ItemTemplate>  
  106.                 <HierarchicalDataTemplate ItemsSource="{Binding Subitems}">  
  107.                     <ContentPresenter Content="{Binding}" />  
  108.                 </HierarchicalDataTemplate>  
  109.             </TreeView.ItemTemplate>  
  110.         </TreeView>  
  111.         <StackPanel Grid.Row="2"  
  112.                     Grid.Column="0"  
  113.                     Grid.ColumnSpan="2"  
  114.                     Orientation="Horizontal">  
  115.             <TextBlock Text="Selected Element:"  
  116.                        Margin="5"  
  117.                        VerticalAlignment="Center" />  
  118.             <ContentPresenter Content="{Binding Manager.SelectedElement}"  
  119.                               VerticalAlignment="Center" />  
  120.         </StackPanel>  
  121.     </Grid>  
  122. </Window>  
Now, our demo looks like this.