将 Xamarin.Forms 自定义呈现器迁移到 .NET MAUI 处理程序
在 Xamarin.Forms 中,自定义呈现器可用于自定义控件的外观和行为,并创建新的跨平台控件。 每个自定义呈现器都对跨平台控件有引用,并经常依赖 INotifyPropertyChanged
发送属性更改通知。 .NET Multi-platform App UI (.NET MAUI) 引入了名为处理程序的新概念,而不是使用自定义呈现器。
与自定义呈现器相比,处理程序在性能上有许多改进。 在 Xamarin.Forms 中,ViewRenderer
类创建父元素。 例如,在 Android 上,创建 ViewGroup
用于辅助定位任务。 在 .NET MAUI 中,ViewHandler<TVirtualView,TPlatformView> 类不会创建父元素,这有助于减小视觉层次结构的大小并提高应用的性能。 处理程序还会将平台控件与框架分离。 平台控件只需满足处理框架的需求。 这不仅更高效,而且在需要时更容易扩展或替代。 处理程序也适合其他框架(如Comet 和 Fabulous)重复使用。 有关处理程序的详细信息,请参阅处理程序。
在 Xamarin.Forms 中,自定义呈现器中的 OnElementChanged
方法会创建平台控件、初始化默认值、订阅事件,并处理呈现器所连接的 Xamarin.Forms 元素 (OldElement
) 和呈现器所连接的元素 (NewElement
)。 此外,单个 OnElementPropertyChanged
方法定义在跨平台控件中的属性更改时要调用的操作。 .NET MAUI 简化了这种方法,因此每个属性更改都由单独的方法处理,而且创建平台控件、执行控件设置和执行控件清理的代码都被分隔为不同的方法。
将每个平台上由自定义呈现器支持的 Xamarin.Forms 自定义控件迁移到每个平台上由处理程序支持的 .NET MAUI 自定义控件的过程如下所示:
- 为跨平台控件创建一个类,这会提供控件的公共 API。 有关详细信息,请参阅创建跨平台控件。
- 创建
partial
处理程序类。 有关详细信息,请参阅创建处理程序。 - 在处理程序类中,创建 PropertyMapper 字典,用于定义在发生跨平台属性更改时要执行的操作。 有关详细信息,请参阅创建属性映射器。
- 为每个平台创建
partial
处理程序类,用于创建实现跨平台控件的本机视图。 有关详细信息,请参阅创建平台控件。 - 在应用的
MauiProgram
类中使用 ConfigureMauiHandlers 和 AddHandler 方法注册处理程序。 有关详细信息,请参阅注册处理程序。
然后,可以使用跨平台控件。 有关详细信息,请参阅使用跨平台控件。
或者,可以转换自定义 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<TVirtualView,TPlatformView> 类中定义,需要提供两个泛型参数:
- 派生自 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)
{
}
}
PropertyMapper 是 Dictionary
,其键为 string
,值为泛型 Action
。 string
表示跨平台控件的属性名称,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<TVirtualView,TPlatformView> 该类,这需要两个类型参数:
- 派生自 View 的跨平台控件的类。
- 在平台上实现跨平台控件的本机视图的类型。 这应与处理程序中
PlatformView
属性的类型相同。
重要说明
ViewHandler<TVirtualView,TPlatformView> 类提供 VirtualView
和 PlatformView
属性。 VirtualView
属性用于从其处理程序访问跨平台控件。 PlatformView
属性用于访问每个平台上实现跨平台控件的本机视图。
每个平台处理程序实现都应重写以下方法:
- CreatePlatformView,用于创建并返回实现跨平台控件的本机视图。
- ConnectHandler,用于执行任何本机视图设置,例如初始化本机视图和执行事件订阅。
- DisconnectHandler,用于执行任何本机视图清理,例如取消订阅事件和释放对象。 这种方法有意不由 .NET MAUI 调用。 实际上,你必须从应用生命周期中的合适位置自行调用它。 有关详细信息,请参阅本机视图清理。
- CreatePlatformView,用于创建并返回实现跨平台控件的本机视图。
- ConnectHandler,用于执行任何本机视图设置,例如初始化本机视图和执行事件订阅。
- DisconnectHandler,用于执行任何本机视图清理,例如取消订阅事件和释放对象。 默认情况下,.NET MAUI 会自动调用此方法,尽管此行为可以更改。 有关详细信息,请参阅 控制处理程序断开连接。
注意
在 Xamarin.Forms 自定义呈现器中,CreatePlatformView、ConnectHandler 和 DisconnectHandler 替代可替代 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<TVirtualView,TPlatformView> 类,其中泛型 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();
}
}
处理程序使用 ConfigureMauiHandlers 和 AddHandler 方法注册。 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();
}
控制处理程序断开连接
每个平台的处理程序实现都会重写 DisconnectHandler 实现,该实现用于执行本地视图清理,如取消订阅事件和释放对象。 默认情况下,处理程序会尽可能自动与其控件断开连接,例如在应用中向后导航时。
在某些情况下,你可能希望控制处理程序何时与其控件断开连接,这可以通过附加属性实现 HandlerProperties.DisconnectPolicy
。 此属性需要一个 HandlerDisconnectPolicy 参数,枚举定义以下值:
Automatic
,指示处理程序将自动断开连接。 这是附加属性HandlerProperties.DisconnectPolicy
的默认值。Manual
,指示处理程序必须通过调用 DisconnectHandler() 实现来手动断开连接。
以下示例演示如何设置 HandlerProperties.DisconnectPolicy
附加属性:
<controls:CustomEntry x:Name="customEntry"
Text="Hello world"
TextColor="Blue"
HandlerProperties.DisconnectPolicy="Manual" />
将附加属性设置为HandlerProperties.DisconnectPolicy
Manual
时,必须从应用的生命周期中的合适位置自行调用处理程序DisconnectHandler的实现。 这可以通过调用 customEntry.Handler?.DisconnectHandler();
来实现。
此外,还有一种 DisconnectHandlers 扩展方法可将处理程序与给定 IView 断开连接:
video.DisconnectHandlers();
断开连接时,DisconnectHandlers 方法将沿控件树向下传播,直到完成或到达已设置手动策略的控件。