Перенос MR.Gestures из Xamarin.Forms в .NET MAUI

Давайте сначала представим краткое введение в библиотеку MR.Gestures, чтобы объяснить, почему она существует и проблему, которую она решает.

Когда Xamarin.Forms был выпущен в 2014 году, он предоставил только то, Tap Gesture Recognizer что нужно было добавить в Gesture Recognizers коллекцию. Эта реализация всегда была для меня чем-то вроде запаха кода. Он скопировал API iOS + Android, но iOS и Android (в то время) использовали только эту архитектуру, потому что Objective-C и Java не поддерживали события. Однако в .NET и C# мы всегда использовали event и ICommand для этих сценариев.

Я создал библиотеку MR.Gestures, чтобы закрыть этот пробел. Он предоставляет events и ICommands для жестов на каждом элементе управления (все Views, Layouts и ContentPages), которые вы можете использовать для реагирования на любые события касания (и мыши).

До MR.Gestures мы писали это на XAML для обработки события tapped в Label:

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

Но с MR.Gestures наш XAML теперь выглядит так:

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

И, что более важно, есть 17 других событий касания (и мыши), которые поддерживает MR.Gestures. Каждое соответствующее событие Event Args также предоставляет больше информации, чем Microsoft GestureRecognizers.

Теперь, когда команда инженеров .NET MAUI опубликовала свой релиз-кандидат, пришло время перенести MR.Gestures из Xamarin.Forms в .NET MAUI.

Запуск миграции

Мы начали с создания нового проекта в Visual Studio с использованием шаблона библиотеки классов .NET MAUI. Шаблон создает один единственный проект (csproj), используя множественный таргетинг для всех платформ.

Этот шаблон также включает папку « Платформы » с подпапками для каждой платформы , Android, и . Но он НЕ настраивает содержимое какой-либо папки с именем , или для компиляции только на соответствующей платформе. Для этого нам нужно скопировать несколько строк из официальной документации .NET MAUI Multi-Targeting в наш файл. iOSMacCatalystWindowsAndroidiOSMacCatalystWindowscsproj

Я немного упростил их, чтобы этого хватило:

<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>

Первый ItemGroup гарантирует, что любые файлы, которые заканчиваются в папке или находятся в ней, компилируются только в том случае, если текущий файл имеет расширение ..Android.csAndroidTargetFrameworknet6.0-android

Второй и третий делают это для iOS, MacCatalyst и Windows соответственно.

И последнее — это исправление для VS, так что обозреватель решений по-прежнему показывает все файлы, которые скомпилированы только на некоторых платформах.

Когда эта конфигурация завершена, давайте начнем программировать!

Миграция элементов управления

Перенести мои элементы управления Xamarin.Forms в .NET MAUI было легко. Я просто изменил базовый класс своих типов с на, например. Все свойства и события в этих классах остались прежними. Xamarin.Forms. *Microsoft.Maui.Controls. *Microsoft.Maui.Controls.Label

Как вы понимаете, это очень много кода. В Xamarin.Forms у MR.Gestures было 34 класса, каждый с 18 событиями, командами и параметрами команд.

Это много повторяющегося кода, который я не хочу писать вручную. Поэтому я использую шаблон T4 для его создания. Когда я впервые создал MR.Gestures в 2014 году, мой шаблон содержал 110 строк и генерировал 19 000 строк кода. Со временем, с дополнительными событиями, дополнительными элементами управления, а теперь и с поддержкой .NET MAUI, я также добавил несколько элементов управления. Теперь мой шаблон T4 имеет 188 строк и генерирует 30 000 строк C#.

Переход к обработчикам

В Xamarin.Forms у каждого из моих кросс-платформенных элементов управления есть пользовательский отрисовщик, который реализует логику обработки касаний для конкретной платформы. В рендерерах я переопределяю OnElementChanged, OnElementPropertyChanged, Disposeа на Android также DispatchTouchEvent и DispatchGenericMotionEvent.

В .NET MAUI Custom Renderers заменены Handlers . Обработчик по-прежнему имеет ту же цель, что и пользовательский рендерер — он синхронизирует изменения между кросс-платформенным элементом управления и платформо-зависимой реализацией, — но обработчик не наследует представление платформы; методы пользовательского рендерера, которые я переопределил, больше не существуют. Мне нужно было найти правильное место, где вызывать мои методы для встроенной обработки жестов.

Лучшая информация, которую я получил о том, как работают обработчики, была в этом репозитории Хавьера Суареса. Я также рекомендую просмотреть исходный код .NET MAUI и исходный код .NET MAUI Community Toolkit для получения дополнительных примеров обработчиков и их реализации.

Я использовал ту же структуру папок, что и .NET MAUI. У него есть папка для каждого элемента управления и в ней файл обработчика для каждой платформы.

Имейте в виду, что это один проект с мультитаргетингом. Чтобы разделить код между кросс-платформенным кодом и кодом для конкретной платформы, каждый из этих файлов реализует файл . Скомпилированный результат представляет собой комбинацию и соответствующий файл платформы, например. Поэтому каждый файл для конкретной платформы может наследоваться от разных базовых классов для конкретной платформы.partial classLabelHandler.csLabelHandler.Android.cs

У каждого обработчика есть свойство PlatformView, но тип этого свойства зависит от платформы.

Наиболее важными свойствами в обработчике являются PlatformView, представляющее собой специфичную для платформы реализацию представления на соответствующей платформе, и VirtualView, представляющий собой кроссплатформенный элемент управления, который мы используем для создания элемента управления .NET MAUI. В этом примере мы используем файл Label.

На момент написания этой записи в блоге существовала ошибка «DisconnectHandler никогда не вызывается», которую планируется исправить в сервисном выпуске .NET v6.0.3.

Как я уже писал выше, Renderer — это представление, специфичное для платформы, которое позволяет мне легко переопределять, например, методы, специфичные для Android, и Dispatch TouchEvent файлы Dispatch GenericMotionEvent.

Теперь мне нужно создать подкласс View используемого обработчиком Android, переопределить отмеченные методы и вернуть новый экземпляр этого класса из CreatePlatform View. Это немного сложнее, но все равно ничего страшного.

Картограф

Когда свойства изменяются в кросс-платформенном элементе управления, .NET MAUI использует Property Mapper для уведомления обработчика. Property Mapper по сути, это Dictionary, который сопоставляет имя свойства с методом, который вызывается каждый раз при изменении свойства. Каждый соответствующий метод вызывается при изменении этого конкретного свойства.

LabelHandler.cs

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

    public LabelHandler() : base(Mapper) { }

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

LabelHandler определяет, что в основном говорится, что каждый раз, когда свойство изменяется, метод вызывается .static Mapper Text Map Text

Обратите внимание, что Property Mapper конструктор также получает файл. Это позволяет связать s вместе. Объединение в цепочку означает, что он будет реагировать не только на изменение свойства, определенного в, но и на любое свойство, определенное в его родительском элементе, например, .ViewHandler.ViewMapper Property Mapper Property Mappers Label Mapper Visibility.

Метод Map Text определяется в соответствующей специфичной для платформы части обработчика. Он имеет параметры универсальных типов, используемых 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);
    }
    // ...
}

Теперь мы можем преобразовать методы, которые мы использовали в пользовательских средствах визуализации Xamarin.Forms, в методы, которые мы будем использовать в обработчиках .NET MAUI для подключения нашего пользовательского кода.

Регистрация обработчика

Xamarin.Forms использовал Export Renderer Attribute для связывания кроссплатформенных элементов управления с их модулями визуализации. У этого была проблема с производительностью: каждый раз, когда приложение запускалось, Xamarin.Forms нужно было сканировать каждый dll указанный атрибут. Это было очень медленно и становилось еще медленнее для каждой добавленной вами ссылки, даже если зависимость вообще не имела ничего общего с Xamarin.Forms.

Другими словами, каждый пакет NuGet, добавленный в ваше приложение Xamarin.Forms, независимо от того, реализованы ли в нем пользовательские средства визуализации или нет, замедлит время запуска вашего приложения.

В .NET MAUI мы регистрируем обработчики в коде запуска нашего приложения в, а именно в методе расширения: MauiProgram .CreateMauiApp () Configure Maui Handlers

public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp()
        .ConfigureMauiHandlers(handlers =>
        {
            handlers.AddHandler<_mr.gestures.label2c_ labelhandler="">();
        });

    return builder.Build();
}

В MR.Gestures я написал метод расширения для одновременной регистрации всех обработчиков MR.Gestures, поэтому все, что вам нужно сделать, это следующее:

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

return builder.Build()

На licenseKey данный момент игнорируется. Он будет использоваться, когда MAUI доберется до GA.

Поддержка дополнительных платформ

.NET MAUI добавляет две новые платформы: MacCatalyst и WinUI3.

Теоретически MacCatalyst должен запускать тот же код iOS. Так что теоретически это должно «просто работать». Но, к сожалению, я еще не мог проверить это.

Для WinUI3 я взял свой код UWP и почти всегда просто изменил пространства имен с на.Windows .UIMicrosoft .UI

Была только одна ловушка, потому что я использовал UWP, который возвращает файл. В WinUI3 еще есть, но он имеет тип UWP. Мне пришлось заменить на что-то другое, чтобы найти корневое окно моего представления. Windows. UI.Xaml. Window. Current. CoreWindowWindows. UI.Core. CoreWindowMicrosoft. UI. Xaml. Window. Current. CoreWindowWindows. UI. Core. CoreWindowCoreWindow

Вывод

В целом я был очень доволен процедурой миграции. Мне просто нужно было подробно изучить, как работают обработчики, и как подключить весь мой код. В общей сложности мне потребовалось две недели, прежде чем я начал изучать работу исходного кода .NET MAUI, пока не опубликовал первый предварительный выпуск MR.Gestures для .NET MAUI.

Материалы по теме