This blog offers three ways to get around this problem. The first two are suitable for a one-off solution, the third is suitable for a solution that can be easily dropped into many pages.
Solution 1: Bind a regular list box to a list of objects that have an IsSelected property. Template the ListBoxItem to add a checkbox to each item. Use LINQ to return a list of Items where IsSelected is true.
Pros: Easy to implement once. Many users prefer the checkbox style
Cons: Difficult to implement many times. Not Windows standard.
Solution 2: Bind a regular list box to a list of objects that have an IsSelected property. Bind the ListBoxItem.IsSelected property to the bound object's IsSelected property. Use LINQ to return a list of Items where IsSelected is true.
Pros: Easy to implement once. Uses Windows standard for multi-select.
Cons: Difficult to implement many times. Some users struggle with the Windows standard.
Solution 3: Create a custom control based on ListBox that maintains and exposes a dependency property that mirrors the SelectedItems property.
Pros: Easy to reuse
Cons: More work up front.
I will implement solutions 1 and 2 at the same time, then solution 3 later. Start a new C# WPF project called MultiSelectListBox.
We will create a grid with three columns and put our first two solutions in columns 1 and 2. The XAML and code for MainWindow look like this.
<Window x:Class="MultiSelectListBox.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:local="clr-namespace:MultiSelectListBox"
mc:Ignorable="d"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="MainWindow" SizeToContent="WidthAndHeight">
<Window.Resources>
<RoutedCommand x:Key="Check1"/>
<RoutedCommand x:Key="Check2"/>
</Window.Resources>
<Window.CommandBindings>
<CommandBinding Command="{StaticResource Check1}" Executed="Check1_Executed"/>
<CommandBinding Command="{StaticResource Check2}" Executed="Check2_Executed"/>
</Window.CommandBindings>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="*"/>
<ColumnDefinition
Width="*"/>
<ColumnDefinition
Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition
Height="100"/>
<RowDefinition
Height="auto"/>
<RowDefinition
Height="auto"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" Grid.Column="0" ItemsSource="{Binding Statuses1}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsSelected}"/>
<TextBlock Text="{Binding StatusCode}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Grid.Row="1" Grid.Column="0" Content="What is checked
1?" Command="{StaticResource Check1}" Width="200"/>
<TextBox Grid.Row="2" Grid.Column="0" Text="{Binding CheckedList1}"/>
<ListBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding Statuses2}" SelectionMode="Extended">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding StatusCode}"/>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style
TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
<Button Grid.Row="1" Grid.Column="1" Content="What is checked
2?" Command="{StaticResource Check2}" Width="200"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding CheckedList2}"/>
</Grid>
</Window>
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
namespace MultiSelectListBox
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public class cStatus
{
public bool IsSelected { get; set; }
public string StatusCode { get; set; }
}
public List<cStatus> Statuses1 { get; set; }
public List<cStatus> Statuses2 { get; set; }
public string _CheckedList1;
public string CheckedList1
{
get
{ return _CheckedList1; }
set
{
_CheckedList1 = value;
NotifyPropertyChanged("CheckedList1");
}
}
public string _CheckedList2;
public string CheckedList2
{
get
{ return _CheckedList2; }
set
{
_CheckedList2 = value;
NotifyPropertyChanged("CheckedList2");
}
}
public MainWindow()
{
Statuses1 = new List<cStatus>()
{ new cStatus() {StatusCode = "OPEN" },
new cStatus() {StatusCode = "APPROVED" },
new cStatus() {StatusCode = "CLOSED" }
};
Statuses2 = new List<cStatus>()
{ new cStatus() {StatusCode = "OPEN" },
new cStatus() {StatusCode = "APPROVED" },
new cStatus() {StatusCode = "CLOSED" }
};
InitializeComponent();
}
private void Check1_Executed(object sender,
System.Windows.Input.ExecutedRoutedEventArgs e)
{
CheckedList1 = string.Join(",", Statuses1.Where((s) =>
s.IsSelected).Select((x) => x.StatusCode).ToArray());
}
private void Check2_Executed(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
{
CheckedList2 = string.Join(",", Statuses2.Where((s) =>
s.IsSelected).Select((x) => x.StatusCode).ToArray());
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(String info)
{
if
(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
}
Now let's add the third solution. We will add a new project to contain the custom listbox control called CustomControls, delete App and MainWindow and add a new user control called ExtendedListBox. Add a reference to the new project to MultiSelectListBox. The solution now looks like this.
Because the new control simply adds a dependency property to the standard ListBox the XAML is trivial.
<ListBox x:Class="WPFCustomControls.ExtendedListBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WPFCustomControls"
mc:Ignorable="d">
</ListBox>
using System.Collections;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace WPFCustomControls
{
public partial class ExtendedListBox : ListBox
{
public ExtendedListBox()
{
this.SelectionChanged
+= ExtendedListBox_SelectionChanged;
}
void
ExtendedListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var
SelectedItemsCasted = SelectedItems as
IList<object>;
if (SelectedItemsCasted == null) return;
foreach (object addedItem in e.AddedItems)
SelectedItemsList.Add(addedItem);
foreach (object removedItem in e.RemovedItems)
SelectedItemsList.Remove(removedItem);
SetValue(SelectedItemsListProperty,
SelectedItemsList);
}
public IList SelectedItemsList
{
get
{ return (IList)GetValue(SelectedItemsListProperty);
}
set
{ SetValue(SelectedItemsListProperty, value);
}
}
public static readonly DependencyProperty SelectedItemsListProperty = DependencyProperty.Register("SelectedItemsList", typeof(IList), typeof(ExtendedListBox));
}
}
Now we have our custom control, we need to to consume it in our WPF window. Return to MultiSelectListBox.xaml and add some XAML to define a new ListBox.
Add a reference to the custom controls project.
xmlns:custom="clr-namespace:WPFCustomControls;assembly=CustomControls"
Add these command bindings in the appropriate places.
<RoutedCommand x:Key="Check3"/>
<CommandBinding Command="{StaticResource Check3}" Executed="Check3_Executed"/>
Put this before the grid close tag </Grid>.
<custom:ExtendedListBox Grid.Row="0" Grid.Column="2" ItemsSource="{Binding Statuses3}" SelectedItemsList="{Binding SelectedStatusList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectionMode="Extended"/>
<Button Grid.Row="1" Grid.Column="2" Content="What is checked
3?" Command="{StaticResource Check3}" Width="200"/>
<TextBox Grid.Row="2" Grid.Column="2" Text="{Binding CheckedList3}"/>
As you can see, using the custom control requires far less XAML than the first two solutions.
We need to go to the code behind and add properties to bind to the ExtendedListBox. We don't need to store the IsSelected state in the source collection any more so we will bind the new ListBox to a list of string.
Add:
public List<String> Statuses3 { get; set; }
public string _CheckedList3;
public string CheckedList3
{
get
{ return _CheckedList3; }
set
{
_CheckedList3 = value;
NotifyPropertyChanged("CheckedList3");
}
}
We will bind the selected items to a new property.
private List<String> _SelectedStatusList;
public List<String> SelectedStatusList
{
get
{ return _SelectedStatusList; }
set
{
_SelectedStatusList = value;
NotifyPropertyChanged("SelectedStatusList");
}
}
Statuses3 = new List<String>() { "OPEN", "APPROVED", "CLOSED" };
SelectedStatusList = new List<String>();
private void Check3_Executed(object sender,
System.Windows.Input.ExecutedRoutedEventArgs e)
{
CheckedList3 = string.Join(",",
SelectedStatusList.ToArray());
}
If you run the project now you can see a third ListBox. The new ListBox functionality is the same as the second one as far as the user is concerned, but it's far easier to use from the developer's point of view.
No comments:
Post a Comment