使用处理程序自定义控件

浏览示例。 浏览示例

可以自定义处理程序,以增强跨平台控件的外观和行为,超越控件 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 视图是实现接口的单行文本输入控件 IEntryEntryHandlerEntry 视图映射到每个平台的以下本机视图:

  • iOS/Mac CatalystUITextField
  • AndroidAppCompatEditText
  • WindowsTextBox
  • iOS/Mac CatalystUITextField
  • AndroidMauiAppCompatEditText
  • WindowsTextBox

下图显示了如何 Entry 通过 EntryHandler以下方法将视图映射到其本机视图:

入口处理程序体系结构。

入口处理程序体系结构。

Entry 中的 EntryHandler 属性映射器将跨平台控件属性映射到本机视图 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
}
#if ANDROID
using Microsoft.Maui.Platform;
#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 MauiAppCompatEditText).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 上本机视图事件的订阅。

分部类

还可以使用分部类将控件自定义代码组织到特定于平台的文件夹和文件中,而不是使用条件编译。 使用此方法,自定义代码分为跨平台分部类和特定于平台的分部类:

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

以下示例演示跨平台分部类:

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 子文件夹中,以确保构建系统仅在针对特定平台生成时尝试构建本机代码。 例如,以下代码显示 CustomizeEntryPartialMethodsPage 项目的 Platforms>Windows 文件夹中的类:

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 项目中 平台 文件夹的组织的信息,请参阅 分部类和方法。 有关如何配置多目标以便无需将平台代码放入 “平台” 文件夹的子文件夹中的信息,请参阅 “配置多目标”。