Today I want to talk about and show you how to fully customize controls in .NET MAUI. Before looking at .NET MAUI , let's go back a few years, back to the Xamarin.Forms days. Back then, we had a lot of ways to customize controls, such as Behaviors when you didn't need access to platform-specific APIs to customize controls; and Effects if you needed to access platform-specific APIs.
Let's take a little look at the Effects API. It was created due to Xamarin's lack of multi-target architecture. This means that we cannot access platform-specific code at the shared level (in .NET standard csproj). It works great and saves you from creating custom renderers.
Today, in .NET MAUI, we can leverage the power of the multi-target architecture and access platform-specific APIs in our shared projects. So do we still need Effects? No need because we have access to all the code and APIs for all the platforms we need.
So let's talk about all the possibilities of customizing a control in .NET MAUI and some of the obstacles you can encounter along the way. To do this, we'll customize the Image control to add the ability to colorize the rendered image.
Note: If you want to use Effects, .NET MAUI still supports it, but it is not recommended to use the source code reference IconTintColor from .NET MAUI Community Toolkit.
Customize existing controls
To add extra functionality to an existing control requires us to extend it and add the required functionality. Let's create a new control, class ImageTintColor : Image and add a new
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)
{
// ...
}
}
Those familiar with Xamarin.Forms will recognize this; it's pretty much the same code you would write in a Xamarin.Forms application.
.NET MAUI platform specific API work will be done on the OnTintColorChanged delegate. let's see.
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;
}
}
}
Because .NET MAUI uses multi-targeting, we can access platform details and customize the controls the way we want. The ImageExtensions.ApplyColor and ImageExtensions.ClearColor methods are helper methods for adding or removing tints from an image.
You may notice null checks for Handler and PlatformView . This may be the first obstacle you encounter while using it. Handler can be null when creating and instantiating an Image control and calling BindableProperty's PropertyChanged delegate. So without null checking, the code will throw NullReferenceException. It sounds like a bug, but it's actually a feature! This enables the .NET MAUI engineering team to maintain the same lifecycle as controls on Xamarin.Forms, avoiding some breaking changes for applications migrating from Forms to .NET MAUI.
Now that we have everything set up, we can use the controls in the ContentPage. In the code snippet below you can see how to use it in 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>
Using Attached Properties and PropertyMapper
Another way to customize a control is to use AttachedProperties, or BindableProperty when you don't need to bind it to a specific custom control.
Here's how we create an AttachedProperty for TintColor:
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()
{
// ...
}
}
Again, we have boilerplate for AttachedProperty on Xamarin.Forms, but as you can see, we don't have a PropertyChanged delegate. To handle property changes, we'll use the Mapper from ImageHandler. You can add Mapper at any level because members are static. I chose to do this in the TintColorMapper class as shown below.
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
}
});
}
}
The code is pretty much the same as shown before, just using another API implementation, in this case the AppendToMapping method. If you don't want this behavior, you can use CommandMapper instead, which will fire when a property changes or when an action occurs.
Note that when we handle Mapper and CommandMapper, we'll add this behavior to all controls in the project that use that handler. In this case, all Image controls fire this code. In some cases this might not be what you want, and if you need a more specific approach, the PlatformBehavior approach will be a good fit.
Now that we have everything set up, we can use the control in the page, in the code snippet below you can see how to use it in 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>
Use platform behavior
PlatformBehavior is a new API created on .NET MAUI that makes it easier to customize controls when you need to access platform-specific APIs in a safe way (this is safe because it ensures that Handler and PlatformView are not null) . It has two methods to override: OnAttachedTo and OnDetachedFrom. This API is used to replace the Effect API in Xamarin.Forms and take advantage of the multi-target architecture.
In this example, we'll use a partial class to implement a platform-specific 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);
}
}
The above code will compile for all platforms we are targeting.
Now let's look at the code for the Android platform:
//文件名: 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
}
Here is the code for the iOS platform:
//文件名: 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
}
As you can see, we don't need to care if Handler is null , because PlatformBehavior<T, U> will handle it for us.
We can specify the types of platform-specific APIs covered by this behavior. If you want to apply controls for multiple types, you don't need to specify the type of platform view (for example, using PlatformBehavior<T> ); you may want to apply your behavior across multiple controls, in which case platformView will be Android PlatformBehavior<View> on iOS and PlatformBehavior<UIView> on iOS.
And the usage is better, you just need to call 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>
Note: PlatformBehavior will call OnDetachedFrom when the Handler is disconnected from the VirtualView, i.e. when the Unloaded event is fired. The Behavior API will not automatically call the OndetachedFrom method, as a developer you need to handle it yourself.
Summarize
In this article, we discussed custom controls and various ways to interact with platform-specific APIs. There is no right or wrong way, all of these are valid solutions, you just need to see which is better for your situation. I would say that in most cases you will want to use PlatformBehavior as it is designed to use a multi-target approach and ensure resources are cleaned up when the control is no longer in use. To learn more, check out the documentation on custom controls .
Long press to identify the QR code and follow Microsoft Developer MSDN
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。