使用处理程序自定义控件

浏览示例。浏览示例

可以自定义处理程序,以增强跨平台控件的外观和行为,而不仅仅是通过控件的 API 实现自定义。 此自定义(修改跨平台控件的本机视图)可通过使用以下方法之一修改处理程序的映射器来实现:

  • PrependToMapping,用于在应用 .NET MAUI 控件映射之前修改处理程序的映射器。
  • ModifyMapping,用于修改现有映射。
  • AppendToMapping,用于在应用 .NET MAUI 控件映射后修改处理程序的映射器。

其中每个方法都有一个需要两个参数的相同签名:

  • 基于 string 的密钥。 修改 .NET MAUI 提供的其中一个映射时,必须指定 .NET MAUI 使用的密钥。 .NET MAUI 控件映射使用的密钥值基于接口和属性名称,例如 nameof(IEntry.IsPassword)。 可以在此处查看提取每个跨平台控件的接口及其属性。 这是一种键格式,如果希望处理程序自定义在每次属性更改时都会运行,则应使用该格式。 否则,键可以是一个任意值,无需与类型公开的属性名称相对应。 例如,可以将 MyCustomization 指定为密钥,并将任何本机视图修改作为自定义执行。 但此键格式的后果是,只有在首次修改处理程序的映射器时,才会运行处理程序自定义。
  • Action 表示执行处理程序自定义的方法。 Action 指定两个参数:
    • handler 参数,提供要自定义的处理程序的实例。
    • view 参数,提供处理程序实现的跨平台控件的实例。

重要

处理程序自定义是全局的,不限于特定的控件实例。 允许在应用中的任意位置进行处理程序自定义。 自定义处理程序后,它会影响该类型在应用中任意位置上的所有控件。

每个处理程序类通过其 PlatformView 属性公开跨平台控件的本机视图。 可以访问此属性以设置本机视图属性、调用本机视图方法和订阅本机视图事件。 此外,处理程序实现的跨平台控件通过其 VirtualView 属性公开。

可以使用条件编译为每个平台自定义处理程序,以基于平台对代码多目标化。 或者,可以使用分部类将代码组织到特定于平台的文件夹和文件中。 有关条件编译的详细信息,请参阅条件编译

自定义控件

.NET MAUI Entry 视图是实现 IEntry 接口的单行文本输入控件。 EntryHandlerEntry 视图映射到每个平台的以下本机视图:

  • iOS/Mac CatalystUITextField
  • AndroidAppCompatEditText
  • WindowsTextBox

下图显示如何通过 EntryHandlerEntry 视图映射到其本机视图:

Entry 的处理程序体系结构。

EntryHandler 类中的 Entry 属性映射器将跨平台控件属性映射到本机视图 API。 这可确保在 Entry 上设置属性时,基础本机视图会根据需要进行更新。

可以修改属性映射器以在每个平台上自定义 Entry

namespace CustomizeHandlersDemo.Views;

public partial class CustomizeEntryPage : ContentPage
{
    public CustomizeEntryPage()
    {
        InitializeComponent();
        ModifyEntry();
    }

    void ModifyEntry()
    {
        Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping("MyCustomization", (handler, view) =>
        {
#if ANDROID
            handler.PlatformView.SetSelectAllOnFocus(true);
#elif IOS || MACCATALYST
            handler.PlatformView.EditingDidBegin += (s, e) =>
            {
                handler.PlatformView.PerformSelector(new ObjCRuntime.Selector("selectAll"), null, 0.0f);
            };
#elif WINDOWS
            handler.PlatformView.GotFocus += (s, e) =>
            {
                handler.PlatformView.SelectAll();
            };
#endif
        });
    }
}

在此示例中,Entry 自定义发生在页面类中。 因此,创建 CustomizeEntryPage 的实例后,将自定义 Android、iOS 和 Windows 上的所有 Entry 控件。 可通过访问处理程序 PlatformView 属性来执行自定义,该属性提供对映射到每个平台上跨平台控件的本机视图的访问权限。 然后,本机代码通过在获得焦点时全选 Entry 中的文本来自定义处理程序。

有关映射器的详细信息,请参阅映射器

自定义特定控件实例

处理程序是全局的,自定义控件的处理程序将导致自定义应用中同一类型的所有控件。 但是,特定控件实例的处理程序可以通过子类化控件,然后修改基控件类型的处理程序(仅在控件是子类化类型时),进行自定义。 例如,要在包含多个 Entry 控件的页面上自定义特定的 Entry 控件,应首先创建 Entry 控件的子类:

namespace CustomizeHandlersDemo.Controls
{
    internal class MyEntry : Entry
    {
    }
}

然后,可以通过 EntryHandler 的属性映射器对其进行自定义 ,以便只对 MyEntry 实例执行所需的修改:

Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping("MyCustomization", (handler, view) =>
{
    if (view is MyEntry)
    {
#if ANDROID
        handler.PlatformView.SetSelectAllOnFocus(true);
#elif IOS || MACCATALYST
        handler.PlatformView.EditingDidBegin += (s, e) =>
        {
            handler.PlatformView.PerformSelector(new ObjCRuntime.Selector("selectAll"), null, 0.0f);
        };
#elif WINDOWS
        handler.PlatformView.GotFocus += (s, e) =>
        {
            handler.PlatformView.SelectAll();
        };
#endif
    }
});

如果处理程序自定义在 App 类中执行,则应用中的任何 MyEntry 实例都将根据处理程序的修改进行自定义。

使用处理程序生命周期自定义控件

所有基于处理程序的 .NET MAUI 控件都支持 HandlerChangingHandlerChanged 事件。 当实现跨平台控件的本机视图可用并对其进行初始化时,会引发 HandlerChanged 事件。 当将要从跨平台控件中移除控件的处理程序时,会引发 HandlerChanging 事件。 有关处理程序生命周期事件的详细信息,请参阅处理程序生命周期

处理程序生命周期可用于执行处理程序自定义。 例如,要订阅和取消订阅本机视图事件,必须在要自定义的跨平台控件上注册 HandlerChangedHandlerChanging 事件的事件处理程序:

<Entry HandlerChanged="OnEntryHandlerChanged"
       HandlerChanging="OnEntryHandlerChanging" />

通过使用条件编译,或使用分部类将代码组织到特定于平台的文件夹和文件中,可以根据平台对处理程序进行自定义。 我们将依次讨论每种方法,而通过自定义 Entry,可以在它获得焦点时选中其所有文本。

条件编译

下面的示例展示了包含 HandlerChangedHandlerChanging 事件的事件处理程序的代码隐藏文件,该示例使用条件编译:

#if ANDROID
using AndroidX.AppCompat.Widget;
#elif IOS || MACCATALYST
using UIKit;
#elif WINDOWS
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml;
#endif

namespace CustomizeHandlersDemo.Views;

public partial class CustomizeEntryHandlerLifecyclePage : ContentPage
{
    public CustomizeEntryHandlerLifecyclePage()
    {
        InitializeComponent();
    }

    void OnEntryHandlerChanged(object sender, EventArgs e)
    {
        Entry entry = sender as Entry;
#if ANDROID
        (entry.Handler.PlatformView as AppCompatEditText).SetSelectAllOnFocus(true);
#elif IOS || MACCATALYST
        (entry.Handler.PlatformView as UITextField).EditingDidBegin += OnEditingDidBegin;
#elif WINDOWS
        (entry.Handler.PlatformView as TextBox).GotFocus += OnGotFocus;
#endif
    }

    void OnEntryHandlerChanging(object sender, HandlerChangingEventArgs e)
    {
        if (e.OldHandler != null)
        {
#if IOS || MACCATALYST
            (e.OldHandler.PlatformView as UITextField).EditingDidBegin -= OnEditingDidBegin;
#elif WINDOWS
            (e.OldHandler.PlatformView as TextBox).GotFocus -= OnGotFocus;
#endif
        }
    }

#if IOS || MACCATALYST                   
    void OnEditingDidBegin(object sender, EventArgs e)
    {
        var nativeView = sender as UITextField;
        nativeView.PerformSelector(new ObjCRuntime.Selector("selectAll"), null, 0.0f);
    }
#elif WINDOWS
    void OnGotFocus(object sender, RoutedEventArgs e)
    {
        var nativeView = sender as TextBox;
        nativeView.SelectAll();
    }
#endif
}

在创建并初始化实现跨平台控件的本机视图后,将引发 HandlerChanged 事件。 因此,应在其事件处理程序中执行本机事件订阅。 这需要将处理程序的 PlatformView 属性强制转换为本机视图的类型或基类型,以便可以访问本机事件。 在此示例中,在 iOS、Mac Catalyst 和 Windows 上,OnEntryHandlerChanged 事件订阅了本机视图事件,而在实现 Entry 的本机视图获得焦点时,会引发此类事件。

OnEditingDidBeginOnGotFocus 事件处理程序在各自的平台上访问 Entry 的本机视图,并选择 Entry 中的所有文本。

在从跨平台控件中移除现有处理程序之前,以及在创建跨平台控件的新处理程序之前,将引发 HandlerChanging 事件。 因此,应在其事件处理程序中移除本机事件订阅,并执行其他清理操作。 伴随此事件的 HandlerChangingEventArgs 对象具有 OldHandlerNewHandler 属性,这两个属性将分别设置为旧处理程序和新处理程序。 在此示例中,OnEntryHandlerChanging 事件将移除 iOS、Mac Catalyst 和 Windows 上的本机视图事件的订阅。

分部类

除了使用条件编译,还可以使用分部类将控件自定义代码组织到特定于平台的文件夹和文件中。 使用这种方法,你的自定义代码被分为跨平台分部类和特定于平台的分部类:

  • 跨平台分部类通常定义成员,但不实现它们,并且是针对所有平台构建的。 此类不应放置在项目的 Platforms 文件夹的任何子文件夹中,因为这样做会使它成为特定于平台的类。
  • 特定于平台的分部类通常实现跨平台分部类中定义的成员,并且是针对单个平台构建的。 此类应放置在所选平台的 Platform 文件夹的子文件夹中。

以下示例显示了跨平台分部类:

namespace CustomizeHandlersDemo.Views;

public partial class CustomizeEntryPartialMethodsPage : ContentPage
{
    public CustomizeEntryPartialMethodsPage()
    {
        InitializeComponent();
    }

    partial void ChangedHandler(object sender, EventArgs e);
    partial void ChangingHandler(object sender, HandlerChangingEventArgs e);

    void OnEntryHandlerChanged(object sender, EventArgs e) => ChangedHandler(sender, e);
    void OnEntryHandlerChanging(object sender, HandlerChangingEventArgs e) => ChangingHandler(sender, e);
}

在此示例中,两个事件处理程序调用名为 ChangedHandlerChangingHandler 的分部方法,并在跨平台分部类中定义其签名。 然后,在特定于平台的分部类中定义分部方法实现,这些分部类应放置在正确的 Platforms 子文件夹中,以确保生成系统仅在针对特定平台执行生成时尝试生成本机代码。 例如,下面的代码显示了项目的 Platforms>Windows 文件夹中的 CustomizeEntryPartialMethodsPage 类:

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

namespace CustomizeHandlersDemo.Views
{
    public partial class CustomizeEntryPartialMethodsPage : ContentPage
    {
        partial void ChangedHandler(object sender, EventArgs e)
        {
            Entry entry = sender as Entry;
            (entry.Handler.PlatformView as TextBox).GotFocus += OnGotFocus;
        }

        partial void ChangingHandler(object sender, HandlerChangingEventArgs e)
        {
            if (e.OldHandler != null)
            {
                (e.OldHandler.PlatformView as TextBox).GotFocus -= OnGotFocus;
            }
        }

        void OnGotFocus(object sender, RoutedEventArgs e)
        {
            var nativeView = sender as TextBox;
            nativeView.SelectAll();
        }
    }
}

此方法的优点是不需要条件编译,也无需在每个平台上实现分部方法。 如果平台未提供该实现,则会在编译时删除方法以及对方法的所有调用。 有关分部方法的信息,请参阅分部方法

有关 .NET MAUI 项目中 Platforms 文件夹的组织的信息,请参阅分部类和方法。 有关如何配置多目标以便无需将平台代码放入 Platforms 文件夹的子文件夹中的信息,请参阅配置多目标