Skip to content

Porting Custom Renderers to Handlers

David Ortinau edited this page Apr 30, 2022 · 7 revisions

In this article we are going to learn and understand the necessary changes to port a custom renderer from a Xamarin.Forms app to a handler in a .NET MAUI app. Alternatively, you can learn here about using custom renderers in .NET MAUI, though there are several benefits to using handlers including improved performance and extensibility.

Let's begin by learning about the .NET MAUI architecture.

.NET MAUI Architecture

The new handler architecture in .NET MAUI removes view wrapping, thus reducing the number of UI controls needed to render a view. It also fundamentally decouples the platform controls from the framework itself.

In Xamarin.Forms, each renderer has a reference to the cross-platform element and often relies on INotifyPropertyChanged to properly work. Rather than using these renderers, .NET MAUI introduces a new pattern called a Handler.

With Handlers, the relationship between the framework and the platform is now inverted; the platform control only needs to handle the needs of the framework. Not only is this more efficient, but it is much easier to extend or override when needed. Gone are the days of needing to create Custom Renderers or Effects. The new architecture also makes the platform handlers much more suitable for reuse by other frameworks such as Fabulous, and experiments like Comet.

.NET MAUI Handlers

.NET MAUI handlers map cross-platform controls to native controls on each platform:

Custom Handler

For example, on iOS a .NET MAUI handler will map a .NET MAUI Button to an iOS UIButton. On Android, the Button will be mapped to an AppCompatButton:

The process for migrating to a handler class is as follows:

  1. Create an interface, implementing IView.
  2. Create a subclass of the ViewHandler class that renders the native control.
  3. In the ViewHandler subclass, override the CreateNativeView method that renders the native control.
  4. Create the PropertyMapper dictionary, which handles what actions to take when property changes occur.
  5. Create a custom control by subclassing the View class and implementing the control interface.
  6. Register the handler using the AddHandler method in the MauiProgram class.

Example

Create an interface, implementing IView:

public interface ICustomEntry : IView
{
    public string Text { get; }
    public Color TextColor { get; }
}

Create a subclass of the ViewHandler class that renders the native control:

public partial class CustomEntryHandler : ViewHandler<ICustomEntry, EditText>
{
    
}

Note: This is an example for an Android EditText.

In the CustomEntryHandler class, override the CreateNativeView method that renders the native control:

public partial class CustomEntryHandler : ViewHandler<ICustomEntry, EditText>
{
    protected override EditText CreateNativeView()
    {
        return new EditText(Context);
    }
}

Note: This is an example for an Android EditText.

Create the PropertyMapper dictionary, which handles what actions to take when property changes occur:

public partial class CustomEntryHandler : ViewHandler<ICustomEntry, EditText>
{
    public static PropertyMapper<ICustomEntry, CustomEntryHandler> CustomEntryMapper = new PropertyMapper<ICustomEntry, CustomEntryHandler>(ViewHandler.ViewMapper)
    {
        [nameof(ICustomEntry.Text)] = MapText,
        [nameof(ICustomEntry.TextColor)] = MapTextColor,
    };
    
    protected override EditText CreateNativeView()
    {
        return new EditText(Context);
    }
    
    static void MapText(EntryHandler handler, ICustomEntry entry)
    {
        handler.NativeView?.Text = entry.Text;
    }
  
    static void MapTextColor(EntryHandler handler, ICustomEntry entry)
    {
        handler.NativeView?.TextColor = entry.TextColor;
    }
}

Create a custom control by subclassing the View class and implementing the control interface:

public class CustomEntry : View, ICustomEntry
{
    public string Text { get; set; }
    public Color TextColor { get; set; }
}

Register the handler using the AddHandler method in the MauiProgram class:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder.UseMauiApp<App>();

        builder.ConfigureMauiHandlers(collection =>
        {
//#if __ANDROID__
            handlers.AddHandler(typeof(CustomEntry), typeof(CustomEntryHandler));
//#endif
        });

        return builder.Build();
    }
}

Handlers' Performance Improvements over Custom Renderers

Handlers are accessed through a control-specific interface provided derived from IView. This avoids the cross-platform control needing to reference its handler, and the handler needing to reference the cross-platform control. The mapping of the cross-platform control API to the platform API is provided by a Mapper.

It seems like a trivial change, moving from ViewRenderer to ViewHandler, but it's much more!

In Xamarin.Forms, ViewRenderer creates a parent Element. On Android, for example, a ViewGroup is created which was used for auxiliary positioning tasks. In .NET MAUI, ViewHandler does not create any parent Element, helping to reduce the visual hierarchy and improve your app's performance.

Remember how many tasks OnElementChanged handled in Xamarin.Forms? This method:

  • Created the native control
  • Initialized default values
  • Subscribed to events
  • Handled OldElement and NewElement

.NET MAUI simplifies and distributes everything that we did previously in the OnElementChanged method in different methods in a simpler way.

Subscribe to events

The ViewHandler class also has methods like ConnectHandler and DisconnectHandler. ConnectHandler is the ideal place to initialize and subscribe to events. DisconnectHandler is great for disposing objects and unsubscribing from events:

public partial class CustomEntryHandler : ViewHandler<ICustomEntry, EditText>
{
    // ...

    protected override void ConnectHandler(EditText nativeView)
    {
        _defaultTextColors = nativeView.TextColors;
        _defaultPlaceholderColors = nativeView.HintTextColors;

        _watcher.Handler = this;
        nativeView.AddTextChangedListener(_watcher);

        base.ConnectHandler(nativeView);
    }

    protected override void DisconnectHandler(EditText nativeView)
    {
        nativeView.RemoveTextChangedListener(_watcher);
        _watcher.Handler = null;

        base.DisconnectHandler(nativeView);
    }
}

Property Mappers

The PropertyMapper is a new concept introduced by Handlers. It is a Dictionary that maps the interface's Properties to their associated Actions. It is defined in the interface of our control and it replaces everything that was done in OnElementPropertyChanged in Xamarin.Forms:

public static PropertyMapper<ICustomEntry, CustomEntryHandler> CustomEntryMapper = new PropertyMapper<ICustomEntry, CustomEntryHandler>(ViewHandler.ViewMapper)
{
    [nameof(ICustomEntry.Text)] = MapText,
    [nameof(ICustomEntry.TextColor)] = MapTextColor
};

static void MapText(EntryHandler handler, ICustomEntry entry)
{
    handler.NativeView?.Text = entry.Text;
}

static void MapTextColor(EntryHandler handler, ICustomEntry entry)
{
    handler.NativeView?.TextColor = entry.TextColor;
}

The Mapper, in addition to simplifying how .NET MAUI handles PropertyChanged events, also provides more extensibility options. For example, we can choose to modify the Mapper anywhere in our code:

using Microsoft.Maui;
using Microsoft.Maui.Controls;

public partial class App : Application
{
    public App()
    {
        InitializeComponent();

#if __ANDROID__
        CustomEntryMapper.AppendToMapping(nameof(ICustomEntry.Text), (handler, view) =>
        {
            (handler.NativeView as Android.Widget.EditText)?.Text = view.text + "custom";
        });
#endif
    }
}

Register the Handler

The MauiProgram class contains a CreateMauiApp method in which the custom handler must be registered:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder.UseMauiApp<App>();

        builder.ConfigureMauiHandlers(collection =>
        {
#if __ANDROID__
            handlers.AddHandler(typeof(CustomEntry), typeof(CustomEntryHandler));
#endif
        });

        return builder.Build();
    }
}

The handler is registered with the AddHandler method. This allows us to avoid Xamarin.Forms' Assembly Scanning technique, which was slow and expensive.

Other changes

As a general rule, all XAML-related concepts in Xamarin.Forms will work without requiring changes in .NET MAUI, except changes in namespaces. In this way, concepts such as Behaviors, Converters, and Triggers have exactly the same API.

Clone this wiki locally