在.NET MAUI中自定义控件详细教程

1,105 阅读7分钟

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

让我们稍微关注一下 EffectsAPI。它是由于Xamarin缺乏多目标架构而产生的。这意味着我们不能在共享层面访问平台特定的代码(在.NET标准csproj )。它的效果相当好,可以让你免于创建自定义渲染器

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

因此,让我们来谈谈在.NET MAUI中定制一个控件的所有可能性,以及你可能会在路上发现的一些龙。为此,我们将对Image 控件进行定制,增加对图像进行着色的能力。

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

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

定制一个现有的控件

为了给现有的控件添加额外的功能,我们要扩展它,并添加我们需要的功能。

让我们创建一个新的控件,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)
        {
            // Workaround for when this executes the Handler and PlatformView is null
            control.HandlerChanged += OnHandlerChanged;
            return;
        }

        if (tintColor is not null)
        {
#if ANDROID
            // Note the use of Android.Widget.ImageView which is an Android-specific API
            // You can find the Android implementation of `ApplyColor` here: https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.android.cs#L9-L12
            ImageExtensions.ApplyColor((Android.Widget.ImageView)control.Handler.PlatformView, tintColor);
#elif IOS
            // Note the use of UIKit.UIImage which is an iOS-specific API
            // You can find the iOS implementation of `ApplyColor` here: 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
            // Note the use of Android.Widget.ImageView which is an Android-specific API
            // You can find the Android implementation of `ClearColor` here: https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.android.cs#L14-L17
            ImageExtensions.ClearColor((Android.Widget.ImageView)control.Handler.PlatformView);
#elif IOS
            // Note the use of UIKit.UIImage which is an iOS-specific API
            // You can find the iOS implementation of `ClearColor` here: 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.ApplyColorImageExtensions.ClearColor 方法是帮助方法,它们将添加或删除图像的色调。

你可能注意到的一件事是null 检查HandlerPlatformView 。这是你在路上可能发现的第一条龙。当Image 控件被创建并实例化,PropertyChanged 委托的BindableProperty 被调用时,Handler 可以是null 。因此,如果没有那个空值检查,代码就会抛出一个NullReferenceException 。这听起来像是一个bug,但实际上是一个特性!这使得.NET MAUI可以实现对空值的检查。这允许.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>

使用附加属性和PropertyMapper

另一种自定义控件的方法是使用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,因为成员是static 。我选择在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
                // Note the use of Android.Widget.ImageView which is an Android-specific API
                // You can find the Android implementation of `ApplyColor` here: https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.android.cs#L9-L12
                ImageExtensions.ApplyColor((Android.Widget.ImageView)control.Handler.PlatformView, tintColor);
#elif IOS
                // Note the use of UIKit.UIImage which is an iOS-specific API
                // You can find the iOS implementation of `ApplyColor` here: https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.ios.cs#L7-L11
                ImageExtensions.ApplyColor((UIKit.UIImageView)handler.PlatformView, tintColor);
#endif
            }
            else
            {
#if ANDROID
                // Note the use of Android.Widget.ImageView which is an Android-specific API
                // You can find the Android implementation of `ClearColor` here: https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.android.cs#L14-L17
                ImageExtensions.ClearColor((Android.Widget.ImageView)handler.PlatformView);
#elif IOS
                // Note the use of UIKit.UIImage which is an iOS-specific API
                // You can find the iOS implementation of `ClearColor` here: https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.ios.cs#L13-L16
                ImageExtensions.ClearColor((UIKit.UIImageView)handler.PlatformView);
#endif
            }
        });
    }
}

代码与之前显示的基本相同,只是使用另一个API实现,在这里是AppendToMapping 方法。如果你不想要这种行为,可以使用CommandMapper ,它将在一个属性改变或一个动作发生时被触发。

请注意,当我们用MapperCommandMapper 来处理时,我们将为项目中使用该处理程序的所有控件添加这种行为。在这种情况下,所有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>

使用PlatformBehavior

PlatformBehavior 是在.NET MAUI上创建的一个新的API,当你需要以安全的方式访问平台特定的API时(安全是因为它确保 和 ,而不是 ),使定制控件的任务更加容易。它有两个方法来访问 : 和 。这个API的存在是为了取代Xamarin.Forms中的 API,并利用多目标架构的优势。Handler PlatformView null override OnAttachedTo OnDetachedFrom Effect

在这个例子中,我们将使用partial class 来实现特定平台的API:

//FileName : 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 平台的代码:

//FileName: ImageTintColorBehavior.android.cs

public partial class IconTintColorBehavior : PlatformBehavior<Image, ImageView> // Note the use of ImageView which is an Android-specific API
{
    protected override void OnAttachedTo(Image bindable, ImageView platformView) =>
        ImageExtensions.ApplyColor(bindable, platformView); // You can find the Android implementation of `ApplyColor` here: https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.android.cs#L9-L12

    protected override void OnDetachedFrom(Image bindable, ImageView platformView) =>
        ImageExtensions.ClearColor(platformView); // You can find the Android implementation of `ClearColor` here: https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.android.cs#L14-L17
}

而这里是iOS 平台的代码:

//FileName: ImageTintColorBehavior.ios.cs

public partial class IconTintColorBehavior : PlatformBehavior<Image, UIImageView> // Note the use of UIImageView which is an iOS-specific API
{
    protected override void OnAttachedTo(Image bindable, UIImageView platformView) => 
        ImageExtensions.ApplyColor(bindable, platformView); // You can find the iOS implementation of `ApplyColor` here: https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.ios.cs#L7-L11

    protected override void OnDetachedFrom(Image bindable, UIImageView platformView) => 
        ImageExtensions.ClearColor(platformView); // You can find the iOS implementation of `ClearColor` here: https://github.com/pictos/MFCC/blob/1ef490e507385e050b0cfb6e4f5d68f0cb0b2f60/MFCC/TintColorExtension.ios.cs#L13-L16
}

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

我们可以指定该行为所涵盖的平台特定API的类型。iOS如果你想为多个类型的控件应用,你不需要指定平台视图的类型(例如使用PlatformBehavior<T> );你可能想在多个控件中应用你的Behavior ,在这种情况下,平台视图将是PlatformBehavior<View> ,在Android ,在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>

注意:当HandlerVirtualView 断开时,PlatformBehavior 将调用OnDetachedFrom ,换句话说,当Unloaded 事件被触发时。Behavior API不会自动调用OnDetachedFrom 方法,作为开发者,你需要自己处理它。

总结

在这篇博文中,我们讨论了定制你的控件和与平台特定API交互的各种方法。没有right ,也没有wrong ,所有这些都是有效的解决方案,你只需要看看哪个更适合你的情况。我想说的是,在大多数情况下,你想使用PlatformBehavior ,因为它被设计为与多目标方法一起工作,并确保在控件不再被使用时清理资源。要了解更多,请查看关于自定义控件的文档