Migrating MR.Gestures from Xamarin.Forms to .NET MAUI

Michael Rumpler

Note: This is a Guest Blog Post by Michael Rumpler. Michael has been a freelance C# developer since 2003, switching from web to mobile in 2014 upon the introduction of Xamarin.Forms.

Let’s first provide a brief introduction to the MR.Gestures Library to explain why it exists and the problem it solves.

When Xamarin.Forms was released in 2014 it only provided TapGestureRecognizer which had to be added to a GestureRecognizers collection. This implementation was always a bit of a code-smell for me. It copied the iOS + Android APIs, but iOS and Android (at the time) only used that architecture because Objective-C and Java didn’t support events. However, In .NET and C# we always used event and ICommand for these scenarios.

I created the MR.Gestures library to close this gap. It provides events and ICommands for gestures on each and every control (all Views, Layouts and ContentPages) which you can leverage to respond to any touch (and mouse) events.

Before MR.Gestures, we would write this in XAML to handle a tapped event on a Label:

<Label Text="{Binding Text}">
    <Label.GestureRecognizers>
        <TapGestureRecognizer Tapped="Label_Tapped" />
    </Label.GestureRecognizers>
</Label>

But with MR.Gestures, our XAML now looks like this:

<mr:Label Text="{Binding Text}" Tapped="Label_Tapped" />

And, more importantly, there are 17 other touch (and mouse) events which MR.Gestures supports. Each event’s respective EventArgs also provide more information than Microsoft’s GestureRecognizers.

Now that the .NET MAUI engineering team has published their release candidate, the time has come to migrate MR.Gestures from Xamarin.Forms to .NET MAUI.

Starting the Migration

We started by creating a new project in Visual Studio using the .NET MAUI Class Library template. The template creates one single project (csproj) using multi-targeting to target all platforms.

This template also includes a Platforms folder with subfolders for every platform, Android, iOS, MacCatalyst and Windows. But it does NOT configure the contents of any folder named Android, iOS, MacCatalyst or Windows to be compiled only on their respective platform. For this we need to copy a few lines from the official .NET MAUI Multi-Targeting documentation to our csproj file.

I simplified them a bit so that this is enough:

<ItemGroup Condition="!$(TargetFramework.StartsWith('net6.0-android'))">
    <Compile Remove="**/*.Android.cs" />
    <Compile Remove="**/Android/**/*.cs" />
</ItemGroup>

<ItemGroup Condition="!$(TargetFramework.StartsWith('net6.0-ios')) AND !$(TargetFramework.StartsWith('net6.0-maccatalyst'))">
    <Compile Remove="**/*.iOS.cs" />
    <Compile Remove="**/iOS/**/*.cs" />
</ItemGroup>

<ItemGroup Condition="!$(TargetFramework.Contains('-windows'))">
    <Compile Remove="**/*.Windows.cs" />
    <Compile Remove="**/Windows/**/*.cs" />
</ItemGroup>

<ItemGroup>
    <None Include="**/*" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder);$(Compile)" />
</ItemGroup>

The first ItemGroup makes sure that any files which end in .Android.cs or are in an Android folder are only compiled if the current TargetFramework is net6.0-android.

The second and third do that for iOS, MacCatalyst and Windows respectively.

And the last one is a fix for VS so that the solution explorer still shows all the files which are only compiled on some platforms.

With this configuration completed, let’s start coding!

Migrating The Controls

Migrating my Xamarin.Forms controls to .NET MAUI was easy. I just changed the base class of my types from Xamarin.Forms.* to Microsoft.Maui.Controls.*, eg Microsoft.Maui.Controls.Label. All of the properties and events in those classes stayed the same.

As you can imagine, this is a lot of code. In Xamarin.Forms, MR.Gestures had 34 classes, each with 18 events, commands and command parameters.

This is a lot of repetitive code which I don’t want to write by hand. So I use a T4 template to generate it. When I first created MR.Gestures in 2014, my template had 110 lines and generated 19,000 lines of code. Over time, with additional events, additional controls, and now with .NET MAUI support, I also added a few controls. Now my T4 template has 188 lines and generates 30,000 lines of C#.

Migrating To Handlers

In Xamarin.Forms, each of my cross-platform controls has a Custom Renderer which implements the platform-specific touch handling logic. In the renderers I override OnElementChanged, OnElementPropertyChanged, Dispose and on Android also DispatchTouchEvent and DispatchGenericMotionEvent.

In .NET MAUI, Custom Renderers are replaced by Handlers. A Handler still has the same purpose as a Custom Renderer – it synchronizes changes between the cross-platform control and the platform-specific implementation – but the Handler does not inherit from the platform view; the methods from the Custom Renderer which I overrode no longer exist. I needed to find the right place where to call into my methods for the native gesture handling.

The best information I got about how handlers work was in this repo by Javier Suárez. I also recommend looking at the .NET MAUI source code and the .NET MAUI Community Toolkit source code for additional examples of Handlers and their implementation.

I used the same folder structure as .NET MAUI. It has a folder for each control and within that a handler file for every platform.

Handler files

Keep in mind that this is a single-project with multi-targeting. To share code between the cross-platform code and the platform-specific code, each of these files is implementing a partial class. The compiled result is a combination of LabelHandler.cs and the respective platform file, e.g. LabelHandler.Android.cs. Therefore each platform-specific file can inherit from different platform-specific base classes.

Every handler has a property PlatformView, but the type of that property differs per platform.

File Platform PlatformView
LabelHandler.cs All
LabelHandler.Android.cs Android AndroidX.AppCompat.Widget.AppCompatTextView
LabelHandler.iOS.cs iOS & MacCatalyst Microsoft.Maui.Platform.MauiLabel which is a UILabel
LabelHandler.Windows.cs Windows Microsoft.UI.Xaml.Controls.TextBlock

The most important properties in a Handler are PlatformView, which is the platform-specific implementation of the view on its respective platform, and VirtualView, which is the cross-platform control that we use to compose a .NET MAUI control. In this example, we are using a Label.

In my Handlers, I inherit from the respective .NET MAUI Handler, e.g. LabelHandler, which provides the following methods which I can override:

Handler Method Purpose
CreatePlatformView Create the platform-specific view
ConnectHandler Initialize the platform-specific view
DisconnectHandler Clean up + Dispose

Note: As of writing this blog post, a bug exists, “DisconnectHandler is never called”, that is scheduled to be fixed in a service release to .NET v6.0.3.

As I wrote above, a Renderer IS A platform-specific view which allows me to easily override, for example, the Android-specific methods DispatchTouchEvent and DispatchGenericMotionEvent.

Now I need to create a sub-class of the View used by the Android handler, override the noted methods, and return a new instance of that class from CreatePlatformView. It’s a bit more complicated, but still no big deal.

Mapper

When properties change in the cross-platform control, .NET MAUI uses a PropertyMapper to notify the handler. A PropertyMapper is basically a Dictionary which maps the property name to a method that is invoked each time the property changes. Each respective method is called when that particular property changes.

LabelHandler.cs

public partial class LabelHandler : ILabelHandler
{
    public static IPropertyMapper<ILabel, ILabelHandler> Mapper = new PropertyMapper<ILabel, ILabelHandler>(ViewHandler.ViewMapper)
    {
        [nameof(ILabel.Text)] = MapText,
        // ...
    };

    public LabelHandler() : base(Mapper) { }

    public LabelHandler(IPropertyMapper? mapper = null) : base(mapper ?? Mapper) { }
}

LabelHandler defines a static Mapper which basically states that each time the Text property changes, the method MapText is invoked.

Note that the PropertyMapper constructor also receives a ViewHandler.ViewMapper. This allows PropertyMappers to be chained together. Chaining together PropertyMappers means that it will not only react when a property defined in Label changes, but also for any property which is defined in its parent’s Mapper, e.g. Visibility.

The MapText method is defined in the respective platform-specific part of the handler. It has parameters of the generic types used by the Mapper:

LabelHandler.Android.cs

public partial class LabelHandler
{
    public static void MapText(ILabelHandler handler, ILabel label)
    {
        // handler.PlatformView is a AppCompatTextView
        handler.PlatformView?.UpdateTextPlainText(label);
    }
    // ...
}

LabelHandler.iOS.cs

public partial class LabelHandler
{
    public static void MapText(ILabelHandler handler, ILabel label)
    {
        // handler.PlatformView is a UILabel
        handler.PlatformView?.UpdateTextPlainText(label);
    }
    // ...
}

LabelHandler.Windows.cs

public partial class LabelHandler
{
    public static void MapText(ILabelHandler handler, ILabel label)
    {
        // handler.PlatformView is a TextBlock
        handler.PlatformView?.UpdateText(label);
    }
    // ...
}

Now we can translate the methods we used in Xamarin.Forms Custom Renderers to the methods we’ll use in .NET MAUI Handlers to wire-up our custom code

Xamarin.Forms Custom Renderer .NET MAUI Handler
OnElementChanged ConnectHandler
OnElementPropertyChanged Mapper
Dispose DisconnectHandler
Dispatch*TouchEvent CreatePlatformView and own sub-class

From each of these methods, I call into my native platform code to handle the gestures. That code did not use Xamarin.Forms at all and therefore it did not change.

Registering the Handler

Xamarin.Forms used the ExportRendererAttribute to link the cross-platform controls to their Renderers. This had a performance problem: each time the app launched, Xamarin.Forms needed to scan every dll for said attribute. This was very slow and it got even slower for every reference which you added even if the dependency had nothing to do with Xamarin.Forms at all.

In other words, every NuGet Package added to your Xamarin.Forms app, regardless of whether it implemented Custom Renderers or not, would slow down your app’s startup time.

In .NET MAUI, we register handlers in the startup code of our app in MauiProgram.CreateMauiApp(), specifically in the ConfigureMauiHandlers extension method:

public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .ConfigureMauiHandlers(handlers =>
        {
            handlers.AddHandler<MR.Gestures.Label, LabelHandler>();
        });

    return builder.Build();
}

In MR.Gestures, I wrote an extension method to register all the MR.Gestures handlers at once, so all you need to do is this:

var builder = MauiApp.CreateBuilder();
builder
    .UseMauiApp<App>()
    .ConfigureMRGestures(licenseKey);

return builder.Build();

Note: The licenseKey is ignored for now. It will be used when MAUI gets to GA.

Supporting Additional Platforms

.NET MAUI adds two new platforms: MacCatalyst and WinUI3.

In theory, MacCatalyst should run the same iOS code. So in theory it should “just work”. But unfortunately I couldn’t test this yet.

For WinUI3 I took my UWP code and almost exclusively just changed the namespaces from Windows.UI to Microsoft.UI.

There was just one pitfall because I used Windows.UI.Xaml.Window.Current.CoreWindow on UWP which returns a Windows.UI.Core.CoreWindow. In WinUI3 Microsoft.UI.Xaml.Window.Current.CoreWindow still exists, but it has the UWP type Windows.UI.Core.CoreWindow. I had to replace CoreWindow with something else to find the root window of my view.

Conclusion

Altogether I was very pleased with the migration procedure. I just had to learn how Handlers work in-detail and how to wire-up all my code. In total, it took me two weeks from starting to look into how the .NET MAUI source-code works until I published the first prerelease of MR.Gestures for .NET MAUI.

0 comments

Discussion is closed.

Feedback usabilla icon