How to create custom controls in .NET MAUI

.NET MAUI is a powerful cross-platform mobile app framework by Microsoft. It is a reliable feature-enriched option for developers of all types of mobile applications. Its community toolkit, XAML-based designing, and modern C# coding support stand out from MAUI among its competitors. One key feature it provides is the creation of reusable custom controls. As a newer framework, many people find defining such controls difficult. In this blog post, I will show you in detail how you can create reusable custom controls throughout the application.  

How to create custom controls in .NET MAUI

For this post, I will use an example of creating a custom button with properties like text, width, and command. Let's jump right into the steps required to do this.

Step 1: Create an MAUI project

I'll use the normal MAUI template available through Visual Studio and name it 'CustomControl':

Create project

When the project has been successfully created, it can be run using the built-in emulator:

Hello world application

Step 2: Create a controls folder to keep custom controls

As we are following Model-View-ViewModel (MVVM), I have created a controls folder where I will save the custom control:

Controls folder

Step 3: Create ContentView

Right-click on the control directory and add a .NET MAUI ContentView (XAML) item as shown here:

Create ContentView

After the ContentView is created, you will see two files, one XAML file that is the visual design (UI) and one xaml.cs file containing logic:

Created files

This corresponds the structure that you may know from other project types utilizing XAML like WPF. A default Label was created:

<!-- GradientButton.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="CustomControl.Views.controls.GradientButton">
  <VerticalStackLayout>
    <Label
      Text="Welcome to .NET MAUI!"
      VerticalOptions="Center"
      HorizontalOptions="Center" />
  </VerticalStackLayout>
</ContentView>

And 

// GradientButton.xaml.cs

namespace CustomControl.Views.controls;

public partial class GradientButton : ContentView
{
    public GradientButton()
    {
        InitializeComponent();
    }
}

Step 4: define custom designer and properties 

Now, we need to modify the design. Change the GradientButton.xaml to:

<?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="CustomControl.Views.controls.GradientButton">
  <VerticalStackLayout>
    <Button
      x:Name="btnGradient"
      Text=""
      VerticalOptions="Center"
      HorizontalOptions="Center" />
  </VerticalStackLayout>
</ContentView>

I have replaced the label with the button that we want to customize for the rest of this post. The button is given a name by adding the x:Name="btnGradient" attribute. Change the code-behind file to:

namespace CustomControl.Views.controls;

public partial class GradientButton : ContentView
{
    public static readonly BindableProperty TextProperty =
        BindableProperty.Create(
            nameof(Text),
            typeof(string),
            typeof(GradientButton),
            string.Empty);

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

    public GradientButton()
    {
        InitializeComponent();
        btnGradient.BindingContext = this;
        btnGradient.SetBinding(Button.TextProperty, new Binding(nameof(Text), source: this));
    }
}

In the code-behind file, A property named TextProperty of type BindableProperty is defined to get input when the GradientButton control is called. This allows the Text property to be bound in XAML or code to a data source and updates the UI whenever the property changes. Later, a Text property is defined to encapsulate the BindableProperty.

I set the value of the Text property to that button in the constructor:

btnGradient.SetBinding(Button.TextProperty, new Binding(nameof(Text), source: this));

Step 5: Use the control on a screen

To use control on a screen, first import it. I am doing it on the main page by including two instances of the custom button with different texts:

<?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:controls="clr-namespace:CustomControl.Views.controls"
             x:Class="CustomControl.MainPage">
  <!-- ... -->
  <controls:GradientButton Text="Login" />
  <controls:GradientButton Text="Register" />

Step 6: run the project 

After building and running the project, you can now see two custom controls added to the screen:

Custom controls added

Yay! The custom control successfully shows up on the app. Let's enrich the button with even more custom features. 

Step 7: define a coloring resource in the app

To define application theme color, we can define it in the App.xaml file:

<Application.Resources>
  <ResourceDictionary>
    <ResourceDictionary.MergedDictionaries>
      <ResourceDictionary Source="Resources/Styles/Colors.xaml" />
      <ResourceDictionary Source="Resources/Styles/Styles.xaml" />
    </ResourceDictionary.MergedDictionaries>
    <Color x:Key="DefaultWhite">#FFFFFF</Color>
    <Color x:Key="DefaultLightBlue">#0066FF</Color>
  </ResourceDictionary>
</Application.Resources>

Here I defined two new colors named DefaultWhite and DefaultLightBlue. These colors can be used in the GradientButton control by referencing them as static resources:

<Button
  x:Name="btnGradient"
  Text=""
  TextColor="{StaticResource DefaultWhite}"
  BackgroundColor="{StaticResource DefaultLightBlue}"
  VerticalOptions="Center"
  HorizontalOptions="Center" />

When launching the project, you will see that the styling of the buttons changed:

Updated buttons

I know that it may be a bit misleading that I named the button GradientButton since the current styling is in fact just a solid color. However since this blog post quickly grew in size, and since the blog post is not about creating gradient styling in XAML, I decided to stick with a single color. If there's any interest, I can write a separate post about how to create gradient styling in XAML.

Step 8: add Width Property

We can similarly add properties that will be set when the control is used. Change the GradientButton.xaml.cs into the following code to include a Width property: 

public partial class GradientButton : ContentView
{
    public static readonly BindableProperty TextProperty =
        BindableProperty.Create(
            nameof(Text),
            typeof(string),
            typeof(GradientButton),
            string.Empty);

    public static readonly BindableProperty WidthProperty =
        BindableProperty.Create(
            nameof(Width),
            typeof(double),
            typeof(GradientButton),
            double.NaN);

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

    public double Width
    {
        get => (double)GetValue(WidthProperty);
        set => SetValue(WidthProperty, value);
    }

    public GradientButton()
    {
        InitializeComponent();
        btnGradient.BindingContext = this;
        btnGradient.SetBinding(Button.TextProperty, new Binding(nameof(Text), source: this));
        btnGradient.SetBinding(Button.WidthRequestProperty, new Binding(nameof(Width), source: this));
    }
}

The Width property can be used just like the Text property:

<controls:GradientButton Text="Login" Width="50" />
<controls:GradientButton Text="Register" Width="250" />

When running the project, we will see different widths on each button just as expected:

Button width

Step 9: Define ICommand for events

Until now, we have added properties for our custom control that changes the design on the UI. Now, let's add a command that will bind with control when control is used. Whenever users click a GradientButton, the specific bound command will be executed:

public static readonly BindableProperty OnCommandProperty =
    BindableProperty.Create(
        nameof(OnCommand),
        typeof(ICommand),
        typeof(GradientButton));

public ICommand OnCommand
{
    get => (ICommand)GetValue(OnCommandProperty);
    set => SetValue(OnCommandProperty, value);
}

private void CommandExecuted(object sender, EventArgs e)
{
    if (OnCommand != null && OnCommand.CanExecute(null))
    {
        OnCommand.Execute(null);
    }
}

In the GradientButton constructor:

btnGradient.SetBinding(Button.CommandProperty, new Binding(nameof(OnCommand), source: this));
btnGradient.Clicked += CommandExecuted;

The OnCommandProperty of type ICommand is declared as a bindable property for the OnCommand value. Its default value is omitted, which means null is the default. It is set when this component is bound with some command (LoginCommand and RegisterCommand in our case) that gets executed when the button is clicked.

The OnCommand property encapsulates OnCommandProperty in a way that is similar to what Text is doing with TextPropertyCommandExecuted handles button click event. 

  • It first checks if the OnCommand is not null.
  • Verifies if the command can be executed using CanExecute(null).
  • Executes the command with Execute(null).

Step 10: Define ViewModel for Page

Add a new folder named ViewModels:

ViewModels folder

And inside this folder, add a new class named MainViewModel:

Add MainViewModel.cs

Step 11: Install CommunityToolkit.Mvvm

To implement MVVM for this project, I'll use the CommunityToolkit.Mvvm NuGet package. I won't go into more details about this package in this post, other than to show how it can simplify developing MVVM in MAUI. The package can be installed through Visual Studio:

Add CommunityToolkit.Mvvm

Or if you prefer the command line:

dotnet add package CommunityToolkit.Mvvm

Step 12: import ObjservableObject from MVVM and implement it in the ViewModel

In the created MainViewModel class, include the following code:

using CommunityToolkit.Mvvm.ComponentModel;
using System.Windows.Input;

namespace CustomControl.ViewModels
{
    public partial class MainViewModel : ObservableObject
    {
        public ICommand LoginCommand { get; }
        public ICommand RegisterCommand { get; }

        public MainViewModel()
        {
            // Initialize the LoginCommand
            LoginCommand = new Command(Login);
            RegisterCommand = new Command(Register);
        }

        private void Login()
        {
            Shell.Current.DisplayAlert("Message", "You successfully logged in", "Ok");
        }

        private void Register()
        {
            Shell.Current.DisplayAlert("Message", "You successfully Registered", "Ok");
        }
    }
}

The model wraps the two commands that we will need for when the user clicks the buttons in the UI. For now, I will simply display an alert in the screen when each button is clicked.

Step 13: Bind the view model with the MainPage

To bind the VM with a page, we need to add these two lines to the ContentPage tag:

xmlns:viewmodel="clr-namespace:CustomControl.ViewModels"
x:DataType="viewmodel:MainViewModel"

And the controls OnCommand attribute binds with the commands from the view model:

<controls:GradientButton OnCommand="{Binding LoginCommand}" Text="Login" Width="100" />
<controls:GradientButton OnCommand="{Binding RegisterCommand}" Text="Register" Width="150" />

In the code-behind of MainPage include the following code:

using CustomControl.ViewModels;

namespace CustomControl.Views;

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
        BindingContext = new MainViewModel();
    }
}

Step 14: Run the project

After successfully running, we can observe that LoginCommand and RegisterCommand are executed on the Login and Register buttons, respectively:

Run final project
Message popup
Second message popup

Conclusion 

.NET MAUI is a popular cross-platform mobile development framework. Due to its ease of development and set of features, many developers opt for it for their small to large-scale applications. One useful feature provided by MAUI is the creation of reusable controls. With such controls, you can define a control in one place and reuse it within the application. We discussed one example of GradientButton in depth and learned how we can create custom controls to meet our needs. 

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website