Home > Software design >  WPF MVVM ListBox MultiSelect
WPF MVVM ListBox MultiSelect

Time:11-10

I Have created a list box containing list of items and i need to bind them on selection changed(Select and deselect).

ABCD.xalm

<ListBox Grid.Column="2" Grid.ColumnSpan="9" Height="30" Margin="0 0 5 0" Foreground="{StaticResource AcresTheme}" SelectedItem="{Binding Path=UpdateSimulationItem,UpdateSourceTrigger=PropertyChanged}"                               
                            ItemsSource="{Binding SmulationTypes, NotifyOnSourceUpdated=True}" 
                            Background="{Binding }"
                            MinHeight="65" SelectionMode="Multiple">
                        <ListBox.ItemTemplate>
                            <DataTemplate>
                                <CheckBox  Foreground="{StaticResource AcresTheme}"
                                           Content="{Binding Item}" 
                                           IsChecked="{Binding Path=IsSelected, Mode=TwoWay}"></CheckBox>
                            </DataTemplate>
                        </ListBox.ItemTemplate>
                        <ListBox.ItemContainerStyle>
                            <Style TargetType="{x:Type ListBoxItem}">
                                <Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/>
                            </Style>
                        </ListBox.ItemContainerStyle>
                    </ListBox>

ABCD.cs (View Model)

public List<string> SimulationTypesList { get; set; } = new List<string>();
    private ObservableCollection<SimulationType> _simulationTypes = new ObservableCollection<SimulationType>();

    public ObservableCollection<Items> SimulationTypes
    {
        get
        {
            return _simulationTypes;
        }
        set
        {
            _simulationTypes = value;
            OnPropertyChanged("SimulationTypes");
        }
    }

    private Items _updateSimulationItem;
    public Items UpdateSimulationItem
    {
        get
        {
            return _updateSimulationItem;
        }
        set
        {
          //Logic for getting the selected item
            _updateSimulationItem = value;
            OnPropertyChanged("UpdateSimulationItem");
        }
    }
public ABCD()
{
        SimulationTypes.Add(new SimulationType() { Item = "Simulation 1", IsSelected = false });
        SimulationTypes.Add(new SimulationType() { Item = "Simulation 2", IsSelected = false });
        SimulationTypes.Add(new SimulationType() { Item = "Simulation 3", IsSelected = false });
}

Items.cs

public class Items: ViewModelBase
{
    private string item;
    public string Item
    {
        get { return item; }
        set
        {
            item = value;
            this.OnPropertyChanged("Item");
        }
    } 
    
    private bool isSelected;
    public bool IsSelected
    {
        get { return isSelected; }
        set
        {
            isSelected = value;
            this.OnPropertyChanged("IsSelected");
        }
    }
}

I did try the solution given in https://stackoverflow.com/a/34632944/12020323 This worked fine for deleting a single item or selecting a single item.

When we select the second item it does not trigger the property change.

CodePudding user response:

Error somewhere not in this code. You may be confused about VM instances. This is often the case for beginners. There may be something wrong with the implementation of SimulationType. You didn't show it.

Here's a complete example of your code demonstrating that multiselect binding works correctly.

using Simplified;
using System;
using System.Collections.ObjectModel;

namespace Core2022.SO.ChaithanyaS
{
    public class Item : ViewModelBase
    {
        private string _title = string.Empty;
        public string Title
        {
            get => _title;
            set => Set(ref _title, value ?? string.Empty);
        }

        private bool _isSelected;
        public bool IsSelected
        {
            get => _isSelected;
            set => Set(ref _isSelected, value);
        }
    }

    public class SimulationType : Item
    {
        private int _count;
        public int Count { get => _count; set => Set(ref _count, value); }
    }

    public class ItemsViewModel : ViewModelBase
    {
        private static readonly Random random = new Random();
        private Item? _selectedItem;

        public ObservableCollection<Item> Items { get; } =
                    new ObservableCollection<Item>()
                    {
                new Item() {Title = "First" },
                new SimulationType() { Title = "Yield Simulation", Count = random.Next(5, 15) },
                new Item() {Title = "Second" },
                new SimulationType() { Title = "HLR Simulation", Count = random.Next(5, 15) },
                new SimulationType() { Title = "UnCorr HLR Simulation", Count = random.Next(5, 15)},
                new Item() {Title = "Third" }
                    };

        public Item? SelectedItem { get => _selectedItem; set => Set(ref _selectedItem, value); }
    }
}
<Window x:Class="Core2022.SO.ChaithanyaS.ItemsWindow"
        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:Core2022.SO.ChaithanyaS"
        mc:Ignorable="d"
        Title="ItemsWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:ItemsViewModel/>
    </Window.DataContext>
    <UniformGrid Columns="2">
        <ListBox Margin="10"
                 SelectedItem="{Binding SelectedItem}"
                 ItemsSource="{Binding Items}"
                 SelectionMode="Multiple">
            <FrameworkElement.Resources>
                <DataTemplate DataType="{x:Type local:Item}">
                    <CheckBox  Foreground="Red"
                               Content="{Binding Title}"
                               IsChecked="{Binding Path=IsSelected, Mode=TwoWay}"/>
                </DataTemplate>
                <DataTemplate DataType="{x:Type local:SimulationType}">
                    <CheckBox Foreground="Green"
                               IsChecked="{Binding Path=IsSelected, Mode=TwoWay}">
                        <TextBlock>
                            <TextBlock.Text>
                                <MultiBinding StringFormat="{}{0} ({1})">
                                    <Binding Path="Title"/>
                                    <Binding Path="Count"/>
                                </MultiBinding>
                            </TextBlock.Text>
                        </TextBlock>
                    </CheckBox>
                </DataTemplate>
            </FrameworkElement.Resources>
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/>
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>

        <ItemsControl ItemsSource="{Binding Items}" Margin="10"
                      BorderBrush="Green" BorderThickness="1"
                      Padding="10">
            <ItemsControl.ItemTemplate>
                <DataTemplate DataType="{x:Type local:Item}">
                    <DataTemplate.Resources>
                        <DataTemplate DataType="{x:Type local:Item}">
                            <TextBlock  Foreground="Red"
                               Text="{Binding Title}"/>
                        </DataTemplate>
                        <DataTemplate DataType="{x:Type local:SimulationType}">
                            <TextBlock Foreground="Green">
                                <TextBlock.Text>
                                    <MultiBinding StringFormat="{}{0} ({1})">
                                        <Binding Path="Title"/>
                                        <Binding Path="Count"/>
                                    </MultiBinding>
                                </TextBlock.Text>
                            </TextBlock>
                        </DataTemplate>
                    </DataTemplate.Resources>
                    <Label x:Name="cc" Content="{Binding}" Margin="1" BorderBrush="Gray" BorderThickness="1"/>
                    <DataTemplate.Triggers>
                        <DataTrigger Binding="{Binding IsSelected}" Value="True">
                            <Setter TargetName="cc" Property="Background" Value="LightPink"/>
                        </DataTrigger>
                    </DataTemplate.Triggers>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </UniformGrid>
</Window>

Perhaps you mean that in multi-selection mode, the SelectedItem property always has only the item that was selected first, until it is deselected?

Unfortunately, the implementation of such a task is not easy. It's better to make a custom AP property and then use it in XAML.

    public static class MuliSelectorHelper
    {
        /// <summary>Returns the value of the IsSelectedItemLast attached property for <paramref name="multiSelector"/>.</summary>
        /// <param name="multiSelector"><see cref="DependencyObject"/> whose property value will be returned.</param>
        /// <returns><see cref="bool"/> property value.</returns>
        public static bool GetIsSelectedItemLast(DependencyObject multiSelector)
        {
            return (bool)multiSelector.GetValue(IsSelectedItemLastProperty);
        }

        /// <summary>Sets the value of the IsSelectedItemLast attached property for <paramref name="multiSelector"/>.</summary>
        /// <param name="multiSelector"><see cref="MultiSelector"/> whose property value will be returned.</param>
        /// <param name="value"><see cref="bool"/> value for property.</param>
        public static void SetIsSelectedItemLast(DependencyObject multiSelector, bool value)
        {
            multiSelector.SetValue(IsSelectedItemLastProperty, value);
        }

        /// <summary><see cref="DependencyProperty"/> for methods <see cref="GetIsSelectedItemLast(MultiSelector)"/>
        /// and <see cref="SetIsSelectedItemLast(MultiSelector, bool)"/>.</summary>
        public static readonly DependencyProperty IsSelectedItemLastProperty =
            DependencyProperty.RegisterAttached(
                nameof(GetIsSelectedItemLast).Substring(3),
                typeof(bool),
                typeof(MuliSelectorHelper),
                new PropertyMetadata(false, OnIsSelectedItemLastChanged));

        private static void OnIsSelectedItemLastChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is not Selector selector || selector.GetValue(ListBox.SelectedItemsProperty) is not IList list)
            {
                throw new NotImplementedException("Implemented only types that derive from Selector and that use the ListBox.SelectedItems dependency property.");
            }

            if (Equals(e.NewValue, true))
            {
                selector.SelectionChanged  = OnSelectionChanged;
            }
            else
            {
                selector.SelectionChanged -= OnSelectionChanged;
            }
        }

        private static async void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (e.AddedItems.Count == 0)
                return;

            Selector selector = (Selector)sender;
            IList selectedItems = (IList)selector.GetValue(ListBox.SelectedItemsProperty);

            if (selectedItems.Count != e.AddedItems.Count && !Equals(selectedItems[0], e.AddedItems[0]))
            {
                selector.SelectionChanged -= OnSelectionChanged;

                int beginIndex = selectedItems.Count - e.AddedItems.Count;
                var selectedItemsArray = new object[selectedItems.Count];
                selectedItems.CopyTo(selectedItemsArray, 0);
                selectedItems.Clear();
                await selector.Dispatcher.BeginInvoke(() =>
                {
                    for (int i = selectedItemsArray.Length-1; i >= beginIndex; i--)
                    {
                        selectedItems.Add(selectedItemsArray[i]);
                    }
                    for (int i = 0; i < beginIndex; i  )
                    {
                        selectedItems.Add(selectedItemsArray[i]);
                    }

                });

                selector.SelectionChanged  = OnSelectionChanged;
            }

        }
    }
    <UniformGrid Columns="2">
        <ListBox Margin="10"
                 SelectedItem="{Binding SelectedItem}"
                 ItemsSource="{Binding Items}"
                 SelectionMode="Multiple"
                 local:MuliSelectorHelper.IsSelectedItemLast="true">
            <FrameworkElement.Resources>
                <DataTemplate DataType="{x:Type local:Item}">
                    <CheckBox  Foreground="Red"
                               Content="{Binding Title}"
                               IsChecked="{Binding Path=IsSelected, Mode=TwoWay}"/>
                </DataTemplate>
                <DataTemplate DataType="{x:Type local:SimulationType}">
                    <CheckBox Foreground="Green"
                               IsChecked="{Binding Path=IsSelected, Mode=TwoWay}">
                        <TextBlock>
                            <TextBlock.Text>
                                <MultiBinding StringFormat="{}{0} ({1})">
                                    <Binding Path="Title"/>
                                    <Binding Path="Count"/>
                                </MultiBinding>
                            </TextBlock.Text>
                        </TextBlock>
                    </CheckBox>
                </DataTemplate>
            </FrameworkElement.Resources>
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/>
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>

        <ContentControl Content="{Binding SelectedItem}">
            <ContentControl.ContentTemplate>
                <DataTemplate DataType="{x:Type local:Item}">
                    <DataTemplate.Resources>
                        <DataTemplate DataType="{x:Type local:Item}">
                            <TextBlock  Foreground="Red"
                               Text="{Binding Title}"/>
                        </DataTemplate>
                        <DataTemplate DataType="{x:Type local:SimulationType}">
                            <TextBlock Foreground="Green">
                                <TextBlock.Text>
                                    <MultiBinding StringFormat="{}{0} ({1})">
                                        <Binding Path="Title"/>
                                        <Binding Path="Count"/>
                                    </MultiBinding>
                                </TextBlock.Text>
                            </TextBlock>
                        </DataTemplate>
                    </DataTemplate.Resources>
                    <Label x:Name="cc" Content="{Binding}" Margin="1" BorderBrush="Gray" BorderThickness="1"/>
                    <DataTemplate.Triggers>
                        <DataTrigger Binding="{Binding IsSelected}" Value="True">
                            <Setter TargetName="cc" Property="Background" Value="LightPink"/>
                        </DataTrigger>
                    </DataTemplate.Triggers>
                </DataTemplate>
            </ContentControl.ContentTemplate>
        </ContentControl>
    </UniformGrid>

CodePudding user response:

Thank you @EldHasp, for the time taken to respond to my question. Very greatfull to the solution provided.

Currently I am intrested in having the complete code in ViewModel, I found the mistake that I had done in the ViewModel.

Old Code:

private ObservableCollection<SimulationType> _simulationTypes = new ObservableCollection<SimulationType>();

public ObservableCollection<Items> SimulationTypes
{
    get
    {
        return _simulationTypes;
    }
    set
    {
        _simulationTypes = value;
        OnPropertyChanged("SimulationTypes");
    }
}

private Items _updateSimulationItem;
public Items UpdateSimulationItem
{
    get
    {
        return _updateSimulationItem;
    }
    set
    {
        //Logic for getting the selected item
        _updateSimulationItem = value;
        OnPropertyChanged("UpdateSimulationItem");
    }
}

_updateSimulationItem = value; Binds the first selected item to UpdateSimulationItem and propertychange will trigger only when that perticular item is changed.

For example:

SimulationTypes.Add(new SimulationType() { Item = "Simulation 1", IsSelected = false }); SimulationTypes.Add(new SimulationType() { Item = "Simulation 2", IsSelected = false }); SimulationTypes.Add(new SimulationType() { Item = "Simulation 3", IsSelected = false });

In this three items, if I select Simulation 1 then the UpdateSimulationItem will bind to Simulation 1 and the propertychange will narrow down to one item i.e. Simulation 1. Now if we click on Simulation 2 the peopertychange will not trigger as the UpdateSimulationItem is bound to only Simulation 1 item changes.

The change that I Made.

Updated code:

private Items _updateSimulationItem;
public Items UpdateSimulationItem
{
    get
    {
        return _updateSimulationItem;
    }
    set
    {
      //Removed unnecessary code and the assignment of value to _updateSimulationItem
      OnPropertyChanged("UpdateSimulationItem");
    }
}

As we have binded the SimulationTypes to ItemSource in the ABC.XAML as shown below

<ListBox Foreground="{StaticResource AcresTheme}" 
         SelectedItem="{Binding Path=UpdateSimulationItem,UpdateSourceTrigger=PropertyChanged}" 
         ItemsSource="{Binding SimulationTypes, NotifyOnSourceUpdated=True}" 
         MinHeight="65" SelectionMode="Multiple">

when i click on the checkbox that is present in the view, it will automatically updat the SimulationTypes as i have bound the checkbox to IsSelected.

<CheckBox  Foreground="{StaticResource AcresTheme}"
           Content="{Binding Item}"
           IsChecked="{Binding Path=IsSelected, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">

@EldHasp the code changes that we have to do in your code is to remove the assignment to _selectedItem in the setter property and just keep the OnPropertychange(nameOf(SelectedItem)).

public Item? SelectedItem { get => _selectedItem; set => Set(ref _selectedItem, value); } The bold text was making the SelectedItem to bind to one item, which was restricting the trigger when other item was selected.

Once Again Thank you @EldHasp for taking out your time on this.

  • Related