How to Build a Reusable Multi-Select Dropdown in .NET MAUI with Custom Design πŸŽ¨πŸ“±

Ali RazaAli Raza
5 min read

When working on mobile apps, providing a seamless and visually appealing multi-select dropdown is essential. In this guide, we'll create a custom multi-select dropdown in .NET MAUI using a modal page. The selected items will be displayed in an Entry-like UI with cards. πŸš€

✨ Features

βœ… Selected items displayed as cards in an Entry-like design. βœ… Multi-select capability with checkboxes. βœ… Smooth user experience with animations. βœ… Custom modal page for item selection. βœ… Fully reusable generic control.

🎨 Designing a Reusable Multi-Select Control

To make our control reusable, we should create a separate custom control that can be used in multiple places without rewriting logic.

πŸ“Œ MultiSelection.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentView
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="NewMultiSelect.Controls.MultiSelect.MultiSelection"
             x:Name="multiSelect">
    <VerticalStackLayout Spacing="5">
        <Label x:Name="lblHeading" Style="{StaticResource SmallMediumHorizontalStart}"/>
        <Border Stroke="#9CA0B1" StrokeShape="RoundRectangle 7" Padding="9,12">
            <Grid ColumnDefinitions="*,Auto" ColumnSpacing="5">
                <Label x:Name="lbl" Grid.Column="0" Grid.ColumnSpan="1" Text="Select options" Style="{StaticResource SmallRegularGray}"
         IsVisible="{Binding PlaceholderVisibility,Source={x:Reference multiSelect}}" TextColor="#C4C4C4" Margin="0,5"/>
                <CollectionView x:Name="collectionView" Grid.Column="0" ItemsSource="{Binding SelectedItems, Source={x:Reference  multiSelect}}" HeightRequest="{OnPlatform iOS=30}" IsVisible="{Binding PlaceholderVisibility,Source={x:Reference multiSelect},Converter={StaticResource InvertedBoolConverter}}">
                    <CollectionView.ItemsLayout>
                        <LinearItemsLayout Orientation="Horizontal" ItemSpacing="5"/>
                    </CollectionView.ItemsLayout>
                    <CollectionView.ItemTemplate>
                        <DataTemplate>
                            <Border Style="{StaticResource VerySmallTransparentBorderOrangeBackgroundRoundRecBorder}" Padding="5,3">
                                <HorizontalStackLayout Spacing="3">
                                    <Image Source="white_cross" Style="{StaticResource ImageCenterAspectFit}" HeightRequest="13" WidthRequest="13" BindingContext="{Binding .}">
                                        <Image.GestureRecognizers>
                                            <TapGestureRecognizer Tapped="RemoveItem"/>
                                        </Image.GestureRecognizers>
                                    </Image>
                                    <Label Text="{Binding Value}" Style="{StaticResource TooMicroMediumWhiteHorizontalCenter}" FontSize="13"/>
                                </HorizontalStackLayout>
                            </Border>
                        </DataTemplate>
                    </CollectionView.ItemTemplate>
                </CollectionView>
                <Image Grid.Column="1" Source="black_dropdown" Opacity=".5" HeightRequest="15" WidthRequest="15" VerticalOptions="Center" Margin="0,0,7,0"/>
            </Grid>
            <Border.GestureRecognizers>
                <TapGestureRecognizer Tapped="NavigateToMultiSelect"/>
            </Border.GestureRecognizers>
        </Border>
        <Label x:Name="lblError" Text="{Binding Source={x:Reference multiSelect},Path= ErrorText}" IsVisible="{Binding Source={x:Reference multiSelect},Path=IsItemSelected}" Style="{StaticResource MicroSemiboldHorizontalStart}" TextColor="#EE0808" Margin="5,0,0,0"></Label>
    </VerticalStackLayout>
</ContentView>

πŸ“Œ MultiSelection.xaml.cs

using System.Collections.ObjectModel;
using NewMultiSelect.Models.Registration;

namespace NewMultiSelect.Controls.MultiSelect;

public partial class MultiSelection : ContentView
{
    public static readonly BindableProperty PlaceholderVisibilityProperty =
    BindableProperty.Create(nameof(PlaceholderVisibility), typeof(bool), typeof(MultiSelection), false, BindingMode.TwoWay);

    public static readonly BindableProperty SelectedItemsProperty =
        BindableProperty.Create(nameof(SelectedItems), typeof(ObservableCollection<Generic>), typeof(MultiSelection), null, BindingMode.TwoWay);

    public static readonly BindableProperty ItemsSourceProperty =
        BindableProperty.Create(nameof(ItemsSource), typeof(ObservableCollection<Generic>), typeof(MultiSelection), new ObservableCollection<Generic>(), BindingMode.TwoWay);


    public static readonly BindableProperty IsItemSelectedProperty =
        BindableProperty.Create(nameof(IsItemSelected), typeof(bool), typeof(MultiSelection), false);

     public static readonly BindableProperty ErrorTextProperty =
       BindableProperty.Create(nameof(ErrorText), typeof(string), typeof(MultiSelection), default(string));

    public string ErrorText
    {
        get => (string)GetValue(ErrorTextProperty);
        set => SetValue(ErrorTextProperty, value);
    }

    public bool IsItemSelected
    {
        get => (bool)GetValue(IsItemSelectedProperty);
        set => SetValue(IsItemSelectedProperty, value);
    }
    public bool PlaceholderVisibility
    {
        get => (bool)GetValue(PlaceholderVisibilityProperty);
        set
        {
            SetValue(PlaceholderVisibilityProperty, value);
        }
    }

   public ObservableCollection<Generic> SelectedItems
{
    get => (ObservableCollection<Generic>)GetValue(SelectedItemsProperty);
    set
    {
        var oldCollection = (ObservableCollection<Generic>)GetValue(SelectedItemsProperty);
        if (oldCollection != null)
        {
            oldCollection.CollectionChanged -= OnSelectedItemsChanged;
        }

        SetValue(SelectedItemsProperty, value);

        if (value != null)
        {
            value.CollectionChanged += OnSelectedItemsChanged;
        }

        // Update PlaceholderVisibility when the collection is set
        UpdatePlaceholderVisibility();
    }
}
private void OnSelectedItemsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
    UpdatePlaceholderVisibility();
}
private void UpdatePlaceholderVisibility()
{
   IsItemSelected = PlaceholderVisibility = SelectedItems == null || SelectedItems.Count == 0;
}
    public ObservableCollection<Generic> ItemsSource
    {
        get => (ObservableCollection<Generic>)GetValue(ItemsSourceProperty);
        set => SetValue(ItemsSourceProperty, value);
    }

    public string Heading
    {
        get { return lblHeading.Text; }
        set { lblHeading.Text = value; }
    }

    public MultiSelection()
    {
        InitializeComponent();
    }

    private async void NavigateToMultiSelect(object sender, TappedEventArgs e)
    {
        // Push the modal for Multi Select
        var countrySelectionPage = new MultiSelectionPage(ItemsSource, SelectedItems);

        countrySelectionPage.Disappearing+=(s,e)=>
        {
          // Ensure SelectedItems is updated from the modal page
        if (countrySelectionPage.SelectedItems != null && countrySelectionPage.SelectedItems.Count > 0)
        {
            if(SelectedItems==null)
             SelectedItems = new ObservableCollection<Generic>();

            SelectedItems = countrySelectionPage.SelectedItems;
        }
        };
        await AppShell.Current.Navigation.PushModalAsync(countrySelectionPage);
    }

    private void RemoveItem(object sender, TappedEventArgs e)
    {
        var img = sender as Image;
        var item = img.BindingContext as Generic;
        SelectedItems.Remove(item);
    }
}

πŸ“Œ Creating the Multi-Select Modal Page

This modal page will display a list of items with checkboxes, allowing users to select multiple options.

MultiSelectionPage.xaml

<?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"
             x:Class="NewMultiSelect.Controls.MultiSelect.MultiSelectionPage">
    <Grid RowDefinitions="Auto,*,Auto" Padding="15" RowSpacing="15">
        <ImageButton Grid.Column="0" Source="left_arrow" Style="{StaticResource CircleImageButton}" Clicked="OnBackPressed" HorizontalOptions="StartAndExpand"/>
        <!-- Multi Selection -->
        <CollectionView x:Name="list"
                        Grid.Row="1"
                        SelectionMode="None"
                        VerticalOptions="FillAndExpand">
            <CollectionView.ItemsLayout>
                <LinearItemsLayout Orientation="Vertical" ItemSpacing="5"/>
            </CollectionView.ItemsLayout>
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <Border Style="{StaticResource RoundRecBorder}" Padding="10,5,0,5" BindingContext="{Binding .}">
                        <Grid RowDefinitions="Auto" ColumnDefinitions="*,Auto" Padding="0" VerticalOptions="Center">
                            <!-- Item Name -->
                            <Label Text="{Binding Value}" Style="{StaticResource SmallMediumHorizontalStart}" VerticalOptions="Center"/>
                            <!-- Multi Select Checkbox -->
                            <CheckBox Grid.Row="0" Grid.Column="1" VerticalOptions="Center" HorizontalOptions="Center" IsChecked="{Binding IsChecked}" IsEnabled="False" Color="{StaticResource Primary}"/>
                        </Grid>
                        <Border.GestureRecognizers>
                            <TapGestureRecognizer Tapped="MultiSelectItem"/>
                        </Border.GestureRecognizers>
                    </Border>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
        <Button x:Name="btnSubmit" Grid.Row="2" Text="Submit" Style="{StaticResource PaidMealsButton}"/>
    </Grid>
</ContentPage>

MultiSelectionPage.xaml.cs

using System.Collections.ObjectModel;
using NewMultiSelect.Models.Generic;

namespace NewMultiSelect.Controls.MultiSelect;

public partial class MultiSelectionPage : ContentPage
{
    public  ObservableCollection<Generic> SelectedItems {get;set;}
    public MultiSelectionPage(ObservableCollection<Generic> itemsSource, ObservableCollection<Generic> selectedItems)
    {
        InitializeComponent();
        btnSubmit.Clicked+= async(s,e)=>
        {
          await AppShell.Current.Navigation.PopModalAsync();
        };
        SelectedItems = selectedItems ?? new ObservableCollection<Generic>();
        var selectedIds = SelectedItems.Select(x => x.Key).ToHashSet();
        itemsSource.ToList().ForEach(item => item.IsChecked = selectedIds?.Contains(item.Key));
        list.ItemsSource =itemsSource;
    }

    private async void OnBackPressed(System.Object sender, System.EventArgs e)
    {
        await AppShell.Current.Navigation.PopModalAsync();
    }

    private void MultiSelectItem(object sender, TappedEventArgs e)
    {
        var border = sender as Border;
        var item = border.BindingContext as Generic;

        if(item.IsChecked.Equals(true))
        {
         item.IsChecked =false;
          SelectedItems.Remove(item);
        }
        else
        {
         item.IsChecked =true;
          SelectedItems.Add(item);
        }

    }
}

πŸ“Œ Integrating the Multi-Select Control in a Page

Now that we have the custom control, let's integrate it into a page! πŸŽ‰

Mainpage.xaml

<?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"
    x:Class="NewMultiSelect.Views.MainPage"
    xmlns:controls="clr-namespace:PaidMeals.Controls">
     <VerticalStackLayout>
     <multiSelect:MultiSelection  Heading="Dietary Options" PlaceholderVisibility="True" 
                    ItemsSource="{Binding MenuItemForm.DiatoryOptions}" SelectedItems="{Binding MenuItemForm.SelectedDiatoryOptions.Value}" 
                    ErrorText="{Binding MenuItemForm.SelectedDiatoryOptions.Error,Mode=TwoWay}"
                    IsItemSelected="{Binding MenuItemForm.SelectedDiatoryOptions.IsInvalid,Mode=TwoWay}"/>
     </VerticalStackLayout>
</ContentPage>

πŸ“Œ Best Practices for Reusable Controls 🎯

To make your own controls truly reusable, follow these best practices:

1️⃣ Encapsulation & Reusability

  • Create a separate custom control (MultiSelectControl) to avoid code duplication.

  • Use bindable properties to pass data dynamically.

2️⃣ Generic Data Handling

  • Instead of hardcoding item lists, allow the control to accept any data type by using generic data binding.

  • Use commands and events to communicate with the parent view.

3️⃣ Separation of Concerns

  • Keep UI logic inside XAML and business logic in the code-behind or ViewModel.

  • Ensure MVVM compatibility for better state management.

4️⃣ Scalability & Performance

  • Use OnProprtyChange for real-time UI updates.

  • Optimize performance by using collectionview.

5️⃣ Customizability & Theming

  • Allow users to customize UI properties like background color, text size, icons, etc.

πŸ† Wrapping Up

Now, we have a fully reusable multi-select dropdown control in .NET MAUI! πŸŽ‰ The users can select multiple items, which are displayed as cards inside an Entry-like UI.

πŸ”₯ Key Takeaways:

  • Created a Separate Control for better reusability.

  • Used Bindable Properties to allow flexibility.

  • Followed Best Practices to make the control scalable and maintainable.

You can now use this generic multi-select control anywhere in your app! πŸš€

Happy coding! πŸ”₯

1
Subscribe to my newsletter

Read articles from Ali Raza directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ali Raza
Ali Raza

πŸš€ Tech Lead | .NET MAUI Expert | Mobile App Developer I'm Ali Raza, a passionate Tech Lead with over 6 years of experience in mobile app development. I specialize in .NET MAUI/Xamarin and have led multiple high-impact projects, including enterprise apps, fintech solutions, and eSIM technology. πŸ”Ή What I Do: βœ” .NET MAUI & Xamarin – Building cross-platform apps with robust architectures βœ” eSIM & Fintech Apps – Leading innovations in digital connectivity & finance βœ” Performance Optimization – Creating high-quality, scalable mobile solutions βœ” Mentorship & Community Sharing – Helping developers master .NET MAUI πŸ“’ Sharing Weekly Insights on .NET MAUI/Xamarin to help developers level up! Follow me for deep dives into Native Interop, API Optimization, and Advanced UI/UX techniques. Let’s connect and build something amazing! πŸš€