MVVM Multiselect Listbox

Although MVVM is a great pattern you have to learn to work with it somethings are hard to do until you get the hang of it.

One of these things is doing a Multiselect Listbox. My first idea Google it, of course. The best solution I found was this one by Marlon Grech. Marlon has a lot of good stuff about WPF in his blog and he definitely knows what he is talking about. This solution however became a little slow when I wanted to perform a Select All and Unselect All on the list.

I decided to implement a specialized list for this kind of situation. I call it a SelectionList and it is a list of SelectionItems. The idea is to have a collection of items that have a IsSelected property and a Item property that contains the real value you want. I think the code speaks for itself.

public class SelectionItem<T> : INotifyPropertyChanged
{
    #region Fields

        private bool isSelected;

        private T item;

        #endregion

    #region Properties

        public bool IsSelected
        {
            get { return isSelected; }
            set
            {
                if (value == isSelected) return;
                isSelected = value;
                OnPropertyChanged("IsSelected");
                OnSelectionChanged();
            }
        }

        public T Item
        {
            get { return item; }
            set
            {
                if (value.Equals(item)) return;
                item = value;
                OnPropertyChanged("Item");
            }
        }

        #endregion

    #region Events

        public event PropertyChangedEventHandler PropertyChanged;

        public event EventHandler SelectionChanged;

        #endregion

    #region ctor

        public SelectionItem(T item)
            : this(false, item)
        {
        }

        public SelectionItem(bool selected, T item)
        {
            this.isSelected = selected;
            this.item = item;
        }

        #endregion

    #region Event invokers

        private void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler changed = PropertyChanged;
            if (changed != null) changed(this, new PropertyChangedEventArgs(propertyName));
        }

        private void OnSelectionChanged()
        {
            EventHandler changed = SelectionChanged;
            if (changed != null) changed(this, EventArgs.Empty);
        }

        #endregion
}

 The SelectionItem class is what really makes things happen. It takes an ordinary class like an string and wraps it adding a IsSelected property. This is the class of the objects that will be bound to each ListItem of the ListBox.

public class SelectionList<T> : 
    ObservableCollection<SelectionItem<T>> where T : IComparable<T>
{
    #region Properties

        /// <summary>
        /// Returns the selected items in the list
        /// </summary>
        public IEnumerable<T> SelectedItems
        {
            get { return this.Where(x => x.IsSelected).Select(x => x.Item); }
        }

        /// <summary>
        /// Returns all the items in the SelectionList
        /// </summary>
        public IEnumerable<T> AllItems
        {
            get { return this.Select(x => x.Item); }
        }

        #endregion

    #region ctor

        public SelectionList(IEnumerable<T> col)
            : base(toSelectionItemEnumerable(col))
        {

        }

        #endregion

    #region Public methods

        /// <summary>
        /// Adds the item to the list
        /// </summary>
        /// <param name="item"></param>
        public void Add(T item)
        {
            int i = 0;
            foreach (T existingItem in AllItems)
            {
                if (item.CompareTo(existingItem) < 0) break;
                i++;
            }
            Insert(i, new SelectionItem<T>(item));
        }

        /// <summary>
        /// Checks if the item exists in the list
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public bool Contains(T item)
        {
            return AllItems.Contains(item);
        }

        /// <summary>
        /// Selects all the items in the list
        /// </summary>
        public void SelectAll()
        {
            foreach (SelectionItem<T> selectionItem in this)
            {
                selectionItem.IsSelected = true;
            }
        }

        /// <summary>
        /// Unselects all the items in the list
        /// </summary>
        public void UnselectAll()
        {
            foreach (SelectionItem<T> selectionItem in this)
            {
                selectionItem.IsSelected = false;
            }
        }

        #endregion

    #region Helper methods

        /// <summary>
        /// Creates an SelectionList from any IEnumerable
        /// </summary>
        /// <param name="items"></param>
        /// <returns></returns>
        private static IEnumerable<SelectionItem<T>> toSelectionItemEnumerable(IEnumerable<T> items)
        {
            List<SelectionItem<T>> list = new List<SelectionItem<T>>();
            foreach (T item in items)
            {
                SelectionItem<T> selectionItem = new SelectionItem<T>(item);
                list.Add(selectionItem);
            }
            return list;
        }

        #endregion
}

The SelectionList is basically an ObservableCollection of SelectionItem.

Now that you have a list of items that can be bound to each ListBoxItem I needed to figure out how to bind the IsSelected property of my items to the is IsSelected property of the ListBoxItem. I found the solution in

this post

in the MSDN Forums. You need to use this style:

<ListBox.ItemContainerStyle>
    <Style TargetType="{x:Type ListBoxItem}">
        <Setter Property="IsSelected" 
                Value="{Binding IsSelected}"/>
    </Style>
</ListBox.ItemContainerStyle>

Example:

Imagine you need a window with a list of sports from which you have to select the sports you like. In this window you need to be able to add items to the list and select and unselect all items at once. Here is the code for the window and the ViewModel:

<Window x:Class="MvvmChecklistBox.MultiSelectWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MvvmChecklistBox"
        Title="MultiSelectWindow" Height="300" Width="300">
    <Window.Resources>
        <local:MainWindowViewModel x:Key="viewModel"/>
    </Window.Resources>
    <StackPanel Margin="5" DataContext="{StaticResource viewModel}">
        <TextBlock>Sport:</TextBlock>
        <TextBox Name="textBoxNewSport"
                 Text="{Binding NewSport}"/>
        <Button Content="Add new sport"
                Command="{Binding AddCommand}"/>
        <ListBox Name="checkboxList"
                 ItemsSource="{Binding Sports}"
                 DisplayMemberPath="Item"
                 Margin="0,5" 
                 SelectionMode="Multiple">
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="IsSelected" 
                            Value="{Binding IsSelected}"/>
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>
        <Button Content="Select All"
                Margin="0,5"
                Command="{Binding SelectAllCommand}"/>
        <Button Content="Unselect All"
                Margin="0,5"
                Command="{Binding UnselectAllCommand}"/>
    </StackPanel>
</Window>
public class MainWindowViewModel : INotifyPropertyChanged
{
    #region Fields

        private ICommand _selectAllCommand;

        private ICommand _unselectAllCommand;

        private ICommand _addCommand;

        private string _newSport;

        #endregion

    #region Properties

        public SelectionList<string> Sports { get; set; }

        public string NewSport
        {
            get { return _newSport; }
            set
            {
                if (value == _newSport) return;
                _newSport = value;
                OnPropertyChanged("NewSport");
            }
        }

        #endregion

    #region Commands

        public ICommand SelectAllCommand
        {
            get
            {
                if (_selectAllCommand == null)
                {
                    _selectAllCommand = new RelayCommand(param => Sports.SelectAll());
                }
                return _selectAllCommand;
            }
        }

        public ICommand UnselectAllCommand
        {
            get
            {
                if (_unselectAllCommand == null)
                {
                    _unselectAllCommand = new RelayCommand(param => Sports.UnselectAll());
                }
                return _unselectAllCommand;
            }
        }

        public ICommand AddCommand
        {
            get
            {
                if (_addCommand == null)
                {
                    _addCommand = new RelayCommand(param =>
                    {
                        Sports.Add(NewSport);
                        NewSport = string.Empty;
                    });
                }
                return _addCommand;
            }
        }

        #endregion

    #region Events

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

    #region ctor

        public MainWindowViewModel()
        {
            string[] sports = { "Baseball", "Basketball", "Football", "Handball", "Soccer", "Volleyball" };
            Sports = new SelectionList<string>(sports);
        }

        #endregion

    #region Event invokers

        private void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler changed = PropertyChanged;
            if (changed != null) changed(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion
}

Here is the result:

MultiSelectWindow

The solution was really simple after I though of the SelectionList. I wanted however to do a little bit more. Instead of the simple Listbox I had to build a list of CheckBoxes. To good thing is that as far as the ViewModel is concerned we are all set, there's no need to change anything. All we need to change is the View (which is a Window in our case) using a DataTemple in the ListBox ItemTemplate to place a Checkbox instead of regular item.

<ListBox.ItemTemplate>
    <DataTemplate>
        <CheckBox IsChecked="{Binding IsSelected}" 
                  Content="{Binding Item}"/>
    </DataTemplate>
</ListBox.ItemTemplate>

Did it work? Kind of... It works but something weird happens because you can select all the checkboxes you want and then select the ListItem which gets highlighted. So now you have the checkbox selection and the item highlight selection and the two of them do not match. I don't want the ListBox item to get highlighted. Once again Google helped and I found a style to do just what I wanted in Alex Filo's blog. Here is the code:

<ListBox.ItemContainerStyle>
    <Style>
        <Setter Property="ListBoxItem.Background" Value="Transparent"/>
        <Setter Property="ListBoxItem.Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ListBoxItem}">
                    <Border x:Name="Bd" 
                    SnapsToDevicePixels="true" 
                    Background="{TemplateBinding Background}" 
                    BorderBrush="{TemplateBinding BorderBrush}" 
                    BorderThickness="{TemplateBinding BorderThickness}" 
                    Padding="{TemplateBinding Padding}">
                        <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                                  VerticalAlignment="{TemplateBinding VerticalContentAlignment}" 
                                  SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsSelected" Value="true">
                            <Setter Property="Background" TargetName="Bd" Value="Transparent" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ListBox.ItemContainerStyle>

CheckBoxListError

Now we're done! Here is the final code for the view:

<Window x:Class="MvvmChecklistBox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="300">
    <StackPanel Margin="5">
        <TextBlock>Sport:</TextBlock>
        <TextBox Name="textBoxNewSport"
                 Text="{Binding NewSport}"/>
        <Button Content="Add new sport"
                Command="{Binding AddCommand}"/>
        <ListBox Name="checkboxList"
                 ItemsSource="{Binding Sports}"
                 Margin="0,5">
            <ListBox.ItemContainerStyle>
                <Style>
                    <Setter Property="ListBoxItem.Background" Value="Transparent"/>
                    <Setter Property="ListBoxItem.Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                                <Border x:Name="Bd" 
                                SnapsToDevicePixels="true" 
                                Background="{TemplateBinding Background}" 
                                BorderBrush="{TemplateBinding BorderBrush}" 
                                BorderThickness="{TemplateBinding BorderThickness}" 
                                Padding="{TemplateBinding Padding}">
                                    <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}" 
                                              SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                                </Border>
                                <ControlTemplate.Triggers>
                                    <Trigger Property="IsSelected" Value="true">
                                        <Setter Property="Background" TargetName="Bd" Value="Transparent" />
                                    </Trigger>
                                </ControlTemplate.Triggers>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </ListBox.ItemContainerStyle>
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <CheckBox IsChecked="{Binding IsSelected}" 
                              Content="{Binding Item}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Button Content="Select All"
                Margin="0,5"
                Command="{Binding SelectAllCommand}"/>
        <Button Content="Unselect All"
                Margin="0,5"
                Command="{Binding UnselectAllCommand}"/>
    </StackPanel>
</Window>

I'm sure that someone has had this problem and the SelectionList seems like a straight forward solution. I haven't found anything like it but I'm sure I can't be the first to think of it. Well, I hope this helps you and if you find of think of a better solution please leave a comment.

blog comments powered by Disqus