在 .NET MAUI 中如何更好地自定义控件

点击上方蓝字

关注我们

(本文阅读时间:10分钟)

今天,我想谈谈并向您展示在.NET MAUI中完全自定义控件的方法。在查看 .NET MAUI 之前,让我们回到几年前,回到 Xamarin.Forms 时代。那时,我们有很多自定义控件的方法, 比如当您不需要访问平台特有的 API 来自定义控件时,可以使用Behaviors ; 如果您需要访问平台特有的 API,可以使用 Effects。

让我们稍微关注一下Effects API。它是由于 Xamarin 缺乏多目标体系结构而创建的。这意味着我们无法在共享级别(在 .NET 标准 csproj 中)访问特定于平台的代码。它工作得很好,可以让您免于创建自定义渲染器。

今天,在 .NET MAUI 中,我们可以利用多目标架构的强大功能,并在我们的共享项目中访问特定于平台的 API。那么我们还需要 Effects 吗?不需要了,因为我们可以访问我们所需要的所有平台的所有代码和 API。

那么让我们谈谈在 .NET MAUI 中自定义一个控件的所有可能性以及在此过程中您可以遇到的一些障碍。为此,我们将自定义 Image 控件,添加对呈现的图像进行着色的功能。

注意:如果您想使用 Effects ,.NET MAUI仍然支持,但不建议使用

源代码参考来自 .NET MAUI Community Toolkit 的IconTintColor。

  • .NET MAUI:

    https://dotnet.microsoft.com/zh-cn/apps/maui?ocid=AID3052907

  • Xamarin.Forms:

    https://docs.microsoft.com/zh-cn/xamarin/xamarin-forms/?WT.mc_id=dotnet-0000-bramin?ocid=AID3052907

  • Behaviors:

    https://docs.microsoft.com/zh-cn/xamarin/xamarin-forms/app-fundamentals/behaviors/?WT.mc_id=dotnet-0000-bramin?ocid=AID3052907

  • Effects:

    https://docs.microsoft.com/zh-cn/xamarin/xamarin-forms/app-fundamentals/effects/introduction?WT.mc_id=dotnet-0000-bramin?ocid=AID3052907

  • 自定义渲染器:

    https://docs.microsoft.com/zh-cn/xamarin/xamarin-forms/app-fundamentals/custom-renderer/?WT.mc_id=dotnet-0000-bramin?ocid=AID3052907

  • IconTintColor:

    https://github.com/CommunityToolkit/Maui/tree/main/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/IconTintColor

8322a8da2ea7cdce015911a7e9105d9e.png

自定义现有控件 

090094abc28d61280f5f3404d26836bf.png

要向现有控件添加额外的功能,需要我们对其进行扩展并添加所需的功能。

让我们创建一个新控件,class ImageTintColor : Image 并添加一个新的 BindableProperty,我们将利用它来更改 Image 的色调颜色。

public class ImageTintColor : Image
{
    public static readonly BindableProperty TintColorProperty =
        BindableProperty.Create(nameof(TintColor), typeof(Color), typeof(ImageTintColor), propertyChanged: OnTintColorChanged);


    public Color? TintColor
    {
        get => (Color?)GetValue(TintColorProperty);
        set => SetValue(TintColorProperty, value);
    }


    static void OnTintColorChanged(BindableObject bindable, object oldValue, object newValue)
    {
        // ...
    }
}

熟悉 Xamarin.Forms 的人会认识到这一点;它与您将在 Xamarin.Forms 应用程序中编写的代码几乎相同。

.NET MAUI 平台特定的 API 工作将在 OnTintColorChanged 委托上进行。让我们来看看。

public class ImageTintColor : Image
{
    public static readonly BindableProperty TintColorProperty =
        BindableProperty.Create(nameof(TintColor), typeof(Color), typeof(ImageTintColor), propertyChanged: OnTintColorChanged);


    public Color? TintColor
    {
        get => (Color?)GetValue(TintColorProperty);
        set => SetValue(TintColorProperty, value);
    }


    static void OnTintColorChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var control = (ImageTintColor)bindable;
        var tintColor = control.TintColor;


        if (control.Handler is null || control.Handler.PlatformView is null)
        {
            // 执行 Handler 且 PlatformView 为 null 时的解决方法
            control.HandlerChanged += OnHandlerChanged;
            return;
        }


        if (tintColor is not null)
        {
#if ANDROID
            // 注意 Android.Widget.ImageView 的使用,它是一个 Android 特定的 API
            // 您可以在这里找到`ApplyColor`的Android实现:https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.android.cs#L9-L12
            ImageExtensions.ApplyColor((Android.Widget.ImageView)control.Handler.PlatformView, tintColor);
#elif IOS
            // 注意 UIKit.UIImage 的使用,它是一个 iOS 特定的 API
            // 您可以在这里找到`ApplyColor`的iOS实现:https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.ios.cs#L7-L11
            ImageExtensions.ApplyColor((UIKit.UIImageView)control.Handler.PlatformView, tintColor);
#endif
        }
        else
        {
#if ANDROID
            // 注意 Android.Widget.ImageView 的使用,它是一个 Android 特定的 API
            // 您可以在这里找到 `ClearColor` 的 Android 实现:https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.android.cs#L14-L17
            ImageExtensions.ClearColor((Android.Widget.ImageView)control.Handler.PlatformView);
#elif IOS
            // 注意 UIKit.UIImage 的使用,它是一个 iOS 特定的 API
            // 您可以在这里找到`ClearColor`的iOS实现:https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.ios.cs#L13-L16
            ImageExtensions.ClearColor((UIKit.UIImageView)control.Handler.PlatformView);
#endif
        }


        void OnHandlerChanged(object s, EventArgs e)
        {
            OnTintColorChanged(control, oldValue, newValue);
            control.HandlerChanged -= OnHandlerChanged;
        }
    }
}

因为 .NET MAUI 使用多目标,我们可以访问平台的详细信息并按照我们想要的方式自定义控件。ImageExtensions.ApplyColor 和 ImageExtensions.ClearColor 方法是添加或删除图像色调的辅助方法。

您可能会注意到 Handler 和 PlatformView 的 null 检查。这可能是您在使用过程中遇到的第一个阻碍。在创建和实例化 Image 控件并调用 BindableProperty 的 PropertyChanged 委托时,Handler 可以为 null。因此,如果不进行 null 检查,代码将抛出 NullReferenceException。这听起来像一个bug,但它实际上是一个特性!这使 .NET MAUI 工程团队能够保持与 Xamarin.Forms 上的控件相同的生命周期,从而避免从 Forms 迁移到 .NET MAUI 的应用程序的一些重大更改。

现在我们已经完成了所有设置,可以在 ContentPage 中使用控件了。在下面的代码片段中,您可以看到如何在 XAML 中使用它:

<ContentPage x:Class="MyMauiApp.ImageControl"
             xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MyMauiApp"
             Title="ImageControl"
             BackgroundColor="White">


            <local:ImageTintColor x:Name="ImageTintColorControl"
                                  Source="shield.png"
                                  TintColor="Orange" />
</ContentPage>

3d86cd6ec62502a53631e20addb55b48.png

使用附加属性和 PropertyMapper 

1182d78d5d3efa08cd155587bfdb8fd3.png

自定义控件的另一种方法是使用 AttachedProperties,当您不需要将其绑定到特定的自定义控件时是 使用BindableProperty。

下面是我们如何为 TintColor 创建一个 AttachedProperty:

public static class TintColorMapper
{
    public static readonly BindableProperty TintColorProperty = BindableProperty.CreateAttached("TintColor", typeof(Color), typeof(Image), null);


    public static Color GetTintColor(BindableObject view) => (Color)view.GetValue(TintColorProperty);


    public static void SetTintColor(BindableObject view, Color? value) => view.SetValue(TintColorProperty, value);


    public static void ApplyTintColor()
    {
        // ...
    }
}

同样,我们在 Xamarin.Forms 上为 AttachedProperty 提供了样板,但如您所见,我们没有 PropertyChanged 委托。为了处理属性更改,我们将使用 ImageHandler 中的 Mapper。您可以在任何级别添加 Mapper,因为成员是静态的。我选择在 TintColorMapper 类中执行此操作,如下所示。

public static class TintColorMapper
{
     public static readonly BindableProperty TintColorProperty = BindableProperty.CreateAttached("TintColor", typeof(Color), typeof(Image), null);


    public static Color GetTintColor(BindableObject view) => (Color)view.GetValue(TintColorProperty);


    public static void SetTintColor(BindableObject view, Color? value) => view.SetValue(TintColorProperty, value);


    public static void ApplyTintColor()
    {
        ImageHandler.Mapper.Add("TintColor", (handler, view) =>
        {
            var tintColor = GetTintColor((Image)handler.VirtualView);


            if (tintColor is not null)
            {
#if ANDROID
                // 注意 Android.Widget.ImageView 的使用,它是一个 Android 特定的 API
                // 您可以在这里找到`ApplyColor`的Android实现:https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.android.cs#L9-L12
                ImageExtensions.ApplyColor((Android.Widget.ImageView)control.Handler.PlatformView, tintColor);
#elif IOS
                // 注意 UIKit.UIImage 的使用,它是一个 iOS 特定的 API
                // 您可以在这里找到`ApplyColor`的iOS实现:https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.ios.cs#L7-L11
                ImageExtensions.ApplyColor((UIKit.UIImageView)handler.PlatformView, tintColor);
#endif
            }
            else
            {
#if ANDROID
                // 注意 Android.Widget.ImageView 的使用,它是一个 Android 特定的 API
                // 您可以在这里找到 `ClearColor` 的 Android 实现:https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.android.cs#L14-L17
                ImageExtensions.ClearColor((Android.Widget.ImageView)handler.PlatformView);
#elif IOS
                // 注意 UIKit.UIImage 的使用,它是一个 iOS 特定的 API
                // 您可以在这里找到`ClearColor`的iOS实现:https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.ios.cs#L13-L16
                ImageExtensions.ClearColor((UIKit.UIImageView)handler.PlatformView);
#endif
            }
        });
    }
}

代码与之前显示的几乎相同,只是使用了另一个 API 实现,在本例中是 AppendToMapping 方法。如果您不想要这种行为,可以改用 CommandMapper,它将在属性更改或操作发生时触发。

请注意,当我们处理 Mapper 和 CommandMapper 时,我们将为项目中使用该处理程序的所有控件添加此行为。在这种情况下,所有Image控件都会触发此代码。在某些情况下这可能并不是您想要的,如果您需要更具体的方法, PlatformBehavior 方法将会非常适合。

现在我们已经设置好了所有内容,可以在页面中使用控件了,在下面的代码片段中,您可以看到如何在 XAML 中使用它。

<ContentPage x:Class="MyMauiApp.ImageControl"
             xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MyMauiApp"
             Title="ImageControl"
             BackgroundColor="White">


            <Image x:Name="Image"
                   local:TintColorMapper.TintColor="Fuchsia"
                   Source="shield.png" />
</ContentPage>

b0e68413ff0d6914c950018a60ad90c0.png

使用PlatformBehavior

0a30578feb4b9ac48222c8a15efea18e.png

PlatformBehavior 是在 .NET MAUI 上创建的新 API,它让您在需要以安全的方式访问平台特有的 API 时,可以更轻松地自定义控件(这是安全的因为它确保 Handler 和 PlatformView 不为 null )。它有两种方法来重写:OnAttachedTo 和 OnDetachedFrom。此 API 用于替换 Xamarin.Forms 中的 Effect API 并利用多目标体系结构。

在此示例中,我们将使用部分类来实现特定于平台的 API:

//文件名 : ImageTintColorBehavior.cs


public partial class IconTintColorBehavior 
{
    public static readonly BindableProperty TintColorProperty =
        BindableProperty.Create(nameof(TintColor), typeof(Color), typeof(IconTintColorBehavior), propertyChanged: OnTintColorChanged);


    public Color? TintColor
    {
        get => (Color?)GetValue(TintColorProperty);
        set => SetValue(TintColorProperty, value);
    }
}

上面的代码将被我们所针对的所有平台编译。

现在让我们看看 Android 平台的代码:

//文件名: ImageTintColorBehavior.android.cs


public partial class IconTintColorBehavior : PlatformBehavior<Image, ImageView> 
// 注意 ImageView 的使用,它是 Android 特定的 API{
    protected override void OnAttachedTo(Image bindable, ImageView platformView) =>
        ImageExtensions.ApplyColor(bindable, platformView); 
// 您可以在这里找到`ApplyColor`的Android实现:https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.android.cs#L9-L12


    protected override void OnDetachedFrom(Image bindable, ImageView platformView) =>
        ImageExtensions.ClearColor(platformView); 
// 您可以在这里找到 `ClearColor` 的 Android 实现:https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.android.cs#L14-L17
}

这是 iOS 平台的代码:

//文件名: ImageTintColorBehavior.ios.cs


public partial class IconTintColorBehavior : PlatformBehavior<Image, UIImageView> 
// 注意 UIImageView 的使用,它是一个 iOS 特定的 API
{
    protected override void OnAttachedTo(Image bindable, UIImageView platformView) => 
        ImageExtensions.ApplyColor(bindable, platformView); 
// 你可以在这里找到`ApplyColor`的iOS实现:https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.ios.cs#L7-L11


    protected override void OnDetachedFrom(Image bindable, UIImageView platformView) => 
        ImageExtensions.ClearColor(platformView); 
// 你可以在这里找到`ClearColor`的iOS实现:https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.ios.cs#L13-L16
}

正如您所看到的,我们不需要关心是否 Handler 为 null ,因为 PlatformBehavior<T, U> 会为我们处理。

我们可以指定此行为涵盖的平台特有的 API 的类型。如果您想为多个类型应用控件,则无需指定平台视图的类型(例如,使用 PlatformBehavior<T> );您可能想在多个控件中应用您的行为,在这种情况下,platformView 将是 Android 上的 PlatformBehavior<View> 和 iOS 上的 PlatformBehavior<UIView>。

而且用法更好,您只需要调用 Behavior 即可:

<ContentPage x:Class="MyMauiApp.ImageControl"
             xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MyMauiApp"
             Title="ImageControl"
             BackgroundColor="White">


            <Image x:Name="Image"
                   Source="shield.png">
                <Image.Behaviors>
                    <local:IconTintColorBehavior TintColor="Fuchsia">
                </Image.Behaviors>
            </Image>
</ContentPage>

注意:当 Handler 与 VirtualView 断开连接时,即触发 Unloaded 事件时,PlatformBehavior 将调用 OnDetachedFrom。Behavior API 不会自动调用 OnDetachedFrom 方法,作为开发者需要自己处理。

4ea1d776ea876a98d16c6b100dc0dd29.png

总结

423fad8a965839bf5b87b91a6dbf1142.png

在这篇文章中,我们讨论了自定义控件以及与平台特有的 API 交互的各种方式。没有正确或错误的方法,所有这些都是有效的解决方案,您只需要看看哪种方法更适合您的情况。我想说的是,在大多数情况下,您会想要使用 PlatformBehavior,因为它旨在使用多目标方法并确保在控件不再使用时清理资源。要了解更多信息,请查看有关自定义控件的文档。

  • 有关自定义控件的文档:

    https://docs.microsoft.com/zh-cn/dotnet/maui/user-interface/handlers/customize?ocid=AID3052907

  • 有关更多的MAUI workshop,请查看:

    https://github.com/dotnet-presentations/dotnet-maui-workshop/blob/main/README.zh-cn.md

ef44044f3f79ede0a415f06dbf5ddf5a.png

谢谢你读完了本文~相信你一定有一些感想、观点、问题想要表达。欢迎在评论区畅所欲言,期待听到你的“声音”哦!

同时,喜欢的内容也不要忘记转发给你的小伙伴们,谢谢你的支持!

65b13b4e2006383c848d17336c6f36c1.jpeg

长按识别二维码

关注微软中国MSDN

2c6b733f2510ad16a37e00ac95cdb4d2.gif

点击「阅读原文」了解更多~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值