将 Xamarin.Forms 自定义呈现器迁移到 .NET MAUI 处理程序

在 Xamarin.Forms 中,自定义呈现器可用于自定义控件的外观和行为,并创建新的跨平台控件。 每个自定义呈现器都对跨平台控件有引用,并经常依赖 INotifyPropertyChanged 发送属性更改通知。 .NET Multi-platform App UI (.NET MAUI) 引入了名为处理程序的新概念,而不是使用自定义呈现器。

与自定义呈现器相比,处理程序在性能上有许多改进。 在 Xamarin.Forms 中,ViewRenderer 类创建父元素。 例如,在 Android 上,创建 ViewGroup 用于辅助定位任务。 在 .NET MAUI 中,ViewHandler 类不会创建父元素,这有助于减小视觉层次结构的大小并提高应用的性能。 处理程序还会将平台控件与框架分离。 平台控件只需满足处理框架的需求。 这不仅更高效,而且在需要时更容易扩展或替代。 处理程序也适合其他框架(如CometFabulous)重复使用。 有关处理程序的详细信息,请参阅处理程序

在 Xamarin.Forms 中,自定义呈现器中的 OnElementChanged 方法会创建平台控件、初始化默认值、订阅事件,并处理呈现器所连接的 Xamarin.Forms 元素 (OldElement) 和呈现器所连接的元素 (NewElement)。 此外,单个 OnElementPropertyChanged 方法定义在跨平台控件中的属性更改时要调用的操作。 .NET MAUI 简化了这种方法,因此每个属性更改都由单独的方法处理,而且创建平台控件、执行控件设置和执行控件清理的代码都被分隔为不同的方法。

将每个平台上由自定义呈现器支持的 Xamarin.Forms 自定义控件迁移到每个平台上由处理程序支持的 .NET MAUI 自定义控件的过程如下所示:

  1. 为跨平台控件创建一个类,这会提供控件的公共 API。 有关详细信息,请参阅创建跨平台控件
  2. 创建 partial 处理程序类。 有关详细信息,请参阅创建处理程序
  3. 在处理程序类中,创建 PropertyMapper 字典,用于定义在发生跨平台属性更改时要执行的操作。 有关详细信息,请参阅创建属性映射器
  4. 为每个平台创建 partial 处理程序类,用于创建实现跨平台控件的本机视图。 有关详细信息,请参阅创建平台控件
  5. 在应用的 MauiProgram 类中使用 ConfigureMauiHandlersAddHandler 方法注册处理程序。 有关详细信息,请参阅注册处理程序

然后,可以使用跨平台控件。 有关详细信息,请参阅使用跨平台控件

或者,可以转换自定义 Xamarin.Forms 控件的自定义呈现器,以便修改 .NET MAUI 处理程序。 有关详细信息,请参阅使用处理程序自定义控件

创建跨平台控件

要创建跨平台控件,应创建派生自 View 的类:

namespace MyMauiControl.Controls
{
    public class CustomEntry : View
    {
        public static readonly BindableProperty TextProperty =
            BindableProperty.Create(nameof(Text), typeof(string), typeof(CustomEntry), null);

        public static readonly BindableProperty TextColorProperty =
            BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(CustomEntry), null);

        public string Text
        {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }

        public Color TextColor
        {
            get { return (Color)GetValue(TextColorProperty); }
            set { SetValue(TextColorProperty, value); }
        }
    }
}

该控件应提供一个公共 API,供其处理程序和控件使用者访问。 跨平台控件应派生自 View,它表示用于在屏幕上放置布局和视图的视觉元素。

创建处理程序

创建跨平台控件后,应为处理程序创建 partial 类:

#if IOS || MACCATALYST
using PlatformView = Microsoft.Maui.Platform.MauiTextField;
#elif ANDROID
using PlatformView = AndroidX.AppCompat.Widget.AppCompatEditText;
#elif WINDOWS
using PlatformView = Microsoft.UI.Xaml.Controls.TextBox;
#elif (NETSTANDARD || !PLATFORM) || (NET6_0_OR_GREATER && !IOS && !ANDROID)
using PlatformView = System.Object;
#endif
using MyMauiControl.Controls;
using Microsoft.Maui.Handlers;

namespace MyMauiControl.Handlers
{
    public partial class CustomEntryHandler
    {
    }
}

处理程序类是一个分部类,其实现将在每个平台上使用附加分部类完成。

条件性 using 语句在每个平台上定义 PlatformView 类型。 最终条件 using 语句将 PlatformView 定义为等于 System.Object。 这是必要的,以便可以在处理程序中使用 PlatformView 类型,从而在所有平台上使用。 另一种方法是必须使用条件编译为每个平台定义一次 PlatformView 属性。

创建属性映射器

每个处理程序通常提供一个属性映射器,用于定义在跨平台控件中发生属性更改时要执行的操作。 PropertyMapper 类型是 Dictionary,用于将跨平台控件的属性映射到其关联的操作。

注意

属性映射器是 Xamarin.Forms 自定义呈现器中方法的替代方法 OnElementPropertyChanged

PropertyMapper 在 .NET MAUI 的泛型 ViewHandler 类中定义,并需要提供两个泛型参数:

  • 派生自 View 的跨平台控件的类。
  • 处理程序的类。

下列代码示例显示使用 PropertyMapper 定义扩展的 CustomEntryHandler 类:

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

    public CustomEntryHandler() : base(PropertyMapper)
    {
    }
}

PropertyMapperDictionary,其键为 string,值为泛型 Actionstring 表示跨平台控件的属性名称,Action 表示需要处理程序和跨平台控件作为参数的 static 方法。 例如,MapText 方法的签名是 public static void MapText(CustomEntryHandler handler, CustomEntry view)

每个平台处理程序都必须提供操作的实现,用于操作本机视图 API。 这可确保在跨平台控件上设置属性时,基础本机视图将根据需要进行更新。 此方法的优势在于,它允许轻松自定义跨平台控件,因为跨平台控件使用者无需子类化即可修改属性映射器。 有关详细信息,请参阅使用处理程序自定义控件

创建平台控件

为处理程序创建映射器后,必须在所有平台上提供处理程序实现。 可以通过在 Platforms 文件夹的子文件夹中添加分部类处理程序实现来达成此目的。 或者,可以将项目配置为支持基于文件名的多目标或基于文件夹的多目标,或者同时支持这两者。

在项目文件中添加以下 XML 作为 <Project> 节点的子级,来配置基于文件名的多目标功能:

<!-- Android -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-android')) != true">
  <Compile Remove="**\*.Android.cs" />
  <None Include="**\*.Android.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

<!-- iOS and Mac Catalyst -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-ios')) != true AND $(TargetFramework.StartsWith('net8.0-maccatalyst')) != true">
  <Compile Remove="**\*.MaciOS.cs" />
  <None Include="**\*.MaciOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

<!-- Windows -->
<ItemGroup Condition="$(TargetFramework.Contains('-windows')) != true ">
  <Compile Remove="**\*.Windows.cs" />
  <None Include="**\*.Windows.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

有关配置多目标的详细信息,请参阅配置多目标

每个平台处理程序类都应是分部类,并派生自泛型 ViewHandler 类,这需要两个类型参数:

  • 派生自 View 的跨平台控件的类。
  • 在平台上实现跨平台控件的本机视图的类型。 这应与处理程序中 PlatformView 属性的类型相同。

重要

ViewHandler 类提供 VirtualViewPlatformView 属性。 VirtualView 属性用于从其处理程序访问跨平台控件。 PlatformView 属性用于访问每个平台上实现跨平台控件的本机视图。

每个平台处理程序实现都应重写以下方法:

  • CreatePlatformView,用于创建并返回实现跨平台控件的本机视图。
  • ConnectHandler,用于执行任何本机视图设置,例如初始化本机视图和执行事件订阅。
  • DisconnectHandler,用于执行任何本机视图清理,例如取消订阅事件和释放对象。 这种方法有意不由 .NET MAUI 调用。 实际上,你必须从应用生命周期中的合适位置自行调用它。 有关详细信息,请参阅本机视图清理

注意

在 Xamarin.Forms 自定义呈现器中,CreatePlatformViewConnectHandlerDisconnectHandler 替代可替代 OnElementChanged 方法。

每个平台处理程序还应实现映射器字典中定义的操作。 此外,每个平台处理程序还应根据需要提供代码,以在平台上实现跨平台控件的功能。 或者,对于更复杂的控件,可以通过其他附加类型来实现。

下列示例展示了 CustomEntryHandler 在 Android 上的实现:

#nullable enable
using AndroidX.AppCompat.Widget;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using MyMauiControl.Controls;

namespace MyMauiControl.Handlers
{
    public partial class CustomEntryHandler : ViewHandler<CustomEntry, AppCompatEditText>
    {
        protected override AppCompatEditText CreatePlatformView() => new AppCompatEditText(Context);

        protected override void ConnectHandler(AppCompatEditText platformView)
        {
            base.ConnectHandler(platformView);

            // Perform any control setup here
        }

        protected override void DisconnectHandler(AppCompatEditText platformView)
        {
            // Perform any native view cleanup here
            platformView.Dispose();
            base.DisconnectHandler(platformView);
        }

        public static void MapText(CustomEntryHandler handler, CustomEntry view)
        {
            handler.PlatformView.Text = view.Text;
            handler.PlatformView?.SetSelection(handler.PlatformView?.Text?.Length ?? 0);
        }

        public static void MapTextColor(CustomEntryHandler handler, CustomEntry view)
        {
            handler.PlatformView?.SetTextColor(view.TextColor.ToPlatform());
        }
    }
}

CustomEntryHandler 派生自 ViewHandler 类,其中泛型 CustomEntry 参数指定跨平台控件类型,以及 AppCompatEditText 参数指定本机控件类型。

CreatePlatformView 替代会创建并返回一个 AppCompatEditText 对象。 ConnectHandler 替代是执行任何必需的本机视图设置的位置。 DisconnectHandler 替代是执行任何本机视图清理的位置,因此在 AppCompatEditText 实例上调用 Dispose 方法。

处理程序还实现了属性映射器字典中定义的操作。 每个操作都是为了响应跨平台控件上更改的属性而执行的,并且是需要 static 处理程序和跨平台控件实例作为参数的方法。 在每种情况下,操作都会调用在本机控件上定义的方法。

注册处理程序

自定义控件及其处理程序必须向应用注册,然后才能使用。 这应当发生在应用项目中 MauiProgram 类的 CreateMauiApp 方法中,这是应用的跨平台入口点:

using Microsoft.Extensions.Logging;
using MyMauiControl.Controls;
using MyMauiControl.Handlers;

namespace MyMauiControl;

public static class MauiProgram
{
  public static MauiApp CreateMauiApp()
  {
    var builder = MauiApp.CreateBuilder();
    builder
      .UseMauiApp<App>()
      .ConfigureFonts(fonts =>
      {
        fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
        fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
      })
      .ConfigureMauiHandlers(handlers =>
      {
        handlers.AddHandler(typeof(CustomEntry), typeof(CustomEntryHandler));
      });

#if DEBUG
    builder.Logging.AddDebug();
#endif

    return builder.Build();
  }
}

处理程序使用 ConfigureMauiHandlersAddHandler 方法注册。 AddHandler 方法的第一个参数是跨平台控件类型,第二个参数是其处理程序类型。

注意

这种注册方法可避免 Xamarin.Forms 的程序集扫描,因为其速度缓慢且成本高昂。

使用跨平台控件

向应用注册处理程序后,便可使用跨平台控件:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:MyMauiControl.Controls"
             x:Class="MyMauiControl.MainPage">
    <Grid>
        <controls:CustomEntry Text="Hello world"
                              TextColor="Blue" />
    </Grid>
</ContentPage>

本机视图清理

每个平台的处理程序实现都会重写 DisconnectHandler 实现,该实现用于执行本地视图清理,如取消订阅事件和释放对象。 但是,.NET MAUI 有意不调用此重写函数。 实际上,你必须从应用生命周期中的合适位置自行调用它。 这可能是指当包含控件的页面导航离开时,会引发页面 Unloaded 事件。

页面 Unloaded 事件的事件处理程序可在 XAML 中注册:

<ContentPage ...
             xmlns:controls="clr-namespace:MyMauiControl.Controls"
             Unloaded="ContentPage_Unloaded">
    <Grid>
        <controls:CustomEntry x:Name="customEntry"
                              ... />
    </Grid>
</ContentPage>

然后,Unloaded 事件的事件处理程序可以在其 Handler 实例上调用 DisconnectHandler 方法:

void ContentPage_Unloaded(object sender, EventArgs e)
{
    customEntry.Handler?.DisconnectHandler();
}

另请参阅