Hi,
In this tutorial, I will show how to implement a multi-checkbox WPF list that allows the selection of multiple items of any type. For this, will use the power of templates in WPF and generics of C#. We will also exploit the very useful “INotifyPropertyChanged” interface.
First, let’s create a WPF project called “TestMultiCheck”.
1- Create the “SelectionItem” class
Add a new class called “SelectionItem” that have to be generic. The code of the class is as follows:
/// <summary>
/// an item that will be selected in the multi check box
/// </summary>
/// <typeparam name="T"></typeparam>
public class SelectionItem<T> : INotifyPropertyChanged
{
#region privat fields
/// <summary>
/// indicates if the item is selected
/// </summary>
private bool _isSelected;
#endregion
public SelectionItem(T element, bool isSelected)
{
Element = element;
IsSelected = IsSelected;
}
public SelectionItem(T element):this(element,false)
{
}
#region public properties
/// <summary>
/// this UI-aware indicates if the element is selected or not
/// </summary>
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
_isSelected = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("IsSelected"));
}
}
/// <summary>
/// the element itself
/// </summary>
public T Element { get; set; }
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
The “SelectionItem” is a container for any element of the type “T”. The property IsSelected indicates if the item has been selected or not. Notice that the class implements the “INotifyPropertyChanged” interface to make the UI reacts if the selection is performed in code.
2- Create the “SelectionList” class
Let’s add another generic class called “SelectionList”
/// <summary>
/// a list that will include the elements that will be selected
/// </summary>
/// <typeparam name="T"></typeparam>
public class SelectionList<T> : List<SelectionItem<T>>, INotifyPropertyChanged
{
#region private fields
/// <summary>
/// the number of selected elements
/// </summary>
private int _selectionCount;
#endregion
#region private methods
/// <summary>
/// this events responds to the "IsSelectedEvent"
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var item = sender as SelectionItem<T>;
if ((item != null) && e.PropertyName == "IsSelected")
{
if (item.IsSelected)
SelectionCount = SelectionCount + 1;
else
SelectionCount = SelectionCount - 1;
}
}
#endregion
public SelectionList()
{ }
/// <summary>
/// creates the selection list from an existing simple list
/// </summary>
/// <param name="elements"></param>
public SelectionList(IEnumerable<T> elements)
{
foreach (T element in elements)
AddItem(element);
}
#region public methods
/// <summary>
/// adds an element to the element and listens to its "IsSelected" property to update the SelectionCount property
/// use this method insteand of the "Add" one
/// </summary>
/// <param name="element"></param>
public void AddItem(T element)
{
var item = new SelectionItem<T>(element);
item.PropertyChanged += item_PropertyChanged;
Add(item);
}
/// <summary>
/// gets the selected elements
/// </summary>
/// <returns></returns>
public IEnumerable<T> GetSelection()
{
return this.Where(e => e.IsSelected).Select(e => e.Element);
}
/// <summary>
/// uses linq expression to select a part of an object (for example, only id)
/// </summary>
/// <typeparam name="U"></typeparam>
/// <param name="expression"></param>
/// <returns></returns>
public IEnumerable<U> GetSelection<U>(Func<SelectionItem<T>, U> expression)
{
return this.Where(e => e.IsSelected).Select(expression);
}
#endregion
#region public properties
/// <summary>
/// the selection count property is ui-bindable, returns the number of selected elements
/// </summary>
public int SelectionCount
{
get { return _selectionCount; }
private set
{
_selectionCount = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("SelectionCount"));
}
}
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
This class can be instantiated empty and filled using the “Additem” method. You can alternatively, use the second overload directly to create the list from an existing enumerable. The property “SelectionCount” indicates the number of selected elements.
The first “GetSelection” methods returns all the selected elements. The second overload returns only a part using linq expressions. It could be very useful for database application to get directly the ids of the selected elements.
3- Create a Sample Entity
To demonstrate the two classes, let’s create a sample entity called course defined as follows :
public class Course
{
public Course()
{ }
public Course(int id, string name)
{
Id = id;
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
}
The tutorial will allow to select multiple courses
4- Create The XAML View
In the mainwindow view, add a stackpanel, two buttons and a list box. The first button will be used to select / unselect an element in the list.
The second button will be enabled only if at least one element is checked. Once clicked, it displays the names of the selected courses. The listbox will be converted using data templates to a list that displays a checkbox aside of the name of the course. For this, we will use the following data template :
<DataTemplate x:Key="ListBoxItemTemplate" >
<WrapPanel>
<CheckBox Focusable="False" IsChecked="{Binding IsSelected}" VerticalAlignment="Center" />
<ContentPresenter Content="{Binding Element.Name, Mode=OneTime}" Margin="2,0" />
</WrapPanel>
</DataTemplate>
The default behavior of the listbox is to display vertically its items. It is not the perfect behavior that we are looking for. It would be better to display the checkboxes side by side, for this, we will used an alternative ItemsPanelTempolate based on “WrapPanels” :
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
To make this behavior concrete, we have to deactivate the horizontal scrolling in the listbox. The full view xaml is like follows:
<Window x:Class="TestMultiCheck.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate x:Key="ListBoxItemTemplate" >
<WrapPanel>
<CheckBox Focusable="False" IsChecked="{Binding IsSelected}" VerticalAlignment="Center" />
<ContentPresenter Content="{Binding Element.Name, Mode=OneTime}" Margin="2,0" />
</WrapPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<Button Content="Select / Unselect Element By Code" Click="Button_Click" Margin="5"/>
<Button x:Name="Save" Content="Save Selection" IsEnabled="False" Margin="5" Click="Save_Click"/>
</StackPanel>
<ListBox x:Name="myList" Grid.Row="1" Margin="5" ItemTemplate="{DynamicResource ListBoxItemTemplate}" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Grid>
</Window>
5 – The Code Behind
The code behind is as follows:
public partial class MainWindow : Window
{
/// <summary>
/// the selection list
/// </summary>
private SelectionList<Course> _list;
/// <summary>
/// the list of courses
/// </summary>
Course[] courses = new Course[] {
new Course(1, "course1"),
new Course(2, "course2"),
new Course(3, "course3"),
new Course(4, "course4"),
new Course(5, "course5")
};
public MainWindow()
{
InitializeComponent();
// create the list
_list = new SelectionList<Course>(courses);
// assign the source of the list (not necessary in an MVVM approach)
myList.ItemsSource = _list;
// listen to the property changed event to enable or disable the save button
_list.PropertyChanged += list_PropertyChanged;
}
void list_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
// the save button is enabled if at least one item is checked
Save.IsEnabled = _list.SelectionCount > 0;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// this methods checks or unchecks programmatically an item to view the UI reaction
_list[1].IsSelected = !_list[1].IsSelected;
}
private void Save_Click(object sender, RoutedEventArgs e)
{
// get the names of the selected courses
IEnumerable<string> selection = _list.GetSelection(elt => elt.Element.Name);
// concantenate the strings
var text = string.Join(",", selection);
MessageBox.Show(text);
}
}
The window contains an array of courses called “courses” and a private selection list called “_list”.
At the page load event we create the list, and we assign an event to the “PropertyChanged” event. We assign the list as itemssource of the listbox. In MVVM approaches, this could be performed by using bindings or conventions.
The event “list_PropertyChanged” enabled or disables the “Save” button according to the number of the checked elements.
The event “Button_Click” checks “programmatically an element of the list to see that the view is effectively bound to the “IsSelected” property.
Finally, the “Save_Click” gets directly the names of the selected courses by using Linq expressions. Then, it displays them.
Enjoy !
The solution code can be downloaded here