點擊上方藍字
關注我們
(本文閱讀時間: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
自定義現有控件?
要向現有控件添加額外的功能,需要我們對其進行擴展并添加所需的功能。
讓我們創建一個新控件,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-L12ImageExtensions.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-L11ImageExtensions.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-L17ImageExtensions.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-L16ImageExtensions.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>
使用附加屬性和 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,因為成員是靜態的。我選擇在 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-L12ImageExtensions.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-L11ImageExtensions.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-L17ImageExtensions.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-L16ImageExtensions.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
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-L12protected 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-L11protected 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
點擊「閱讀原文」了解更多~