Step-by-Step Guide to Creating a Custom PhoneEntry with Country Code Dropdown in .NET MAUI

Step-by-Step Guide to Creating a Custom PhoneEntry with Country Code Dropdown in .NET MAUI

ยท

5 min read

A country code dropdown is a crucial component in mobile apps, especially when paired with a custom phone entry field for a seamless user experience. In this article, we'll create a custom country code dropdown with a customized entry field in .NET MAUI.

Features of Our Custom Dropdown ๐ŸŽฏ

  • A custom design with Entry field displaying the country flag, code, and phone number input ๐Ÿ“ฑ

  • A dropdown (Modal Page) to select the country code ๐Ÿ”ฝ

  • A searchable country list for better usability ๐Ÿ”

  • Data binding using Bindable Properties ๐Ÿ”—

  • Custom handlers to remove the entry border for Andorid, IOS, Mac and Windows ๐ŸŽจ

Step 1: Write a custom handler to remove the border from the entry ๐Ÿš€

To ensure a clean design, we remove the default border from the Entry field using platform-specific handlers.

Android, IOS, Mac, Windows Entry Handler (MauiProgram.cs) ๐Ÿค–๐Ÿ“ฑ๐Ÿ–ฅ๏ธ๐ŸชŸ


            // Custom mapper for borderless entry
            Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping(nameof(Entry), (handler, view) =>
            {
#if IOS || MACCATALYST
                handler.PlatformView.BackgroundColor = UIKit.UIColor.Clear;
                handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
#elif ANDROID
                handler.PlatformView.BackgroundTintList = Android.Content.Res.ColorStateList.ValueOf(Microsoft.Maui.Graphics.Colors.Transparent.ToAndroid());
#elif WINDOWS
                handler.PlatformView.BorderThickness = new Microsoft.UI.Xaml.Thickness(0);
                handler.PlatformView.Background = null;
#endif
            });

Step 2: Create the Custom Control for PhoneEntry โœ๏ธ

In this step, you will create a custom control for the phone entry field. This control will display the country flag, code, and provide an input for the phone number, enhancing the user interface and experience.

CustomPhoneEntry.Xaml ๐Ÿ“ฑ

<Grid xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="PhoneEntryControl.Controls.CustomPhoneEntry"
             xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls"
             xmlns:ff="clr-namespace:FFImageLoading.Svg.Maui;assembly=FFImageLoading.Compat.Svg.Maui"
             RowDefinitions="Auto"
             ColumnDefinitions="Auto,*"
             x:Name="this">

        <Border Grid.Column="0" Stroke="#9CA0B1" StrokeShape="RoundRectangle 7" Padding="10,3">

            <HorizontalStackLayout BindingContext="{Binding SelectedCountry, Source={x:Reference this}}" Spacing="10" VerticalOptions="Center" HorizontalOptions="Fill">
                <!-- Flag -->
                   <ff:SvgCachedImage x:Name="imgFlag" Source="{Binding Image}" 
                   HeightRequest="25" WidthRequest="40" Aspect="AspectFit" LoadingPlaceholder="imgloading.gif" DownsampleToViewSize="True"/>

                <!-- Country Name -->
                 <Label x:Name="lblName" Text="{Binding Value}" VerticalOptions="Center"/>

                 <!-- DD Arrow -->
                 <Image Source="black_dropdown" Opacity=".5" HeightRequest="15" WidthRequest="15" VerticalOptions="Center"/>
            </HorizontalStackLayout>
            <Border.GestureRecognizers>
                <TapGestureRecognizer Tapped="OpenCountiesCodeModel"/>
            </Border.GestureRecognizers>
        </Border>

        <Border Grid.Column="1" Stroke="#9CA0B1" StrokeShape="RoundRectangle 7" Padding="10,3">
                <Entry  
                    x:Name="txtEntry"
                    Grid.Column="0"
                    ios:Entry.CursorColor="Black"
                    IsSpellCheckEnabled="False"
                    ClearButtonVisibility="WhileEditing"
                    IsTextPredictionEnabled="False"
                    Keyboard="{Binding Source={x:Reference this},Path= Keyboard}"
                    Text="{Binding Source={x:Reference this},Path= Text,Mode=TwoWay}"
                    </Entry.Behaviors>
                </Entry>
        </Border>
</Grid>

Now we need to create a bindable property for the list of countries that we will pass to generic dropdown page and selected country to display on the phone entry.

CustomPhoneEntry.cs

public partial class CustomPhoneEntry: Grid
{
    public CustomPhoneEntry()
    {
        try
        {
            InitializeComponent();
        }
        catch { }
    }

    public static readonly BindableProperty SelectedCountryProperty =
        BindableProperty.Create(nameof(SelectedCountry), typeof(Generic), typeof(CustomPhoneEntry), null, BindingMode.TwoWay);

    public Generic SelectedCountry
    {
        get => (Generic)GetValue(SelectedCountryProperty);
        set  { SetValue(SelectedCountryProperty, value); }
    }

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

    public ObservableCollection<Generic> ItemsSource
    {
        get => (ObservableCollection<Generic>)GetValue(ItemsSourceProperty);
        set => SetValue(ItemsSourceProperty, value);
    }

    public static readonly BindableProperty TextProperty = BindableProperty.Create(
        propertyName: nameof(Text),
        returnType: typeof(string),
        declaringType: typeof(CustomPhoneEntry),
        defaultValue: null,
        defaultBindingMode: BindingMode.TwoWay);

    public string Text
    {
        get => (string)GetValue(TextProperty);
        set { SetValue(TextProperty, value); }
    }


    private async Task OpenModal()
    {
        // Push the modal for selecting a country
        var countrySelectionPage = new GenericDropdownPage(ItemsSource, SelectedCountry,"Search by code");
        countrySelectionPage.CountrySelected += (s, selectedCountry) =>
        {
            SelectedCountry = selectedCountry;
        };

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

    async void OpenCountiesCodeModel(System.Object sender, Microsoft.Maui.Controls.TappedEventArgs e)
    {
      await OpenModal();
    }
}

Step 3: Create the Custom Dropdown (Modal Page) ๐Ÿ“œ

A modal page can be used to display the country list:

GenericDropdownPage.Xaml

ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="PhoneEntryControl.Controls.GenericDropdownPage"
             xmlns:ff="clr-namespace:FFImageLoading.Svg.Maui;assembly=FFImageLoading.Compat.Svg.Maui"
             Title="CountryCodeDropdown">
   <Grid RowDefinitions="Auto,*" Padding="15" RowSpacing="15">

        <!-- Search Bar -->

       <Grid Grid.Row="0" ColumnDefinitions="Auto,*" ColumnSpacing="0">
           <ImageButton Grid.Column="0" Source="left_arrow" Clicked="OnBackPressed"/>

           <Border Grid.Column="1" BackgroundColor="#F0F0F0" Stroke="#E0E0E0" StrokeThickness="1" Style="{StaticResource RoundRecBorder}" Padding="5">
            <Grid ColumnDefinitions="Auto,*" ColumnSpacing="10" VerticalOptions="Center">
                <Image Source="search"
                       Grid.Column="0"
                       Margin="10,0,0,0"
                       WidthRequest="20"
                       HeightRequest="20"
                       VerticalOptions="Center" />
                <Entry x:Name="txtSearch"
                       Placeholder="Search by name or code"
                       Grid.Column="1"
                       FontSize="16"
                       Text="{Binding SearchQuery, Mode=TwoWay}"
                       VerticalOptions="Center"
                       ClearButtonVisibility="WhileEditing"
                       TextChanged="OnSearchTextChanged"/>
            </Grid>
           </Border>
           </Grid>

        <!-- Dropdown Menu -->
        <CollectionView x:Name="dropdownMenu"
                        Grid.Row="1"
                        ItemsSource="{Binding FilteredItemsSource}"
                        SelectionMode="Single"
                        SelectionChanged="OnSelectionChanged"
                        VerticalOptions="FillAndExpand">
             <CollectionView.ItemsLayout>
                 <LinearItemsLayout Orientation="Vertical" ItemSpacing="5"/>
             </CollectionView.ItemsLayout>
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <Border Style="{StaticResource RoundRecBorder}">
                        <HorizontalStackLayout Spacing="10" VerticalOptions="Center" Padding="10">
                            <!-- Country Flag -->
                            <ff:SvgCachedImage Source="{Binding Image}" 
                                   WidthRequest="40"
                                   HeightRequest="25"
                                   Aspect="AspectFit" 
                                   LoadingPlaceholder="imgloading.gif" DownsampleToViewSize="True" />

                            <!-- Country Details -->
                                <Label Text="{Binding Value}" Style="{StaticResource SmallMediumHorizontalStart}" VerticalOptions="Center"/>
                        </HorizontalStackLayout>
                    </Border>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </Grid>
</ContentPage>

GenericDropdownPage.cs

public partial class GenericDropdownPage : ContentPage
{
    public event EventHandler<Generic> CountrySelected;

    public ObservableCollection<Generic> ItemsSource { get; set; }
    public ObservableCollection<Generic> _cachedList;
    public string SearchQuery { get; set; }

    public GenericDropdownPage(ObservableCollection<Generic> itemsSource, Generic selectedCountry, string placeholder)
    {
        InitializeComponent();
        txtSearch.Placeholder = placeholder;
        ItemsSource = itemsSource;
        dropdownMenu.ItemsSource = new ObservableCollection<Generic>(itemsSource);
        _cachedList= new ObservableCollection<Generic>(itemsSource);
        BindingContext = this;
    }

    private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (e.CurrentSelection.FirstOrDefault() is Generic selectedCountry)
        {
            CountrySelected?.Invoke(this, selectedCountry);
            Navigation.PopModalAsync();
        }
    }

    private void OnSearchTextChanged(object sender, TextChangedEventArgs e)
    {
        var query = e.NewTextValue?.ToLowerInvariant();

        if (string.IsNullOrWhiteSpace(query))
        {
            // Reset to original list
            dropdownMenu.ItemsSource = new ObservableCollection<Generic>(_cachedList);
        }
        else
        {
            // Filter from the master list (_allItems) instead of already filtered data
            dropdownMenu.ItemsSource = new ObservableCollection<Generic>(
                _cachedList.Where(c => c.Value.ToLowerInvariant().Contains(query))
            );
        }
    }

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

Finally, we are done with our dropdown. It's time to add it to a page and make it work .๐Ÿ”ฅ

Finally, integrate the newly built control into any design and enjoy!

CreateAccountPage.Xaml

ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="PhoneEntryControl.Views.Registeration.CreateAccountPage"
             xmlns:customEntry="clr-namespace:PhoneEntryControl.Controls"
             Title="CountryCodeDropdown">
  <VerticalStackLayout>
    <customEntry:CustomPhoneEntry Text="{Binding MobileNumber}" ItemsSource="{Binding CountriesList}" SelectedCountry="{Binding SelectedCountry}"/>
 </VerticalStackLayout>
</ContentPage>

CreateAccountPage.cs

public partial class CreateAccountPage : ContentPage
{
    public CreateAccountPage(CreateAccountViewModel createAccountViewModel)
    {
        try
        {
            InitializeComponent();
            BindingContext = createAccountViewModel;
        }
        catch(Exception ex) { }
    }
}

CreateAccountViewModel.cs

public class CreateAccountViewModel : ObservableObject
{
       #region Properties

       [ObservableProperty]
       public Generic _selectedCountry;

       [ObservableProperty]
       public ObservableCollection<Generic> _countriesList;

       [ObservableProperty]
       public string _mobileNumber;

       #endregion

    public CountryPickerViewModel()
    {
        CountriesList = new ObservableCollection<Country>
        {
            new Country { Name = "Pakistan", Code = "+92", Flag = "pk" },
            new Country { Name = "Saudia Arabia", Code = "+966", Flag = "sa" },
            new Country { Name = "Turkey", Code = "+90", Flag = "tr" },
        };

        SelectedCountry = Countries.First();
    }
}

Conclusion ๐ŸŽฏ

By implementing a custom country code dropdown with an entry field, we enhance the user experience and provide a more intuitive phone number input system. This approach can be further customized based on specific app requirements.

Stay tuned for more awesome .NET MAUI tutorials.

Happy coding! ๐Ÿš€๐Ÿ”ฅ

ย