Mastering Custom Controls in .NET MAUI

Mastering Custom Controls in .NET MAUI: Build Beautiful, Reusable UI Elements from Scratch

Are you still writing the same XAML over and over for your app’s UI? What if I told you that one powerful .NET MAUI feature could save you dozens of hours while keeping your UI clean, consistent, and scalable? Let’s dive deep into creating custom controls in .NET MAUI and learn how to build polished, reusable UI components that feel native on every platform.

Understanding Custom Controls in .NET MAUI

What Are Custom Controls?

Custom controls are UI elements you define yourself, from scratch, often with encapsulated behavior, appearance, and logic. Unlike user controls (which are more like reusable UI composites) or templates (which define look-and-feel), custom controls extend the core rendering pipeline and integrate deeply into the framework.

In short:

  • Custom Controls = new UI components built from scratch.
  • User Controls = compositions of existing controls.
  • Templates = visual customization of existing controls.

Why Use Custom Controls?

Use custom controls when:

  • You need cross-platform, reusable components.
  • Your logic/UI is repeated in many places.
  • You want to expose properties for easy binding (MVVM).
  • You need full control over rendering and platform behavior.

For example, a rating star component, color picker, or notification banner might be ideal as a custom control.

Key Components of a MAUI Custom Control

To build a full-featured MAUI control, you’ll often use:

  • Handlers: Bridge between your control and the native platforms.
  • Renderers (legacy, pre-MAUI): Used in Xamarin.Forms.
  • Bindable Properties: To support data binding and MVVM.
  • Styling & Visual States: For theme and state management.

Building Your First Custom Control

Planning the Control

Before coding, define:

  • Purpose: What problem does this control solve?
  • Design: How should it look and behave?
  • Usage: How will other developers use it?

Let’s build a CustomBadge control to display labeled badges with optional icons.

Creating the Control Class

public class CustomBadge : GraphicsView
{
    public CustomBadge()
    {
        Drawable = new BadgeDrawable(this);
    }
}

This class inherits GraphicsView, perfect for custom drawing. We assign a custom BadgeDrawable to handle rendering.

Adding Bindable Properties

public static readonly BindableProperty TextProperty =
    BindableProperty.Create(nameof(Text), typeof(string), typeof(CustomBadge), string.Empty);

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

Now Text can be data-bound from your ViewModel, which is essential for MVVM.

Customizing Appearance and Behavior

In BadgeDrawable:

public class BadgeDrawable : IDrawable
{
    private readonly CustomBadge _badge;

    public BadgeDrawable(CustomBadge badge) => _badge = badge;

    public void Draw(ICanvas canvas, RectF dirtyRect)
    {
        canvas.FillColor = Colors.CornflowerBlue;
        canvas.FillRoundedRectangle(dirtyRect, 8);

        canvas.FontSize = 14;
        canvas.FontColor = Colors.White;
        canvas.DrawString(_badge.Text, dirtyRect, HorizontalAlignment.Center, VerticalAlignment.Center);
    }
}

This paints a rounded badge with text centered in the middle.

Advanced Custom Control Techniques

Composing Controls Using XAML

Sometimes it’s easier to define visuals in XAML:

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             x:Class="MyControls.FancyButton">
    <Frame BackgroundColor="LightGray" CornerRadius="12">
        <Label x:Name="buttonLabel" Text="Click Me!" HorizontalOptions="Center" VerticalOptions="Center"/>
    </Frame>
</ContentView>

This is useful for composite visuals but can expose bindable properties too.

Creating Platform-Specific Customizations

Add platform logic using partial classes or conditional compilation:

#if ANDROID
    // Android-specific behavior
#elif IOS
    // iOS-specific behavior
#endif

Or register handlers:

#if ANDROID
Microsoft.Maui.Handlers.ButtonHandler.Mapper.AppendToMapping("MyCustom", (handler, view) =>
{
    // custom behavior
});
#endif

Performance Considerations

  • Use GraphicsView instead of SkiaSharp for simpler needs.
  • Avoid layout thrashing: minimize InvalidateMeasure().
  • Debounce property change notifications.

Reusability and Distribution

Packaging Controls into Reusable Libraries

  • Use .NET MAUI Class Library project.
  • Isolate platform-specific parts using folders: Platforms/Android, Platforms/iOS, etc.
  • Use InternalsVisibleTo for internal sharing between test projects.

Publishing to NuGet

Steps:

  1. Add metadata in .csproj.
  2. Use dotnet pack to generate .nupkg.
  3. Push to NuGet using dotnet nuget push.

Versioning and Documentation Best Practices

  • Follow Semantic Versioning.
  • Use XML comments and DocFX for documentation.
  • Create sample projects to demonstrate usage.

Real-World Examples and Use Cases

Custom Slider with Icons

Imagine a volume slider where each stop has an icon:

  • Combine a Slider with custom tick rendering.
  • Add icons using AbsoluteLayout.

CustomSliderWithIcons.xaml

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             x:Class="CustomControls.CustomSliderWithIcons">
    <Grid>
        <Slider x:Name="SliderControl"
                Minimum="0"
                Maximum="4"
                ValueChanged="Slider_ValueChanged" />

        <Grid ColumnDefinitions="*,*,*,*,*"
              VerticalOptions="Center"
              HorizontalOptions="FillAndExpand"
              InputTransparent="True">
            <Image Source="icon1.png" Grid.Column="0" HeightRequest="24" WidthRequest="24" HorizontalOptions="Center" />
            <Image Source="icon2.png" Grid.Column="1" HeightRequest="24" WidthRequest="24" HorizontalOptions="Center" />
            <Image Source="icon3.png" Grid.Column="2" HeightRequest="24" WidthRequest="24" HorizontalOptions="Center" />
            <Image Source="icon4.png" Grid.Column="3" HeightRequest="24" WidthRequest="24" HorizontalOptions="Center" />
            <Image Source="icon5.png" Grid.Column="4" HeightRequest="24" WidthRequest="24" HorizontalOptions="Center" />
        </Grid>
    </Grid>
</ContentView>

CustomSliderWithIcons.xaml.cs

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

    private void Slider_ValueChanged(object sender, ValueChangedEventArgs e)
    {
        // Respond to slider changes here
        Console.WriteLine($"Slider value: {e.NewValue}");
    }
}

Calendar Control with Custom Events

Use CollectionView to render dates:

  • Bind each cell to a date object.
  • Highlight events using background color or badges.

EventCalendar.xaml

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             x:Class="CustomControls.EventCalendar">
    <CollectionView x:Name="CalendarView"
                    ItemsLayout="UniformGrid, 7"
                    SelectionMode="None">
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <Frame Padding="5" Margin="2" BackgroundColor="{Binding EventColor}">
                    <Label Text="{Binding Date}" FontSize="12" HorizontalOptions="Center" />
                </Frame>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</ContentView>

EventCalendar.xaml.cs

public partial class EventCalendar : ContentView
{
    public ObservableCollection<CalendarDate> Dates { get; set; } = new();

    public EventCalendar()
    {
        InitializeComponent();
        CalendarView.ItemsSource = Dates;
    }
}

public class CalendarDate
{
    public string Date { get; set; }
    public Color EventColor { get; set; } = Colors.Transparent;
}

Reusable Notification Banner

Build a banner that:

  • Animates in/out.
  • Binds to a message queue.
  • Uses TranslateTo and FadeTo for transitions.

NotificationBanner.xaml

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             x:Class="CustomControls.NotificationBanner"
             IsVisible="False">
    <Frame BackgroundColor="DodgerBlue"
           Padding="10"
           CornerRadius="6"
           HorizontalOptions="FillAndExpand">
        <Label x:Name="MessageLabel" TextColor="White" />
    </Frame>
</ContentView>

NotificationBanner.xaml.cs

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

    public async Task ShowMessageAsync(string message, int duration = 3000)
    {
        MessageLabel.Text = message;
        IsVisible = true;
        await this.FadeTo(1).ContinueWith(async _ =>
        {
            await Task.Delay(duration);
            await this.FadeTo(0);
            IsVisible = false;
        });
    }
}

Use this with a view model or message queue to show transient notifications on screen.

FAQ: Common Questions on MAUI Custom Controls

When should I use a custom control vs. a user control?

Use custom controls for deep customization or new components. Use user controls for combining existing ones.

Can I use XAML in custom controls?

Yes, especially when you inherit from ContentView or Layout.

How do I handle gestures or events?

Override touch handlers or use GestureRecognizer depending on control base class.

Conclusion: Make Your UI Smarter, Not Harder

Creating custom controls in .NET MAUI is like building your own LEGO pieces for cross-platform apps. They save time, enforce consistency, and open up endless customization. So what are you waiting for? Start building smarter UI blocks today and level up your MAUI development game.

Have you tried building a custom control? Share your experience or ask your questions in the comments!

Leave a Reply

Your email address will not be published. Required fields are marked *