Bob
Bob

Reputation: 315

.Net MAUI: How to select single and multiple items using MVVM and CollectionView

Problem

I'm attempting a multi selection using CollectionView and MvvM. The (official docs don't do the greatest job of differentiating between normal code-behind and MVVM, and for us noobies that hurts.

I can get the single selection working, but making the leap to multiple selection is beyond me.

I will show my working code for single selection and discuss how to make it work for multiple selection. Maybe someone knows more than I?

Single Selection

Here's the working code for single selection:

Pass an ObservableCollection of type Person to a ModelView. Declare an instance of Person which will be the "selected object".

namespace Sandbox.ViewModel;

[QueryProperty("Persons", "Persons")]
public partial class SelectPageViewModel : ObservableObject
{
    [ObservableProperty]
    private ObservableCollection<Person> persons;

    [ObservableProperty]
    private Person selectedPerson;

    public SelectPageViewModel()
    {
        Persons = new();
    }
}

In the View, create a CollectionView and make some good guesses for its attributes:

<Grid>
    <Label Text="Select from List"/>

    <CollectionView ItemsSource="{Binding Persons}"
                    SelectionMode="Single"
                    SelectedItem="{Binding SelectedPerson}"
                    SelectionChangedCommand="{Binding SelectionChangedCommand}">
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="model:Person">
                <Grid>
                    <Label Text="{Binding Name}"/>
                </Grid>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</Grid>

Back in the ViewModel, the SelectionChanged command: if the user is satisfied with their choice of SelectedPerson, I pass it back to the page from whence it came, otherwise I null the selection and return:

[RelayCommand]
private async Task SelectionChanged() 
{
    bool keepSelection = await App.Current.MainPage.DisplayAlert(SelectedPerson.Name, "Keep this selection?", "Yes", "No");
    if (keepSelection)
    {
        Dictionary<string, object> throwParam = new()
        {
            { "SelectedPerson", SelectedPerson }
        };
        await Shell.Current.GoToAsync("..", throwParam);
    }

    // else clear the selection and return
    SelectedPerson = null;
    return;
}

Multiple Selection

After much wrestling, here is working code. Something very important: note the type of the ObservableCollection that is used to in the binding to the collection (hint, it's Object).

Another Edit (my current code)

My current code is the same as the above code, but I will show both ViewModel and View in total, plus screenshots of the List that's supposed to be populated.

ViewModel:

namespace Sandbox.ViewModel;

[QueryProperty("Persons","Persons")]
public partial class SelectPageViewModel : ObservableObject
{
    [ObservableProperty]
    private ObservableCollection<Person> persons;

    [ObservableProperty]
    private ObservableCollection<Object> selectedPersons;

    [ObservableProperty]
    private Person selectedPerson;

    public SelectPageViewModel()
    {
        Persons = new();
        SelectedPersons = new();
    }


    [RelayCommand]
    private void SelectionChanged()
    {
// every time something is selected, the object is added to SelectedPersons automagically.
        int a = SelectedPersons.Count; // will +1 every time
    }
}

View:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:viewmodel="clr-namespace:Sandbox.ViewModel"
             xmlns:model="clr-namespace:Sandbox.Model"
             x:DataType="viewmodel:SelectPageViewModel"
             x:Class="Sandbox.View.SelectPage"
             Title="SelectPage">

    <Grid RowDefinitions="Auto,Auto" Padding="10">
        <Label Grid.Row="0"
               Text="Select from List"
               FontSize="Large"
               FontAttributes="Bold" />

        <CollectionView Grid.Row="1"
                        ItemsSource="{Binding Persons}"                    
                        SelectionMode="Multiple"
                        SelectedItems="{Binding SelectedPersons, Mode=TwoWay}"
                        SelectionChangedCommand="{Binding SelectionChangedCommand}">
            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="model:Person">
                    <Grid Padding="10">
                        <Label Text="{Binding Name}"
                               FontSize="Medium" />
                    </Grid>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </Grid>
</ContentPage>

Upvotes: 2

Views: 3625

Answers (2)

Stephen Quan
Stephen Quan

Reputation: 25956

https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/collectionview/selection states:

  • SelectedItems, of type IList<object>, the selected items in the list. This property has a default binding mode of OneWay, and has a null value when no items are selected.
  • SelectionChangedCommand, of type ICommand, which is executed when the selected item changes.

https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.collectionview?view=net-maui-7.0 states:

  • SelectedItems (Inherited from SelectableItemsView)

https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.selectableitemsview.selecteditems?view=net-maui-7.0#microsoft-maui-controls-selectableitemsview-selecteditems states:

public System.Collections.Generic.IList<object> SelectedItems { get; set; }

This basically means when SelectionMode=Multiple it is expecting an IList<object> which it will update accordingly, but, you will need to implement SelectionChangedCommand to see changes in the selection.

// MainPage.xaml.cs
using System.Diagnostics;
using System.Windows.Input;

namespace StackOverflow.Maui.Mvvm.SelectMultiple;

public class Person
{
    public string Name { get; set; }
}

public partial class MainPage : ContentPage
{
    public IList<Person> Persons { get; }
        = new List<Person>("Tom,Dick,Harry"
                               .Split(",")
                               .Select(s => new Person() { Name = s }));
    public IList<object> SelectedPersons { get; } = new List<object>();
    public ICommand CheckCommand { get; }     
    public MainPage()
    {
        CheckCommand = new Command(() =>
        {
            string SelectedPersonsText = string.Join(", ", SelectedPersons.Select(p => ((Person)p).Name));
            Debug.WriteLine($"SelectedPersons = [{SelectedPersons.Count}] {SelectedPersonsText}");
        });
        OnPropertyChanged(nameof(CheckCommand));
        BindingContext = this;
        InitializeComponent();
    }
}
<!-- MainPage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:StackOverflow.Maui.Mvvm.SelectMultiple"
             x:Class="StackOverflow.Maui.Mvvm.SelectMultiple.MainPage">
    <CollectionView ItemsSource="{Binding Persons}"
                    SelectionMode="Multiple"
                    SelectedItems="{Binding SelectedPersons}"
                    SelectionChangedCommand="{Binding CheckCommand}">
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="{x:Type local:Person}">
                <Frame>
                    <Label Text="{Binding Name}" />
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup Name="CommonStates">
                            <VisualState Name="Normal"/>
                            <VisualState Name="Selected">
                                <VisualState.Setters>
                                    <Setter Property="BackgroundColor" Value="Orange"></Setter>
                                </VisualState.Setters>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                </Frame>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</ContentPage>

Upvotes: 0

Jason
Jason

Reputation: 89102

create a property in your VM (note that it needs to be a collection of object (see this question)

[ObservableProperty]
private ObservableCollection<Person> persons;

[ObservableProperty]
private ObservableCollection<object> selectedPersons;

initialize them

public SelectPageViewModel()
{
    Persons = new();
    SelectedPersons = new();
}

then bind your CollectionView to it

 <CollectionView ItemsSource="{Binding Persons}"
                SelectionMode="Multiple"
                SelectedItems="{Binding SelectedPersons}"
                SelectionChangedCommand="{Binding SelectionChangedCommand}">

if the user selects 3 rows, those 3 objects will be contained in SelectedPersons. SelectedPersons will be a subset of your ItemsSource Persons

[RelayCommand]
private void SelectionChanged()
{
    foreach(var p in SelectedPersons)
    {
       if (p is Person person)
        {
            Console.WriteLine($"{person.Name} is selected");
        }
    }
}

[ObservableProperty]
private ObservableCollection<Person> persons;

[ObservableProperty]
private ObservableCollection<object> selectedPersons

Upvotes: 2

Related Questions