今天,我想談談並向您展示在.NET MAUI中完全自定義控件的方法。在查看 .NET MAUI 之前,讓我們回到幾年前,回到 Xamarin.Forms 時代。那時,我們有很多自定義控件的方法, 比如當您不需要訪問平台特有的 API 來自定義控件時,可以使用Behaviors ; 如果您需要訪問平台特有的 API,可以使用 Effects。
讓我們稍微關注一下Effects API。它是由於 Xamarin 缺乏多目標體系結構而創建的。這意味着我們無法在共享級別(在 .NET 標準 csproj 中)訪問特定於平台的代碼。它工作得很好,可以讓您免於創建自定義渲染器。
今天,在 .NET MAUI 中,我們可以利用多目標架構的強大功能,並在我們的共享項目中訪問特定於平台的 API。那麼我們還需要Effects嗎?不需要了,因為我們可以訪問我們所需要的所有平台的所有代碼和 API。
那麼讓我們談談在 .NET MAUI 中自定義一個控件的所有可能性以及在此過程中您可以遇到的一些障礙。為此,我們將自定義 Image 控件,添加對呈現的圖像進行着色的功能。
注意:如果您想使用Effects,.NETMAUI仍然支持,但不建議使用
源代碼參考來自 .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
要向現有控件添加額外的功能,需要我們對其進行擴展並添加所需的功能。
讓我們創建一個新控件,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>自定義控件的另一種方法是使用 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>
PlatformBehavior 是在 .NET MAUI 上創建的新 API,它讓您在需要以安全的方式訪問平台特有的 API 時,可以更輕鬆地自定義控件(這是安全的因為它確保 Handler 和 PlatformView 不為 null )。它有兩種方法來重寫:OnAttachedTo 和 OnDetachedFrom。此 API 用於替換 Xamarin.Forms 中的 Effect API 並利用多目標體系結構。
在此示例中,我們將使用部分類來實現特定於平台的 API:
//文件名 : ImageTintColorBehavior.cspublic 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.cspublic 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.cspublic 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 方法,作為開發者需要自己處理。
在這篇文章中,我們討論了自定義控件以及與平台特有的 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
謝謝你讀完了本文~相信你一定有一些感想、觀點、問題想要表達。歡迎在評論區暢所欲言,期待聽到你的「聲音」哦!同時,喜歡的內容也不要忘記轉發給你的小夥伴們,謝謝你的支持!
長按識別二維碼
關注微軟中國MSDN