Search code examples
c#mvvmmauicollectionviewsourcemultipleselection

.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>

Solution

  • 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