Click here to Skip to main content
Click here to Skip to main content

Single Selection Across Multiple ItemsControls

, 26 Oct 2012 Apache
Rate this:
Please Sign up or sign in to vote.
How to implement a single selection set across as many ItemsControls as your app needs.

Introduction

In this article I will show you how to implement a single selection set across as many ItemsControls as your app needs.

Background 

I was building an app that has a calendar interface where each day contained a ListBox of items, and the backing collection for each day was contained in the ViewModel for that day. When I ran the app for the first time, I was appalled (though I shouldn't have been) that each ListBox manages its own selection. That meant that I was unable to deselect an item in one day by merely selecting an item in another day, although this was the behavior I wanted. 

After posting my question on another site, the only answers I received were to combine all of the items into a central collection and use the ListBox's IsSynchronizedWithCurrentItem property, but alas, this didn't work for me since my architecture wouldn't allow me to combine my collections.

I had to find another solution.  

Using the code 

The code provided is an extremely simple example that can and should be expanded upon. 

To use the code you should have a thorough understanding of most of the principle behind WPF, including:

  1. Data contexts 
  2. Binding 
  3. Data templates
  4. MVVM 

Points of Interest  

Essentially the SelectionManager, combined with ISelectable objects, provides an override of the ListBox's inherent selection functionality. What is nice about this implementation is that the items displayed in the ListBox don't have to be derived from DependencyObject, which saves a lot of unneeded overhead. 

Another key point is that this approach allows the developer to maintain MVVM practices. 

The first thing needed is a way for an object to indicate that it is selected. That is, it needs an IsSelected property. The best way to ensure that is to create an interface that the object must implement. 

public interface ISelectable
{
    bool IsSelected { get; set; }
} 

That should do. Next up, we should probably create a wrapper class for any types that we don't control (and therefore cannot explicitly make them implement ISelectable).

class Selectable<T> : INotifyPropertyChanged, ISelectable
{
    private bool _isSelected;
    public bool IsSelected
    {
        get { return _isSelected; }
        set
        {
            _isSelected = value;
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs("IsSelected"));
        }
    }
    public T Value { get; set; }
    public Selectable(T t)
    {
        Value = t;
    }
    public event PropertyChangedEventHandler PropertyChanged;
}

Notice here that INotifyPropertyChanged is also implemented. This is an important part since we want our interface to update visually when the selection (IsSelected property) changes.

We also need a manager that will handle which objects are selected.

public class SelectionManager
{
    private List<ISelectable> _selectedItems;
    public ISelectable SelectedItem
    {
        get { return _selectedItems.LastOrDefault(); }
        set { Select(value); }
    }
    public SelectionManager()
    {
        _selectedItems = new List<ISelectable>();
    }
    public void Select(ISelectable item, bool append = false)
    {
        if (!append)
        {
            _selectedItems.ForEach(i => i.IsSelected = false);
            _selectedItems.Clear();
        }
        if (item == null) return;
        item.IsSelected = true;
        _selectedItems.Add(item);
    }
} 

Simple yet effective. This affords us an easy way to change the selection of items by setting and clearing the IsSelected property for each item.

Almost done. We need to wire it into our UI. The first step is to implement a couple commands in our view model.

// this will be the content of our lists
public ObservableCollection<Selectable<int>> Numbers { get; set; }
public ObservableCollection<Selectable<DateTime>> Dates { get; set; }
// Need an instance of our SelectionManager
private SelectionManager _selectionManager = new SelectionManager();
public ICommand SelectItem
{
    get { return new SimpleDelegateCommand(i => SelectItemExecute(i)); }
}
public ICommand AppendSelectItem
{
    get { return new SimpleDelegateCommand(i => SelectItemExecute(i, true)); }
}
// Handles setting the selection.
private void SelectItemExecute(object obj, bool append = false)
{
    _selectionManager.Select((ISelectable) obj, append);
}   

(The SimpleDelegateCommand class can be found in MSDN's documentation .)

Finally, we'll need to bind to these commands in our DataTemplates.

<DataTemplate x:Key="SelectableTemplate" DataType="{x:Type SingleSelection:ISelectable}">
    <Border x:Name="Border" Background="Transparent">
        <Border.InputBindings>
            <MouseBinding Gesture="LeftClick" CommandParameter="{Binding}"
                          Command="{Binding RelativeSource={RelativeSource FindAncestor,
                                AncestorType=SingleSelection:MainWindow}, 
                                Path=DataContext.SelectItem}"/>
            <MouseBinding Gesture="Ctrl+LeftClick" CommandParameter="{Binding}"
                          Command="{Binding RelativeSource={RelativeSource FindAncestor,
                                AncestorType=SingleSelection:MainWindow}, 
                                Path=DataContext.AppendSelectItem}"/>
        </Border.InputBindings>
        <TextBlock x:Name="Content" Text="{Binding Value}"/>
    </Border>
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding IsSelected}" Value="True">
            <Setter TargetName="Border" Property="Background"
                    Value="{StaticResource {x:Static SystemColors.HighlightBrushKey}}"/>
            <Setter TargetName="Content" Property="Foreground"
                    Value="{StaticResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>  

Note that we also set triggers to show that the item is selected using the current system theme. This is done because the SelectedItem property of the ListBox will not be set by clicking on the item. In fact, we don't even use the SelectedItem property anymore simply because its scope is only defined within the ListBox.

Now we just need a window with a couple ListBoxes and some items.

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <ListBox ItemsSource="{Binding Numbers}" ItemTemplate="{StaticResource SelectableTemplate}"
             HorizontalContentAlignment="Stretch" />
    <ListBox Grid.Column="1" ItemsSource="{Binding Dates}"
             ItemTemplate="{StaticResource SelectableTemplate}"
             HorizontalContentAlignment="Stretch" />
</Grid>
public MainWindow()
{
    var vm = new MainWindowViewModel
    {
        Dates = new ObservableCollection<Selectable<DateTime>>
                                {
                                    new Selectable<DateTime>(DateTime.MinValue),
                                    new Selectable<DateTime>(DateTime.Today),
                                    new Selectable<DateTime>(DateTime.MaxValue),
                                },
        Numbers = new ObservableCollection<Selectable<int>>
                                {
                                    new Selectable<int>(1),
                                    new Selectable<int>(2),
                                    new Selectable<int>(3),
                                    new Selectable<int>(4),
                                    new Selectable<int>(5),
                                    new Selectable<int>(6),
                                    new Selectable<int>(7),
                                },
    };
    DataContext = vm;
    InitializeComponent();
}

That's it. Running the application, we can see that when an item is clicked in either list, the selection of the other list is removed. Also, by Ctrl-clicking, we can append the selection, just like the default selection behavior of the ListBox

History  

  • 2012/10/06 - Published article.
  • 2012/10/07 - Added code snippets.

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0

Share

About the Author

Greg Dennis
Software Developer
United States United States
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.141223.1 | Last Updated 26 Oct 2012
Article Copyright 2012 by Greg Dennis
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid