Creating Reusable Components with .NET MAUI

Sahu
6 min readSep 2, 2023
Photo by Sigmund on Unsplash

.NET MAUI is the latest iteration of the Open Source Cross Platform Development Framework by Microsoft. It allows Developers to use the .NET Platform to create applications that target iOS, macOS, Windows, Android, Tizen, and hopefully a whole lot more.

Succeeding Xamarin.Forms, .NET MAUI brings in various ideologies from the latest versions of .NET, few of which are — a simplified experience incorporating the simple project setup as seen in Console/WebAPI projects in the latest .NET versions, cross-platform development (development on Mac, Windows or Linux — using Visual Studio or Visual Studio Code), making the Framework open source, etc.,.

To the seasoned .NET/Xamarin.Forms/WPF developers, the experience will feel pretty familiar, albeit there will be differences waiting around the corner. For developers trying it out for the first time, I would recommend focusing on INotifyPropertyChanged , partial classes, the XAML binding syntax to understand what is exactly happening behind the scenes.

To see how to set your machine up to get started with .NET MAUI, checkout my previous article — Setting up your Mac for .NET MAUI

Creating reusable components in .NET MAUI

Like any other Framework/Library used for creating User Interfaces like Svelte, React or Angular and the like, .NET MAUI provides a way to create reusable components (sometimes called atoms) which are used to together to create larger more complex features (sometimes referred to as molecules) which eventually create the pages of the application.

.NET MAUI already comes with a long list of Pages, Layouts and Controls — you can find the list here https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls. In this article however, we’ll see how to create your own custom controls which you can then either reuse in your app or publish as a nuget package for others to use.

Note: I’ll be using components and controls interchangeably in this article. They mean the same thing. I’m just used to calling them components due to my frequent use of them in Svelte.

What’re we building?

A common component that comes to mind is a RatingInput component, which is used in many applications to gather feedback from users. So, we’ll build something that looks like this —

RatingInput component/control

So the RatingInput control’s user story can be summarised as:

As a user of the application, I want to be able to display
a rating using the RatingInput control. The control should allow me to
tap on stars to set a rating value. Additionally, I want the ability
to execute a command when the rating is changed by tapping a star.

Acceptance Criteria:

Scenario: Display Rating
Given I have a RatingInput control on the page
When I tap on a star in the RatingInput control
Then the selected rating should be displayed using hollow and filled stars.

Scenario: Handle Rating Change
Given I have a RatingInput control on the page
When I tap on a star in the RatingInput control
The RatingChangedCommand, if provided, should be executed.

Okay, let’s start building. All the code is available here — https://github.com/mrsauravsahu/ss-maui-controls

  1. If you don’t have a .NET MAUI solution already, create one. In the codebase I have shared, I have also gone ahead and separated the control to a different project so I can easily publish it to Nuget if I want.
  2. We need two files for any Control. The xaml and the xaml.cs — I guess you can skip xaml if you use C# for your UI. I’m also creating another file for a ViewModel (you can indeed skip this; but this makes the control very separated from logic and it makes Unit Testing easy)

Before we look at the UI, I like to think about controls in terms how they will be consumed, which gives an idea of the data they need to display. Our control needs a List of Stars and a place to hold the function to call which is passed from the consumer of the control.

I want my control to be used like this, just a RatingInput and I should be able to pass the Command to run when the rating changes.

<ss:RatingInput x:Name="MyRatingInput"
RatingChangedCommand="{Binding RatingChangedCommand}"
/>

So, the ViewModel has an ObservableCollection of Stars (ObservableCollection because whenever these stars change, we need to update the UI) and a Command (I’m using the community Toolkit, but the consumer of the control doesn’t have to.)

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

namespace SS.Maui.Controls;

public partial class RatingInputViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject
{

private ObservableRangeCollection<Rating> stars;

public ObservableRangeCollection<Rating> Stars
{
get
{
if (stars is not null) return stars;

var starsList = Enumerable.Range(1, 5)
.Select(p => new Rating
{
Value = p,
IsRated = false
});

stars = new();
stars.AddRange(starsList);

return stars;
}
}

private int currentRating = 0;
public int CurrentRating
{
set
{
if (value == currentRating) return;

var starsList = Enumerable.Range(1, 5)
.Select(p => new Rating
{
Value = p,
IsRated = p <= value
});

currentRating = value;

Stars.Clear();
Stars.AddRange(starsList);

OnPropertyChanged(nameof(Stars));
OnPropertyChanged(nameof(CurrentRating));
}
}

public ICommand ExternalRatingChangedCommand { get; set; }


[RelayCommand]
private void RatingChanged(Rating rating)
{
this.CurrentRating = rating.Value;
ExternalRatingChangedCommand?.Execute(rating);
}

}

This might look daunting at first, but essentially

  • The Stars ObservableCollection is used to decide which stars are hollow and which are filled.
  • RatingChanged is the command that will run whenever a star is tapped. So I update the CurrentRating and call the user provided Command with the current Rating.
  • Because in the future, the consumer might need to know what the CurrentRating is, I’m also going ahead and creating a property for it.

The XAML file looks like this —

  • I’m creating the Stars using a StackLayout (horizontally).
  • Whenever an Image is Tapped, I use a TapGestureRecognizer I run the RatingChangedCommand in my ViewModel
  • The most important part of this XAML file is the BindingContext. Because the BindingContext will actually come from where this RatingInput is rendered, I override it to use the ViewModel instead, which is a property on my RatingInput control — you’ll see this in the backing C# file.
  • For the images, I have used some basic stars png files, one for hollow and the other one for filled.
<?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"
xmlns:local="clr-namespace:SS.Maui.Controls"
x:Class="SS.Maui.Controls.RatingInput">

<StackLayout BindableLayout.ItemsSource="{Binding Stars}"
Orientation="Horizontal"
HorizontalOptions="FillAndExpand"
BindingContext="{Binding Source={RelativeSource AncestorType={x:Type local:RatingInput}}, Path=ViewModel}"
x:DataType="{x:Type local:RatingInputViewModel}">
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="{x:Type local:Rating}">
<Image HeightRequest="30"
WidthRequest="30"
Source="star.png"
HorizontalOptions="CenterAndExpand">
<Image.Triggers>
<DataTrigger TargetType="Image" Binding="{Binding IsRated}" Value="True">
<Setter Property="Source" Value="star_filled.png" />
</DataTrigger>
<DataTrigger TargetType="Image" Binding="{Binding IsRated}" Value="False">
<Setter Property="Source" Value="star_hollow.png" />
</DataTrigger>
</Image.Triggers>

<Image.GestureRecognizers>
<TapGestureRecognizer NumberOfTapsRequired="1"
Command="{Binding Source={RelativeSource AncestorType={x:Type local:RatingInputViewModel}}, Path=RatingChangedCommand}"
CommandParameter="{Binding}"/>
</Image.GestureRecognizers>
</Image>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>


</ContentView>

The backing C# file creates the BindableProperty, instantiates the ViewModel and connects the two using propertyChanged parameter.

Any parameter you want to expose to the consumer of your control should be a BindableProperty.

using System.Windows.Input;

namespace SS.Maui.Controls;

public partial class RatingInput : ContentView
{
public RatingInputViewModel ViewModel { get; set; }

public RatingInput()
{
InitializeComponent();
this.ViewModel = new RatingInputViewModel();
}

public static readonly BindableProperty CurrentRatingProperty =
BindableProperty.Create(
"CurrentRating",
typeof(int),
typeof(RatingInput),
0,
propertyChanged: (BindableObject bindable, object oldValue, object newValue) =>
{
{
if (bindable is RatingInput ratingInput)
{
ratingInput.ViewModel.CurrentRating = (int)newValue;
}
}
});

public static readonly BindableProperty RatingChangedCommandProperty =
BindableProperty.Create(
"RatingChangedCommand",
typeof(ICommand),
typeof(RatingInput),
null,
BindingMode.TwoWay,
propertyChanged: (BindableObject bindable, object oldValue, object newValue) =>
{
{
if (bindable is RatingInput ratingInput)
{
ratingInput.ViewModel.ExternalRatingChangedCommand = (ICommand)newValue;
}
}
});

}

So, that’s the RatingInput control complete, it looks like this.

Using the RatingInput control

Even though this code might look like a lot, but most of it is just boilerplate stuff. And I’m planning to make use of Source Generators to automate some parts of this, that might be in a future article.

Now using the same principles, you can create any type of reusable control in .NET.

The codebase I shared earlier (ss-maui-controls) will eventually have all the controls I want to publish to Nuget so make sure you star it! And I’m also working on a couple of helper projects for MAUI, so you’d like to be updated, be sure to follow me on YouTube and GitHub.

Are you working on .NET MAUI as well? Would love to hear about it, comment down below!

— S

--

--

Sahu

Personal Opinions • Beyond Full Stack Engineer @ McKinsey & Company