{"id":49199,"date":"2022-05-17T10:40:35","date_gmt":"2022-05-17T17:40:35","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/xamarin\/?p=49199"},"modified":"2022-05-18T23:48:19","modified_gmt":"2022-05-19T06:48:19","slug":"migrating-mrgestures-to-dotnet-maui","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/xamarin\/migrating-mrgestures-to-dotnet-maui\/","title":{"rendered":"Migrating MR.Gestures from Xamarin.Forms to .NET MAUI"},"content":{"rendered":"<blockquote>\n<p>Note: This is a Guest Blog Post by <a href=\"https:\/\/www.mrumpler.at\">Michael Rumpler<\/a>. Michael has been a freelance C# developer since 2003, switching from web to mobile in 2014 upon the introduction of Xamarin.Forms.<\/p>\n<\/blockquote>\n<p>Let&#8217;s first provide a brief introduction to the <a href=\"https:\/\/www.mrgestures.com\">MR.Gestures Library<\/a> to explain why it exists and the problem it solves.<\/p>\n<p>When Xamarin.Forms was released in 2014 it only provided <code>TapGestureRecognizer<\/code> which had to be added to a <code>GestureRecognizers<\/code> 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&#8217;t support events. However, In .NET and C# we always used <code>event<\/code> and <code>ICommand<\/code> for these scenarios.<\/p>\n<p>I created the <a href=\"https:\/\/www.mrgestures.com\/\">MR.Gestures<\/a> library to close this gap. It provides <code>event<\/code>s and <code>ICommand<\/code>s for gestures on each and every control (all <code>View<\/code>s, <code>Layout<\/code>s and <code>ContentPage<\/code>s) which you can leverage to respond to any touch (and mouse) events.<\/p>\n<p>Before MR.Gestures, we would write this in XAML to handle a tapped event on a <code>Label<\/code>:<\/p>\n<pre><code class=\"language-xml\">&lt;Label Text=\"{Binding Text}\"&gt;\n    &lt;Label.GestureRecognizers&gt;\n        &lt;TapGestureRecognizer Tapped=\"Label_Tapped\" \/&gt;\n    &lt;\/Label.GestureRecognizers&gt;\n&lt;\/Label&gt;<\/code><\/pre>\n<p>But with MR.Gestures, our XAML now looks like this:<\/p>\n<pre><code class=\"language-xml\">&lt;mr:Label Text=\"{Binding Text}\" Tapped=\"Label_Tapped\" \/&gt;<\/code><\/pre>\n<p>And, more importantly, there are 17 other touch (and mouse) events which MR.Gestures supports. Each event&#8217;s respective <code>EventArgs<\/code> also provide more information than Microsoft&#8217;s <code>GestureRecognizers<\/code>.<\/p>\n<p>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.<\/p>\n<h2>Starting the Migration<\/h2>\n<p>We started by creating a new project in Visual Studio using the <strong>.NET MAUI Class Library<\/strong> template. The template creates one single project (<code>csproj<\/code>) using multi-targeting to target all platforms.<\/p>\n<p>This template also includes a <strong>Platforms<\/strong> folder with subfolders for every platform, <code>Android<\/code>, <code>iOS<\/code>, <code>MacCatalyst<\/code> and <code>Windows<\/code>. But it does <strong>NOT<\/strong> configure the contents of any folder named <code>Android<\/code>, <code>iOS<\/code>, <code>MacCatalyst<\/code> or <code>Windows<\/code> to be compiled only on their respective platform. For this we need to copy a few lines from the <a href=\"https:\/\/docs.microsoft.com\/dotnet\/maui\/platform-integration\/configure-multi-targeting?WT.mc_id=dotnet-0000-bramin\">official .NET MAUI Multi-Targeting documentation<\/a> to our <code>csproj<\/code> file.<\/p>\n<p>I simplified them a bit so that this is enough:<\/p>\n<pre><code>&lt;ItemGroup Condition=\"!$(TargetFramework.StartsWith('net6.0-android'))\"&gt;\n    &lt;Compile Remove=\"**\/*.Android.cs\" \/&gt;\n    &lt;Compile Remove=\"**\/Android\/**\/*.cs\" \/&gt;\n&lt;\/ItemGroup&gt;\n\n&lt;ItemGroup Condition=\"!$(TargetFramework.StartsWith('net6.0-ios')) AND !$(TargetFramework.StartsWith('net6.0-maccatalyst'))\"&gt;\n    &lt;Compile Remove=\"**\/*.iOS.cs\" \/&gt;\n    &lt;Compile Remove=\"**\/iOS\/**\/*.cs\" \/&gt;\n&lt;\/ItemGroup&gt;\n\n&lt;ItemGroup Condition=\"!$(TargetFramework.Contains('-windows'))\"&gt;\n    &lt;Compile Remove=\"**\/*.Windows.cs\" \/&gt;\n    &lt;Compile Remove=\"**\/Windows\/**\/*.cs\" \/&gt;\n&lt;\/ItemGroup&gt;\n\n&lt;ItemGroup&gt;\n    &lt;None Include=\"**\/*\" Exclude=\"$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder);$(Compile)\" \/&gt;\n&lt;\/ItemGroup&gt;\n<\/code><\/pre>\n<p>The first <code>ItemGroup<\/code> makes sure that any files which end in <code>.Android.cs<\/code> or are in an <code>Android<\/code> folder are only compiled if the current <code>TargetFramework<\/code> is <code>net6.0-android<\/code>.<\/p>\n<p>The second and third do that for iOS, MacCatalyst and Windows respectively.<\/p>\n<p>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.<\/p>\n<p>With this configuration completed, let&#8217;s start coding!<\/p>\n<h2>Migrating The Controls<\/h2>\n<p>Migrating my Xamarin.Forms controls to .NET MAUI was easy. I just changed the base class of my types from <code>Xamarin.Forms.*<\/code> to <code>Microsoft.Maui.Controls.*<\/code>, eg <code>Microsoft.Maui.Controls.Label<\/code>. All of the properties and events in those classes stayed the same.<\/p>\n<p>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.<\/p>\n<p>This is a lot of repetitive code which I don&#8217;t want to write by hand. So I use a <a href=\"https:\/\/docs.microsoft.com\/visualstudio\/modeling\/writing-a-t4-text-template?WT.mc_id=dotnet-0000-bramin\">T4 template<\/a> 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#.<\/p>\n<h2>Migrating To Handlers<\/h2>\n<p>In Xamarin.Forms, each of my cross-platform controls has a <a href=\"https:\/\/docs.microsoft.com\/xamarin\/xamarin-forms\/app-fundamentals\/custom-renderer\/?WT.mc_id=dotnet-0000-bramin\">Custom Renderer<\/a> which implements the platform-specific touch handling logic. In the renderers I override <code>OnElementChanged<\/code>, <code>OnElementPropertyChanged<\/code>, <code>Dispose<\/code> and on Android also <code>DispatchTouchEvent<\/code> and <code>DispatchGenericMotionEvent<\/code>.<\/p>\n<p>In .NET MAUI, Custom Renderers are replaced by <a href=\"https:\/\/docs.microsoft.com\/dotnet\/maui\/user-interface\/handlers\/customize?WT.mc_id=dotnet-0000-bramin\">Handlers<\/a>. A Handler still has the same purpose as a Custom Renderer &#8211; it synchronizes changes between the cross-platform control and the platform-specific implementation &#8211; 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.<\/p>\n<p>The best information I got about how handlers work was <a href=\"https:\/\/github.com\/jsuarezruiz\/xamarin-forms-to-net-maui\/tree\/main\/Handlers\">in this repo by Javier Su\u00e1rez<\/a>. I also recommend looking at the <a href=\"https:\/\/github.com\/dotnet\/maui\">.NET MAUI source code<\/a> and the <a href=\"https:\/\/github.com\/CommunityToolkit\/Maui\">.NET MAUI Community Toolkit source code<\/a> for additional examples of Handlers and their implementation.<\/p>\n<p>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.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/xamarin\/wp-content\/uploads\/sites\/44\/2022\/05\/MAUI_handlers.png\" alt=\"Handler files\" \/><\/p>\n<p>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 <code>partial class<\/code>. The compiled result is a combination of <code>LabelHandler.cs<\/code> and the respective platform file, e.g. <code>LabelHandler.Android.cs<\/code>. Therefore each platform-specific file can inherit from different platform-specific base classes.<\/p>\n<p>Every handler has a property <code>PlatformView<\/code>, but the type of that property differs per platform.<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left\">\n        File\n      <\/th>\n<th style=\"text-align: left\">\n        Platform\n      <\/th>\n<th style=\"text-align: left\">\n        PlatformView\n      <\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left\">\n        LabelHandler.cs\n      <\/td>\n<td style=\"text-align: left\">\n        All\n      <\/td>\n<td style=\"text-align: left\">\n      <\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">\n        LabelHandler.Android.cs\n      <\/td>\n<td style=\"text-align: left\">\n        Android\n      <\/td>\n<td style=\"text-align: left\">\n        <code>AndroidX.AppCompat.Widget.AppCompatTextView<\/code>\n      <\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">\n        LabelHandler.iOS.cs\n      <\/td>\n<td style=\"text-align: left\">\n        iOS &#038; MacCatalyst\n      <\/td>\n<td style=\"text-align: left\">\n        <code>Microsoft.Maui.Platform.MauiLabel<\/code> which is a <code>UILabel<\/code>\n      <\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">\n        LabelHandler.Windows.cs\n      <\/td>\n<td style=\"text-align: left\">\n        Windows\n      <\/td>\n<td style=\"text-align: left\">\n        <code>Microsoft.UI.Xaml.Controls.TextBlock<\/code>\n      <\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The most important properties in a Handler are <code>PlatformView<\/code>, which is the platform-specific implementation of the view on its respective platform, and <code>VirtualView<\/code>, which is the cross-platform control that we use to compose a .NET MAUI control. In this example, we are using a <code>Label<\/code>.<\/p>\n<p>In my Handlers, I inherit from the respective .NET MAUI Handler, e.g. <code>LabelHandler<\/code>, which provides the following methods which I can override:<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left\">\n        Handler Method\n      <\/th>\n<th style=\"text-align: left\">\n        Purpose\n      <\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left\">\n        <code>CreatePlatformView<\/code>\n      <\/td>\n<td style=\"text-align: left\">\n        Create the platform-specific view\n      <\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">\n        <code>ConnectHandler<\/code>\n      <\/td>\n<td style=\"text-align: left\">\n        Initialize the platform-specific view\n      <\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">\n        <code>DisconnectHandler<\/code>\n      <\/td>\n<td style=\"text-align: left\">\n        Clean up + Dispose\n      <\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<blockquote>\n<p><strong>Note<\/strong>: As of writing this blog post, a bug exists, <a href=\"https:\/\/github.com\/dotnet\/maui\/issues\/3604\">&#8220;DisconnectHandler is never called&#8221;<\/a>, that is scheduled to be fixed in a service release to .NET v6.0.3.<\/p>\n<\/blockquote>\n<p>As I wrote above, a Renderer IS A platform-specific view which allows me to easily override, for example, the Android-specific methods <code>DispatchTouchEvent<\/code> and <code>DispatchGenericMotionEvent<\/code>.<\/p>\n<p>Now I need to create a sub-class of the <code>View<\/code> used by the Android handler, override the noted methods, and return a new instance of that class from <code>CreatePlatformView<\/code>. It&#8217;s a bit more complicated, but still no big deal.<\/p>\n<h3>Mapper<\/h3>\n<p>When properties change in the cross-platform control, .NET MAUI uses a <code>PropertyMapper<\/code> to notify the handler. A <code>PropertyMapper<\/code> is basically a <code>Dictionary<\/code> 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.<\/p>\n<h4>LabelHandler.cs<\/h4>\n<pre><code class=\"language-cs\">public partial class LabelHandler : ILabelHandler\n{\n    public static IPropertyMapper&lt;ILabel, ILabelHandler&gt; Mapper = new PropertyMapper&lt;ILabel, ILabelHandler&gt;(ViewHandler.ViewMapper)\n    {\n        [nameof(ILabel.Text)] = MapText,\n        \/\/ ...\n    };\n\n    public LabelHandler() : base(Mapper) { }\n\n    public LabelHandler(IPropertyMapper? mapper = null) : base(mapper ?? Mapper) { }\n}<\/code><\/pre>\n<p><code>LabelHandler<\/code> defines a <code>static Mapper<\/code> which basically states that each time the <code>Text<\/code> property changes, the method <code>MapText<\/code> is invoked.<\/p>\n<p>Note that the <code>PropertyMapper<\/code> constructor also receives a <code>ViewHandler.ViewMapper<\/code>. This allows <code>PropertyMapper<\/code>s to be chained together. Chaining together <code>PropertyMappers<\/code> means that it will not only react when a property defined in <code>Label<\/code> changes, but also for any property which is defined in its parent&#8217;s <code>Mapper<\/code>, e.g. <code>Visibility<\/code>.<\/p>\n<p>The <code>MapText<\/code> method is defined in the respective platform-specific part of the handler. It has parameters of the generic types used by the <code>Mapper<\/code>:<\/p>\n<h4>LabelHandler.Android.cs<\/h4>\n<pre><code class=\"language-cs\">public partial class LabelHandler\n{\n    public static void MapText(ILabelHandler handler, ILabel label)\n    {\n        \/\/ handler.PlatformView is a AppCompatTextView\n        handler.PlatformView?.UpdateTextPlainText(label);\n    }\n    \/\/ ...\n}<\/code><\/pre>\n<h4>LabelHandler.iOS.cs<\/h4>\n<pre><code class=\"language-cs\">public partial class LabelHandler\n{\n    public static void MapText(ILabelHandler handler, ILabel label)\n    {\n        \/\/ handler.PlatformView is a UILabel\n        handler.PlatformView?.UpdateTextPlainText(label);\n    }\n    \/\/ ...\n}<\/code><\/pre>\n<h4>LabelHandler.Windows.cs<\/h4>\n<pre><code class=\"language-cs\">public partial class LabelHandler\n{\n    public static void MapText(ILabelHandler handler, ILabel label)\n    {\n        \/\/ handler.PlatformView is a TextBlock\n        handler.PlatformView?.UpdateText(label);\n    }\n    \/\/ ...\n}<\/code><\/pre>\n<p>Now we can translate the methods we used in Xamarin.Forms Custom Renderers to the methods we&#8217;ll use in .NET MAUI Handlers to wire-up our custom code<\/p>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left\">\n        Xamarin.Forms Custom Renderer\n      <\/th>\n<th style=\"text-align: left\">\n        .NET MAUI Handler\n      <\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left\">\n        <code>OnElementChanged<\/code>\n      <\/td>\n<td style=\"text-align: left\">\n        <code>ConnectHandler<\/code>\n      <\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">\n        <code>OnElementPropertyChanged<\/code>\n      <\/td>\n<td style=\"text-align: left\">\n        <code>Mapper<\/code>\n      <\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">\n        <code>Dispose<\/code>\n      <\/td>\n<td style=\"text-align: left\">\n        <code>DisconnectHandler<\/code>\n      <\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">\n        <code>Dispatch*TouchEvent<\/code>\n      <\/td>\n<td style=\"text-align: left\">\n        <code>CreatePlatformView<\/code> and own sub-class\n      <\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>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.<\/p>\n<h2>Registering the Handler<\/h2>\n<p>Xamarin.Forms used the <code>ExportRendererAttribute<\/code> 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 <code>dll<\/code> 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.<\/p>\n<p>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&#8217;s startup time.<\/p>\n<p>In .NET MAUI, we register handlers in the startup code of our app in <code>MauiProgram.CreateMauiApp()<\/code>, specifically in the <code>ConfigureMauiHandlers<\/code> extension method:<\/p>\n<pre><code class=\"language-cs\">public static MauiApp CreateMauiApp()\n{\n    var builder = MauiApp.CreateBuilder();\n    builder\n        .UseMauiApp&lt;App&gt;()\n        .ConfigureMauiHandlers(handlers =&gt;\n        {\n            handlers.AddHandler&lt;MR.Gestures.Label, LabelHandler&gt;();\n        });\n\n    return builder.Build();\n}<\/code><\/pre>\n<p>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:<\/p>\n<pre><code class=\"language-cs\">var builder = MauiApp.CreateBuilder();\nbuilder\n    .UseMauiApp&lt;App&gt;()\n    .ConfigureMRGestures(licenseKey);\n\nreturn builder.Build();<\/code><\/pre>\n<blockquote>\n<p><strong>Note:<\/strong> The <code>licenseKey<\/code> is ignored for now. It will be used when MAUI gets to GA.<\/p>\n<\/blockquote>\n<h2>Supporting Additional Platforms<\/h2>\n<p>.NET MAUI adds two new platforms: MacCatalyst and WinUI3.<\/p>\n<p>In theory, MacCatalyst should run the same iOS code. So in theory it should &#8220;just work&#8221;. But unfortunately <a href=\"https:\/\/github.com\/dotnet\/maui\/issues\/6674\">I couldn&#8217;t test this yet<\/a>.<\/p>\n<p>For WinUI3 I took my UWP code and almost exclusively just changed the namespaces from <code>Windows.UI<\/code> to <code>Microsoft.UI<\/code>.<\/p>\n<p>There was just one pitfall because I used <code>Windows.UI.Xaml.Window.Current.CoreWindow<\/code> on UWP which returns a <code>Windows.UI.Core.CoreWindow<\/code>. In WinUI3 <code>Microsoft.UI.Xaml.Window.Current.CoreWindow<\/code> still exists, but it has the UWP type <code>Windows.UI.Core.CoreWindow<\/code>. I had to replace <code>CoreWindow<\/code> with something else to find the root window of my view.<\/p>\n<h2>Conclusion<\/h2>\n<p>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.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this guest blog post, Michael Rumpler shares how he ported his popular Xamarin.Forms library, MR.Gestures, to support .NET MAUI<\/p>\n","protected":false},"author":91724,"featured_media":39167,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[9211,1],"tags":[8867,589,9216,9217],"class_list":["post-49199","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-net-maui","category-xamarin","tag-net-maui","tag-community","tag-library","tag-migration"],"acf":[],"blog_post_summary":"<p>In this guest blog post, Michael Rumpler shares how he ported his popular Xamarin.Forms library, MR.Gestures, to support .NET MAUI<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/posts\/49199","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/users\/91724"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/comments?post=49199"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/posts\/49199\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/media\/39167"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/media?parent=49199"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/categories?post=49199"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/xamarin\/wp-json\/wp\/v2\/tags?post=49199"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}