Introduction
In this article I will show you how to implement a single selection set across as many ItemsControl
s 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:
- Data contexts
- Binding
- Data templates
- 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.
public ObservableCollection<Selectable<int>> Numbers { get; set; }
public ObservableCollection<Selectable<DateTime>> Dates { get; set; }
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)); }
}
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 ListBox
es 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.