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.
To be handled by Selection Manager, the object should implement ISelectableElement interface.
- /// <summary>
- /// Classes must implement this interface to be handled by <see cref="SelectionManager"/>
- /// <remarks>Property <see cref="Selected"/> have to fire PropertyChanged event./></remarks>
- /// </summary>
- public interface ISelectableElement: INotifyPropertyChanged
- {
- /// <summary>
- /// Selection flag.
- /// </summary>
- bool Selected { get; set; }
- }
- /// <summary>
- /// Manages SelectedElement in hierarchical collection of elements (only one element selected at the particular moment).
- /// </summary>
- public interface ISelectionManager: INotifyPropertyChanged
- {
- /// <summary>
- /// Gets and sets selected element
- /// </summary>
- ISelectableElement SelectedElement { get; set; }
- /// <summary>
- /// Adds collection of the objects to manager
- /// </summary>
- /// <param name="collection">The collection to be added</param>
- void AddCollection(INotifyCollectionChanged collection);
- /// <summary>
- /// Removes collection of the objects from manager
- /// </summary>
- /// <param name="collection">The collection to be removed</param>
- void RemoveCollection(INotifyCollectionChanged collection);
- }
PropertyHelper is used to get the property name.
- internal class PropertyHelper
- {
- public static string GetPropertyName<T>(Expression<Func<T>> propertyLambda)
- {
- var me = propertyLambda.Body as MemberExpression;
- if (me == null)
- {
- throw new ArgumentException(
- "You must pass a lambda of the form: '() => Class.Property' or '() => object.Property'");
- }
- return me.Member.Name;
- }
- }
- /// <summary>
- /// Works the same as <see cref="ObservableCollection{T}"/>.
- /// 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.
- /// <see cref="ObservableCollection{T}"/> fires event with <see cref="NotifyCollectionChangedEventArgs.Action"/> equal to <see cref="NotifyCollectionChangedAction.Reset"/> and empty list of old items./>
- /// </summary>
- /// <typeparam name="T">The type of elements in the list.</typeparam>
- public class ObservableCollectionEx<T> : ObservableCollection<T>
- {
- /// <summary>
- /// Removes all items from the collection and fire CollectionChanged
- /// </summary>
- protected override void ClearItems()
- {
- var items = new List<T>(Items);
- base.ClearItems();
- OnCollectionChanged(
- new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, items));
- }
- }
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.
- public class SelectionManager : ISelectionManager
- {
- public event PropertyChangedEventHandler PropertyChanged;
- /// <summary>
- /// Gets and sets selected element
- /// </summary>
- public ISelectableElement SelectedElement
- {
- get
- {
- return _selectedElement;
- }
- set
- {
- _selectedElement = value;
- OnPropertyChanged();
- }
- }
- private ISelectableElement _selectedElement;
- private readonly List<ISelectableElement> _elements = new List<ISelectableElement>();
- /// <summary>
- /// Adds collection of the objects to manager
- /// </summary>
- /// <param name="collection">The collection to be added</param>
- public void AddCollection(INotifyCollectionChanged collection)
- {
- collection.CollectionChanged += collection_CollectionChanged;
- foreach (var element in (ICollection)collection)
- {
- var selectableElement = element as ISelectableElement;
- if (selectableElement != null)
- {
- AddElement(selectableElement);
- }
- }
- }
- /// <summary>
- /// Removes collection of the objects from manager
- /// </summary>
- /// <param name="collection">The collection to be removed</param>
- public void RemoveCollection(INotifyCollectionChanged collection)
- {
- collection.CollectionChanged -= collection_CollectionChanged;
- foreach (var element in (ICollection)collection)
- {
- var selectableElement = element as ISelectableElement;
- if (selectableElement != null)
- {
- RemoveElement(selectableElement);
- }
- }
- }
- private void OnPropertyChanged([CallerMemberName] string property = null)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
- }
- private void AddElement(ISelectableElement element)
- {
- _elements.Add(element);
- element.PropertyChanged += element_PropertyChanged;
- AddSelectableElements(element);
- if (_elements.Any() && _elements.All(e => !e.Selected))
- {
- _elements[0].Selected = true;
- }
- }
- private void RemoveElement(ISelectableElement element)
- {
- _elements.Remove(element);
- RemoveSelectableElements(element);
- element.PropertyChanged -= element_PropertyChanged;
- if (SelectedElement == element)
- {
- SelectedElement = null;
- if (_elements.Count > 0)
- {
- _elements[0].Selected = true;
- }
- }
- }
- private void element_PropertyChanged(object sender, PropertyChangedEventArgs e)
- {
- var currentElement = (ISelectableElement)sender;
- if (e.PropertyName != PropertyHelper.GetPropertyName(() => currentElement.Selected))
- {
- return;
- }
- if (currentElement.Selected)
- {
- foreach (var selectedElement in _elements
- .Where(element => element != currentElement && element.Selected))
- {
- selectedElement.Selected = false;
- }
- SelectedElement = currentElement;
- }
- else
- {
- SelectedElement = null;
- }
- }
- private void collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
- {
- if (e.NewItems != null)
- {
- foreach (var item in e.NewItems)
- {
- if (e.OldItems == null || !e.OldItems.Contains(item))
- {
- var element = item as ISelectableElement;
- if (element != null)
- {
- AddElement(element);
- }
- }
- }
- }
- if (e.OldItems != null)
- {
- foreach (var item in e.OldItems)
- {
- if (e.NewItems == null || !e.NewItems.Contains(item))
- {
- var element = item as ISelectableElement;
- if (element != null)
- {
- RemoveElement(element);
- }
- }
- }
- }
- }
- private void AddSelectableElements(ISelectableElement rootElement)
- {
- foreach (var prop in rootElement.GetType().GetProperties().Where(IsPropertyObservable))
- {
- var value = (INotifyCollectionChanged)prop.GetValue(rootElement);
- AddCollection(value);
- }
- }
- private void RemoveSelectableElements(ISelectableElement rootElement)
- {
- foreach (var prop in rootElement.GetType().GetProperties().Where(IsPropertyObservable))
- {
- var value = (INotifyCollectionChanged)prop.GetValue(rootElement);
- RemoveCollection(value);
- }
- }
- private bool IsPropertyObservable(PropertyInfo prop)
- {
- if (!prop.PropertyType.IsGenericType)
- {
- return false;
- }
- var observableCollectionType = GetObservableCollectionType(prop.PropertyType);
- if (observableCollectionType != null &&
- typeof(ISelectableElement).IsAssignableFrom(observableCollectionType.GenericTypeArguments[0]))
- {
- return true;
- }
- return false;
- }
- private Type GetObservableCollectionType(Type type)
- {
- if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ObservableCollection<>))
- {
- return type;
- }
- if (type.BaseType == null)
- {
- return null;
- }
- return GetObservableCollectionType(type.BaseType);
- }
- }
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.
- class ListElementViewModel : ViewModelBase, ISelectableElement
- {
- private string _description;
- public string Description
- {
- get { return _description; }
- set { Set(ref _description, value); }
- }
- private bool _selected;
- public bool Selected
- {
- get { return _selected; }
- set { Set(ref _selected, value); }
- }
- }
- class HierarchicalElementViewModel: ViewModelBase, ISelectableElement
- {
- private string _name;
- public string Name
- {
- get { return _name; }
- set { Set(ref _name, value); }
- }
- public ObservableCollection<HierarchicalElementViewModel> Subitems { get; set; }
- private bool _selected;
- public bool Selected
- {
- get { return _selected; }
- set { Set(ref _selected, value); }
- }
- public ICommand AddSubitemCommand { get; }
- public ICommand RemoveCommand { get; }
- public HierarchicalElementViewModel ParentViewModel { get; }
- public HierarchicalElementViewModel(HierarchicalElementViewModel parentViewModel)
- {
- ParentViewModel = parentViewModel;
- Subitems = new ObservableCollection<HierarchicalElementViewModel>();
- AddSubitemCommand = new RelayCommand(Add);
- RemoveCommand = new RelayCommand(Remove, () => ParentViewModel != null);
- }
- private void Add()
- {
- Subitems.Add(new HierarchicalElementViewModel(this) { Name = "Child Element" });
- }
- private void Remove()
- {
- ParentViewModel.Subitems.Remove(this);
- }
- }
- class MainViewModel : ViewModelBase
- {
- public ObservableCollection<HierarchicalElementViewModel> HierarchicalElements { get; }
- public ObservableCollection<ListElementViewModel> ListElements { get; }
- public RelayCommand AddHierarchicalElementCommand { get; }
- public RelayCommand RemoveHierarchicalElementCommand { get; }
- public RelayCommand AddListElementCommand { get; }
- public RelayCommand RemoveListElementCommand { get; }
- public ISelectionManager Manager { get; }
- public MainViewModel()
- {
- HierarchicalElements = new ObservableCollection<HierarchicalElementViewModel>();
- ListElements = new ObservableCollection<ListElementViewModel>();
- AddHierarchicalElementCommand = new RelayCommand(AddHierarchicalElement);
- RemoveHierarchicalElementCommand = new RelayCommand(
- RemoveHierarchicalElement,
- () => Manager.SelectedElement is HierarchicalElementViewModel);
- AddListElementCommand = new RelayCommand(AddListElement);
- RemoveListElementCommand = new RelayCommand(
- RemoveListElement,
- () => Manager.SelectedElement is ListElementViewModel);
- Manager = new SelectionManager.SelectionManager();
- Manager.PropertyChanged += ManagerOnPropertyChanged;
- Manager.AddCollection(HierarchicalElements);
- Manager.AddCollection(ListElements);
- }
- private void AddHierarchicalElement()
- {
- var selectedHierarchicalElement = Manager.SelectedElement as HierarchicalElementViewModel;
- if (selectedHierarchicalElement != null)
- {
- var newItem = new HierarchicalElementViewModel(selectedHierarchicalElement) { Name = "Child Element" };
- selectedHierarchicalElement.Subitems.Add(newItem);
- newItem.Selected = true;
- }
- else
- {
- var newItem = new HierarchicalElementViewModel(null) { Name = "Root Element" };
- HierarchicalElements.Add(newItem);
- newItem.Selected = true;
- }
- }
- private void RemoveHierarchicalElement()
- {
- var hierarchicalElement = Manager.SelectedElement as HierarchicalElementViewModel;
- if (hierarchicalElement?.ParentViewModel != null)
- {
- hierarchicalElement.ParentViewModel.Subitems.Remove(hierarchicalElement);
- }
- else
- {
- HierarchicalElements.Remove(hierarchicalElement);
- }
- }
- private void AddListElement()
- {
- var newItem = new ListElementViewModel { Description = "List Element" };
- ListElements.Add(newItem);
- newItem.Selected = true;
- }
- private void RemoveListElement()
- {
- ListElements.Remove((ListElementViewModel)Manager.SelectedElement);
- }
- private void ManagerOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
- {
- RemoveHierarchicalElementCommand.RaiseCanExecuteChanged();
- RemoveListElementCommand.RaiseCanExecuteChanged();
- }
- }
- <Window x:Class="SelectionManagerDemo.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:viewModel="clr-namespace:SelectionManagerDemo.ViewModel"
- mc:Ignorable="d"
- Title="Selection Manager Demo"
- Height="350"
- Width="525"
- d:DataContext="{d:DesignInstance IsDesignTimeCreatable=False, d:Type=viewModel:MainViewModel}">
- <Grid>
- <Grid.Resources>
- <DataTemplate DataType="{x:Type viewModel:ListElementViewModel}">
- <StackPanel Orientation="Horizontal">
- <Ellipse Fill="AliceBlue"
- Height="15"
- Width="15"
- Stroke="Blue"
- StrokeThickness="2"
- Margin="5"
- VerticalAlignment="Center" />
- <TextBlock Text="{Binding Description}"
- VerticalAlignment="Center"
- Margin="5" />
- </StackPanel>
- </DataTemplate>
- <DataTemplate DataType="{x:Type viewModel:HierarchicalElementViewModel}">
- <StackPanel Orientation="Horizontal">
- <Polygon Points="0,0 15,0 15,15 0,15"
- Stroke="Crimson"
- StrokeThickness="2"
- Margin="5"
- VerticalAlignment="Center"
- Fill="AliceBlue" />
- <TextBlock Text="{Binding Name}"
- VerticalAlignment="Center"
- Margin="5" />
- </StackPanel>
- </DataTemplate>
- </Grid.Resources>
- <Grid.ColumnDefinitions>
- <ColumnDefinition />
- <ColumnDefinition />
- </Grid.ColumnDefinitions>
- <Grid.RowDefinitions>
- <RowDefinition Height="Auto" />
- <RowDefinition Height="*" />
- <RowDefinition Height="Auto" />
- </Grid.RowDefinitions>
- <StackPanel Orientation="Horizontal">
- <TextBlock Text="List Elements"
- VerticalAlignment="Center"
- Margin="3" />
- <Button Content="+"
- Margin="3"
- Width="25"
- Height="25"
- Command="{Binding AddListElementCommand}" />
- <Button Content="-"
- Margin="3"
- Width="25"
- Height="25"
- Command="{Binding RemoveListElementCommand}" />
- </StackPanel>
- <StackPanel Grid.Row="0"
- Grid.Column="1"
- Orientation="Horizontal">
- <TextBlock Text="Hierarchical Elements"
- VerticalAlignment="Center" />
- <Button Content="+"
- Margin="3"
- Width="25"
- Height="25"
- Command="{Binding AddHierarchicalElementCommand}" />
- <Button Content="-"
- Margin="3"
- Width="25"
- Height="25"
- Command="{Binding RemoveHierarchicalElementCommand}" />
- </StackPanel>
- <ListBox Grid.Row="1"
- Grid.Column="0"
- ItemsSource="{Binding ListElements}">
- <ListBox.ItemContainerStyle>
- <Style TargetType="{x:Type ListBoxItem}"
- d:DataContext="{d:DesignInstance viewModel:ListElementViewModel}">
- <Setter Property="IsSelected"
- Value="{Binding Selected, Mode=TwoWay}" />
- </Style>
- </ListBox.ItemContainerStyle>
- </ListBox>
- <TreeView Grid.Row="1"
- Grid.Column="1"
- ItemsSource="{Binding HierarchicalElements}">
- <TreeView.ItemContainerStyle>
- <Style TargetType="{x:Type TreeViewItem}"
- d:DataContext="{d:DesignInstance viewModel:HierarchicalElementViewModel}">
- <Setter Property="IsSelected"
- Value="{Binding Selected, Mode=TwoWay}" />
- <Setter Property="IsExpanded"
- Value="True" />
- </Style>
- </TreeView.ItemContainerStyle>
- <TreeView.ItemTemplate>
- <HierarchicalDataTemplate ItemsSource="{Binding Subitems}">
- <ContentPresenter Content="{Binding}" />
- </HierarchicalDataTemplate>
- </TreeView.ItemTemplate>
- </TreeView>
- <StackPanel Grid.Row="2"
- Grid.Column="0"
- Grid.ColumnSpan="2"
- Orientation="Horizontal">
- <TextBlock Text="Selected Element:"
- Margin="5"
- VerticalAlignment="Center" />
- <ContentPresenter Content="{Binding Manager.SelectedElement}"
- VerticalAlignment="Center" />
- </StackPanel>
- </Grid>
- </Window>


Join the conversation! Your thoughts help the community grow.