今天,我想谈谈并向你展示在.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.ApplyColor 和ImageExtensions.ClearColor 方法是帮助方法,它们将添加或删除图像的色调。
你可能注意到的一件事是null 检查Handler 和PlatformView 。这是你在路上可能发现的第一条龙。当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 ,它将在一个属性改变或一个动作发生时被触发。
请注意,当我们用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时(安全是因为它确保 和 ,而不是 ),使定制控件的任务更加容易。它有两个方法来访问 : 和 。这个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>
注意:当
Handler与VirtualView断开时,PlatformBehavior将调用OnDetachedFrom,换句话说,当Unloaded事件被触发时。BehaviorAPI不会自动调用OnDetachedFrom方法,作为开发者,你需要自己处理它。
总结
在这篇博文中,我们讨论了定制你的控件和与平台特定API交互的各种方法。没有right ,也没有wrong ,所有这些都是有效的解决方案,你只需要看看哪个更适合你的情况。我想说的是,在大多数情况下,你想使用PlatformBehavior ,因为它被设计为与多目标方法一起工作,并确保在控件不再被使用时清理资源。要了解更多,请查看关于自定义控件的文档。