MAUI (CraftUI Part 8) Custom Multi Selection Picker

In this article of the MAUI CraftUI series, we dive into building a custom Multi-Selection Picker component. Unlike the default single-selection behavior in .NET MAUI, our enhanced version allows users to select multiple items and provides an intuitive UI for managing their choices.
MAUI (CraftUI Part 8) Custom Multi Selection Picker

Intro

Disclaimer: This article is intended for an audience already familiar with MAUI. Explanations are concise and may be challenging for beginners.

Welcome to this series of articles where we’ll build a complete Design System library of custom controls in MAUI. This article builds on the previous one MAUI (CraftUI Part 4) Custom Picker with Collection View and Popup.

There’s no native multi-picker in .NET MAUI. A quick search for “MAUI multi-picker” reveals a gap, you’ll find paid libraries like DevExpress, Syncfusion, or Telerik, but no reliable and beautifully designed open-source alternative.

This is exactly why I wanted to create a free, community-driven solution.

Important note: This implementation works only with CommunityToolkit version 11.2.0. Major breaking changes were introduced in the library starting from version 12.x.x., migration documentation here.

Goal

Based on the CfPickerPopup control, we will create an enhanced version to support multiple selection in the CollectionView that displays the list of items.

The final result will look like this:

Final Result

Implement the popup CfCollectionMultiSelectionPopup

To recall the core of this control, it is built with two main controls:

  • CfMultiPickerPopup: displays the picker. It is based on the LabelBase control (inherited by all inner controls) and shows the selected items.
  • CfCollectionMultiSelectionPopup: displays the full list of items in a CollectionView, embedded within a Popup control from CommunityToolkit.

CfMultiPickerPopup and CfCollectionMultiSelectionPopup

Here is my first technical decision: I need to extend CfCollectionPopup to support both single and multiple selection. Should I reuse CfCollectionPopup or create a separate control?

I could override CfCollectionPopup to handle both single and multiple selection. However, this would introduce unnecessary complexity and make the control harder to maintain over time. It would become the core for two picker controls, prioritizing genericity over single responsibility.

To keep things clean and focused, I decided to rename CfCollectionPopup to CfCollectionSingleSelectionPopup. This ensures the control has one clear responsibility and supports a single, well-defined behavior.

Let’s create the code behind for CfCollectionMultiSelectionPopup:

–> CraftUI.Library.Maui/Controls/Popups/CfCollectionMultiSelectionPopup.xaml.cs

public partial class CfCollectionMultiSelectionPopup
{
    public static readonly BindableProperty TitleProperty = 
        BindableProperty.Create(nameof(Title), typeof(string), 
            typeof(CfCollectionMultiSelectionPopup), propertyChanged: TitleChanged, 
            defaultBindingMode: BindingMode.OneWayToSource);
    public static readonly BindableProperty ItemsSourceProperty = 
        BindableProperty.Create(nameof(ItemsSource), typeof(IList), 
        typeof(CfCollectionMultiSelectionPopup), propertyChanged: ItemsSourceChanged);
    public static readonly BindableProperty SelectedItemsProperty = 
        BindableProperty.Create(nameof(SelectedItems), typeof(IList<object>), 
        typeof(CfCollectionMultiSelectionPopup), defaultBindingMode: BindingMode.TwoWay);
    public static readonly BindableProperty ItemDisplayProperty = 
        BindableProperty.Create(nameof(ItemDisplay), typeof(string), 
        typeof(CfCollectionMultiSelectionPopup));

    public string Title
    {
        get => (string)GetValue(TitleProperty);
        set => SetValue(TitleProperty, value);
    }

    public IList? ItemsSource
    {
        get => (IList?)GetValue(ItemsSourceProperty);
        set => SetValue(ItemsSourceProperty, value);
    }

    public IList<object>? SelectedItems
    {
        get => (IList<object>?)GetValue(SelectedItemsProperty);
        set => SetValue(SelectedItemsProperty, value);
    }

    public string? ItemDisplay
    {
        get => (string?)GetValue(ItemDisplayProperty);
        set
        {
            SetValue(ItemDisplayProperty, value);
            InitCollectionViewItems();
        }
    }
    
    public CfCollectionMultiSelectionPopup()
    {
        InitializeComponent();

        var tapped = new TapGestureRecognizer();
        tapped.Tapped += (_, _) => Close();
        CloseImage.GestureRecognizers.Add(tapped);
    }

    private static void TitleChanged
        (BindableObject bindable, object oldValue, object newValue) => 
            ((CfCollectionMultiSelectionPopup)bindable).UpdateTitleView();
    private static void ItemsSourceChanged
        (BindableObject bindable, object oldValue, object newValue) =>
            ((CfCollectionMultiSelectionPopup)bindable).UpdateItemsSourceView();

    private void UpdateTitleView() => TitleLabel.Text = Title;

    private void UpdateItemsSourceView()
    {
        PickerCollectionView.ItemsSource = ItemsSource;
    }

    private void InitCollectionViewItems()
    {
        PickerCollectionView.ItemTemplate = new DataTemplate(() =>
        {
            var contentView = new ContentView
            {
                HorizontalOptions = LayoutOptions.Fill,
                VerticalOptions = LayoutOptions.Fill,
                Padding = new Thickness(16, 14),
                WidthRequest = GutterSystem.WidthScreen,
                BackgroundColor = Colors.Transparent
            };

            /// Define the style and structure of each items of the ItemTemplate.
            /// See the Github page for the full code.

            return contentView;
        });

        PickerCollectionView.MaximumHeightRequest = GutterSystem.HeightScreen / 1.3;
    }
}

Compared to CfPickerPopup, the CfMultiPickerPopup uses the SelectedItems property to support multiple selections. This property is of type IList<object> to provide generic compatibility with any item type.

Unlike CfPickerPopup, the popup does not close immediately after selecting an item. Instead, it stays open to allow the user to make multiple selections before confirming their choice.

Let’s implement the view side, it is identical to CfPickerPopup, except that we set SelectionMode=”Multiple”.

–> CraftUI.Library.Maui/Controls/Popups/CfCollectionMultiSelectionPopup.xaml

<toolkit:Popup
    HorizontalOptions="Fill"
    VerticalOptions="End"
    x:Class="CraftUI.Library.Maui.Controls.Popups.CfCollectionMultiSelectionPopup"
    x:Name="This"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:markup="clr-namespace:CraftUI.Library.Maui.MarkupExtensions"
    xmlns:controls="clr-namespace:CraftUI.Library.Maui.Controls.Popups">

    <toolkit:Popup.Resources>
        <!-- Define page resources.  -->
        <!-- See the Github page for the full code.  -->
    </toolkit:Popup.Resources>

    <Grid
        BindingContext="{x:Reference This}"
        RowDefinitions="Auto,Auto"
        Style="{StaticResource BottomPopupRootGrid}"
        x:DataType="controls:CfCollectionMultiSelectionPopup">
        
        <VerticalStackLayout>
            <!-- Define popup title.  -->
            <!-- See the Github page for the full code.  -->
        </VerticalStackLayout>

        <CollectionView
            Grid.Row="1"
            ItemSizingStrategy="MeasureFirstItem"
            ItemsLayout="VerticalList"
            ItemsSource="{Binding ItemsSource}"
            SelectionMode="Multiple"
            x:Name="PickerCollectionView">
        </CollectionView>
    </Grid>
</toolkit:Popup>

Create the picker: CfMultiPickerPopup

Now let’s create the control that will open our CfCollectionMultiSelectionPopup.

Here is a summary of its properties:

  • Title: The title text displayed on the picker popup, transmitted to CfCollectionMultiSelectionPopup.
  • SelectedItems: Holds the list of currently selected items. Supports two-way binding and manages selection changes.
  • ItemDisplay: Defines the property name of the item to display as text in the list.
  • DefaultValue: The text to show when no items are selected (acts as a placeholder).
  • ItemsSource: Provides the collection of items to display in the picker.
  • SelectionChangedCommand: Command invoked when the selected items change.

Again, very similar to CfPickerPopup, one exception, instead of SelectedItem we use SelectedItems as collection.

Let’s implement the code behind for CfMultiPickerPopup:

–> CraftUI.Library.Maui/Controls/CfMultiPickerPopup.xaml.cs

public partial class CfMultiPickerPopup
{
    private CfCollectionMultiSelectionPopup? _collectionPopup;
    private readonly TapGestureRecognizer _tapGestureRecognizer;

    public static readonly BindableProperty TitleProperty = 
    BindableProperty.Create(nameof(Title), typeof(string), typeof(CfMultiPickerPopup));
    public static readonly BindableProperty SelectedItemsProperty = 
        BindableProperty.Create(nameof(SelectedItems), typeof(IList<object>), 
        typeof(CfMultiPickerPopup), defaultBindingMode: BindingMode.TwoWay);
    public static readonly BindableProperty ItemDisplayProperty = 
        BindableProperty.Create(nameof(ItemDisplay), typeof(string), 
        typeof(CfMultiPickerPopup), defaultBindingMode: BindingMode.OneWay);
    public static readonly BindableProperty DefaultValueProperty = 
        BindableProperty.Create(nameof(DefaultValue), typeof(string), 
        typeof(CfMultiPickerPopup), defaultBindingMode: BindingMode.OneWay);
    public static readonly BindableProperty ItemsSourceProperty = 
        BindableProperty.Create(nameof(ItemsSource), typeof(IList), 
        typeof(CfMultiPickerPopup), defaultBindingMode: BindingMode.OneWay);
    public static readonly BindableProperty SelectionChangedCommandProperty = 
        BindableProperty.Create(nameof(SelectionChangedCommand), 
        typeof(ICommand), typeof(CfMultiPickerPopup));
    
    public ObservableCollection<string> SelectedStrings { get; set; }

    public IList? ItemsSource
    {
        get => (IList?)GetValue(ItemsSourceProperty);
        set => SetValue(ItemsSourceProperty, value);
    }

    public IList<object>? SelectedItems
    {
        get => (IList<object>?)GetValue(SelectedItemsProperty);
        set => SetValue(SelectedItemsProperty, value);
    }

    public string ItemDisplay
    {
        get => (string)GetValue(ItemDisplayProperty);
        set => SetValue(ItemDisplayProperty, value);
    }

    public string DefaultValue
    {
        get => (string)GetValue(DefaultValueProperty);
        set => SetValue(DefaultValueProperty, value);
    }

    public string Title
    {
        get => (string)GetValue(TitleProperty);
        set => SetValue(TitleProperty, value);
    }
    
    public ICommand? SelectionChangedCommand
    {
        get => (ICommand?)GetValue(SelectionChangedCommandProperty);
        set => SetValue(SelectionChangedCommandProperty, value);
    }

    public CfMultiPickerPopup()
    {
        InitializeComponent();
        
        _tapGestureRecognizer = new TapGestureRecognizer();
        _tapGestureRecognizer.Tapped += OnTapped;

        SelectedStrings = new ObservableCollection<string>();
        
        GestureRecognizers.Add(_tapGestureRecognizer);
    }

    protected override void OnBindingContextChanged()
    {
        base.OnBindingContextChanged();

        ActionIconSource ??= "chevron_bottom.png";
        ActionIconCommand ??= new Command(() => OnTapped(null, EventArgs.Empty));
    }
    
    /// Define all the internal methods.
    /// See the Github page for the full code.

    private void OnTapped(object? sender, EventArgs e)
    {
        _collectionPopup = new CfCollectionMultiSelectionPopup
        {
            BindingContext = this,
            Title = !string.IsNullOrEmpty(Title) ? Title : Label,
            ItemsSource = ItemsSource,
            SelectedItems = SelectedItems,
            ItemDisplay = ItemDisplay
        };
        
        _collectionPopup.Closed += (_, _) =>
        {
            SelectionChangedCommand?.Execute(null);
        };
        
        _collectionPopup.SetBinding(CfCollectionMultiSelectionPopup.ItemsSourceProperty, 
            path: nameof(ItemsSource));
        _collectionPopup.SetBinding(CfCollectionMultiSelectionPopup.SelectedItemsProperty,
            path: nameof(SelectedItems));

        Shell.Current.ShowPopup(_collectionPopup);
    }
}

Let’s implement the view for CfMultiPickerPopup:

–> CraftUI.Library.Maui/Controls/CfMultiPickerPopup.xaml

<common:LabelBase 
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:common="clr-namespace:CraftUI.Library.Maui.Common"
    x:Class="CraftUI.Library.Maui.Controls.CfMultiPickerPopup"
    x:Name="Root">
    
    <common:LabelBase.View>
        <FlexLayout
            Direction="Row"
            Wrap="Wrap"
            Margin="8,8,8,0"
            MinimumHeightRequest="32"
            BindingContext="{x:Reference Name=Root}"
            BindableLayout.ItemsSource="{Binding SelectedStrings, Mode=TwoWay}">
            <BindableLayout.ItemTemplate>
                <DataTemplate>

                    <Border
                        BackgroundColor="{StaticResource Gray100}"
                        StrokeShape="RoundRectangle 12"
                        Padding="4"
                        StrokeThickness="0"
                        Margin="0,0,6,8">
                        <Label
                            Text="{Binding .}"
                            TextColor="{StaticResource Gray900}"
                            FontSize="14"
                            VerticalOptions="Center"/>
                    </Border>
                    
                </DataTemplate>
            </BindableLayout.ItemTemplate>
        </FlexLayout>    
    </common:LabelBase.View>
</common:LabelBase>

Notice, the code behind is a bit different from the Github code, it’s simplified show you an important internal mecanism you should be aware of.

If you run this code, it will produce this behaviors :

SelectedItems not working

The collection SelectedItems is not updated in the ViewModel.

In CfPickerPopup, this was straightforward because it used a SelectedItem. When the value changed, the binding mechanism automatically refreshed both the ViewModel and the View.

However, in CfMultiPickerPopup, we use IList<object> and modify the internal collection directly. Using ObservableCollection might seem like an option, but it misses some crucial steps and doesn’t work properly (I’ve already tested this approach for you).

To find the right solution, I reviewed the source code of CollectionView in the MAUI GitHub repository.

Have a try, create a CollectionView, define a SelectedItems property and go to the reference to observe the code.

It will lead you to the SelectableItemsView class, where you’ll notice SelectionList on the setter:

–> maui/src/Controls/src/Core/Items/SelectableItemsView.cs

public IList<object> SelectedItems
{
    get => (IList<object>)GetValue(SelectedItemsProperty);
    set => SetValue(SelectedItemsProperty, new SelectionList(this, value));
}

Implement SelectionList

What is SelectionList?

It’s a class used by SelectableItemsView to keep track of (and respond to changes in) the SelectedItems property. You can access the CollectionChanged event when the collection changed:

–> maui/src/Controls/src/Core/Items/SelectionList.cs

public SelectionList(SelectableItemsView selectableItemsView, IList<object> items = null)
{
    _selectableItemsView = selectableItemsView ?? 
        throw new ArgumentNullException(nameof(selectableItemsView));
    _internal = items ?? new List<object>();
    _shadow = Copy();

    if (items is INotifyCollectionChanged incc)
    {
        incc.CollectionChanged += OnCollectionChanged;
    }
}

That’s exactly what we need.

However, since this class is internal, we can’t use it directly. We will create a new class with the same name, but a slightly lighter implementation.

Let’s implement SelectionList as internal (because only CraftUI.Library.Maui should use it).

–> CraftUI.Library.Maui/Common/SelectionList.cs

internal class SelectionList : IList<object>
{
    private static readonly IList<object> SEmpty = new List<object>(0);
    private readonly CfMultiPickerPopup _selectableItemsView;
    private readonly IList<object> _internal;
    private IList<object> _shadow;
    private bool _externalChange;

    public SelectionList(CfMultiPickerPopup selectableItemsView, 
        IList<object>? items = null)
    {
        _selectableItemsView = selectableItemsView ?? 
            throw new ArgumentNullException(nameof(selectableItemsView));
        _internal = items ?? new List<object>();
        _shadow = Copy();

        if (items is INotifyCollectionChanged incc)
        {
            incc.CollectionChanged += OnCollectionChanged;
        }
    }

    public object this[int index] 
    { 
        get => _internal[index]; 
        set => _internal[index] = value; 
    }

    public int Count => _internal.Count;

    public bool IsReadOnly => false;

    public void Add(object item)
    {
        _externalChange = true;
        _internal.Add(item);
        _externalChange = false;

        _selectableItemsView.SelectedItemsPropertyChanged(_shadow, _internal);
        _shadow.Add(item);
    }

    /// Define all methods for a collection Add, Clear, Contains, CopyTo,
    /// IndexOf, Insert, Remove, RemoveAt, Copy.
    /// See the Github page for the full code.
}

When you Add(), Clear(), Insert(), Remove(), etc. items in SelectedItems, it triggers SelectedItemsPropertyChanged method, which updates the UI through UpdateSelectedStrings and raises a property change for SelectedItems, allowing the ViewModel to be notified of collection changes (we’ll see the implementation of this method below).

Now let’s update our CfMultiPickerPopup to use SelectionList.

–> CraftUI.Library.Maui/Controls/CfMultiPickerPopup.xaml.cs

public static readonly BindableProperty SelectedItemsProperty = 
    BindableProperty.Create(nameof(SelectedItems), typeof(IList<object>), 
        typeof(CfMultiPickerPopup), defaultBindingMode: BindingMode.TwoWay, 
        propertyChanged: SelectedItemsPropertyChanged, 
        coerceValue: CoerceSelectedItems, 
        defaultValueCreator: DefaultValueCreator);

We create a new instance of SelectionList in the setter so we can use the goodness by the event CollectionChanged to update the UI.

public IList<object>? SelectedItems
{
    get => (IList<object>?)GetValue(SelectedItemsProperty);
    set => SetValue(SelectedItemsProperty, new SelectionList(this, value));
}

Let’s look at the implementation of the new method defined in the BindableProperty:

  • PropertyChanged: updates the SelectedStrings (collection used to display the selected items) and raised property changed event for SelectedItems.
  • CoerceValue: This is used to validate or adjust the value before it’s set.
  • DefaultValueCreator: This provides a default value when the property is first created or when no value is explicitly set.
private static void SelectedItemsPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
    var selectableItemsView = (CfMultiPickerPopup)bindable;
    var oldSelection = (IList<object>)oldValue;
    var newSelection = (IList<object>)newValue;

    selectableItemsView.SelectedItemsPropertyChanged(oldSelection, newSelection);
}

private static object CoerceSelectedItems(BindableObject bindable, object? value)
{
    if (value == null)
    {
        return new SelectionList((CfMultiPickerPopup)bindable);
    }

    if (value is SelectionList)
    {
        return value;
    }

    return new SelectionList((CfMultiPickerPopup)bindable, value as IList<object>);
}

private static object DefaultValueCreator(BindableObject bindable)
{
    return new SelectionList((CfMultiPickerPopup)bindable);
}

internal void SelectedItemsPropertyChanged
    (IList<object> oldSelection, IList<object> newSelection)
{
    UpdateSelectedStrings();
    OnPropertyChanged(SelectedItemsProperty.PropertyName);
}

private void UpdateSelectedStrings()
{
    SelectedStrings.Clear();

    if (SelectedItems is null)
    {
        return;
    }
    
    foreach (var item in SelectedItems)
    {
        var displayValue = item.GetDisplayString(ItemDisplay);
        if (!string.IsNullOrEmpty(displayValue) && 
            !SelectedStrings.Contains(displayValue))
        {
            SelectedStrings.Add(displayValue);
        }
    }

    OnPropertyChanged(nameof(SelectedStrings));
    InvalidateSurfaceForCanvasView();
}

Display CfMultiPickerPopup in a page

Everything is in place, let’s display CfMultiPickerPopup on a page.

I created a new page called MultiPickerPopupPage, associated with the PickerPageViewModel. The business logic will remain almost same as the CfPickerPopup.

Just add a new property SelectedCountries, and new methods, updated PickerPageViewModel:

–> CraftUI.Demo/Presentation/Pages/Controls/Pickers/PickerPageViewModel

public partial class PickerPageViewModel : ViewModelBase
{
    [ObservableProperty]
    private ObservableCollection<object> _selectedCountries;

    /// Define all properties and methods.
    /// See the Github page for the full code.

    [RelayCommand]
    private async Task ShowSelectedItems()
    {
        _logger.LogInformation(
            "ShowSelectedItems( Count: {Count} )", SelectedCountries.Count);
        
        await _displayService.ShowPopupAsync(
            title: "Selected Countries",
            message:  "No countries selected.",
            accept: "OK");
    }

    [RelayCommand]
    private Task CountriesChanged()
    {
        _logger.LogInformation("CountriesChanged()");

        return Task.CompletedTask;
    }
}

The content of the page MultiPickerPopupPage:

–> CraftUI.Demo/Presentation/Pages/Pickers/MultiPickerPopupPage.cs

<base:ContentPageBase
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:base="clr-namespace:CraftUI.Demo.Presentation.Common"
xmlns:page="clr-namespace:CraftUI.Demo.Presentation.Pages.Controls.Pickers"
xmlns:controls="clr-namespace:CraftUI.Library.Maui.Controls;assembly=CraftUI.Library.Maui"
Title="Picker Demo"
x:DataType="page:PickerPageViewModel"
x:Class="CraftUI.Demo.Presentation.Pages.Controls.Pickers.MultiPickerPopupPage">

    <Grid RowDefinitions="*,Auto" >
        <VerticalStackLayout Margin="16, 32" Spacing="25">
                
            <controls:CfMultiPickerPopup
                Label="Country"
                SelectedItems="{Binding SelectedCountries, Mode=TwoWay}"
                SelectionChangedCommand="{Binding CountriesChangedCommand}"
                ItemsSource="{Binding CountriesLoader.Result}" 
                ItemDisplay="{x:Static page:PickerPageViewModel.CountryDisplayProperty}"
                IsLoading="{Binding CountriesLoader.ShowLoader}"
                />
                
        </VerticalStackLayout>
        
        <VerticalStackLayout Grid.Row="1" 
                             Style="{StaticResource BottomElementVerticalStackLayout}">
            <Button Command="{Binding ResetCommand}" 
                    Style="{StaticResource FilledPrimaryButton}" 
                    Text="Reset" />
            
            <Button Command="{Binding ShowSelectedItemsCommand}" 
                    Style="{StaticResource FilledPrimaryButton}" 
                    Text="Show Selected Items" />
        </VerticalStackLayout>
    </Grid>
</base:ContentPageBase>

See it in action

Final Result

Conclusion

The full code is available on GitHub, with a working example.

This article introduces a custom CfMultiPickerPopup control in .NET MAUI to support multi-selection in a clean and reusable way.

It explains key design decisions like splitting single and multi-selection logic for maintainability and implementing a custom SelectionList to sync UI and ViewModel.

By the end, you’ll have a fully functional multi-picker component that fills the gap left by the lack of native support in MAUI.

If you enjoyed this blog post, then follow me on LinkedIn, subscribe the newsletter so you don’t miss out on any future posts. Don’t forget to share this with your friends and colleagues who are interested in learning about this topic. Thank you 🥰

Happy coding!

MAUI (CraftUI Part 8) Custom Multi Selection Picker