-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Porting Custom Renderers to Handlers
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.
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 map cross-platform controls to native controls on each platform:
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:
- Create an interface, implementing
IView
. - Create a subclass of the
ViewHandler
class that renders the native control. - In the
ViewHandler
subclass, override theCreateNativeView
method that renders the native control. - Create the
PropertyMapper
dictionary, which handles what actions to take when property changes occur. - Create a custom control by subclassing the
View
class and implementing the control interface. - Register the handler using the
AddHandler
method in theMauiProgram
class.
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 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
andNewElement
.NET MAUI simplifies and distributes everything that we did previously in the OnElementChanged
method in different methods in a simpler way.
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);
}
}
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
}
}
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.
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.