Custom controls in .NET MAUI

Today I want to talk 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 several ways to customize controls: Behaviors we used when you didn't need access to platform-specific APIs to customize a control, and Effects if you needed access to platform-specific APIs, we also had.

Let's focus a little bit on the Effects API. It was created due to Xamarin's lack of multi-target architecture. This means we cannot access platform-specific code at the shared level (csproj in .NET Standard). It works pretty well and saves you from having to create a custom renderer.

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, because we have access to all code and APIs for all platforms we target.

So let's discuss all the possibilities of customizing controls in .NET MAUI and some dragons you might find along the way. To do this, we will customize the control, Image, to add the ability to colorize the rendered image.

Note: .NET MAUIEffects is still supported if you want to use it, but not recommended

Customizing an Existing Control
To add additional functionality to an existing control, we extend it and add the functionality we need.

Let's create a new control and add a new control with which we will change the .class ImageTintColor :

ImageBindablePropertyImage

public class ImageTintColor : Image
{
    
    
    public static readonly BindableProperty TintColorProperty =
        BindableProperty.Create(nameof(TintColor), typeof(Color), typeof(TintColorBehavior), 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'd write in a Xamarin.Forms application.

.NET MAUI 平台特定的 API 工作将在OnTintColorChanged委托上进行。让我们来看看它。

public class ImageTintColor : Image
{
    
    
    public static readonly BindableProperty TintColorProperty =
        BindableProperty.Create(nameof(TintColor), typeof(Color), typeof(TintColorBehavior), 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;
        }
    }
}

Because .NET MAUI uses multi-targeting, we can access platform specifics and customize the controls the way we want. The and methods are helper methods for adding or removing tints to an image. ImageExtensions.ApplyColorImageExtensions.ClearColor

One thing you might notice is the null check Handlerand PlatformView. This is the first dragon you're likely to find along the way. When the Image control is created and instantiated and the PropertyChanged delegate is called, it can be. So without that null check, the code will throw one. It sounds like a bug, but it's actually a feature! This allows 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. BindablePropertyHandlernullNullReferenceException

Now that we've got everything set up, we can use it 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>


Another way to use attached properties and PropertyMapper custom controls is to use AttachedProperties, which is a BindableProperty when you don't need to bind it to a specific custom control.

Here's how we create 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 will use a Mapper. ImageHandler You can add a Mapper at any level since the members are static. I chose to do it inside 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
                // 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
            }
        });
    }
}

The code is almost the same as shown before, but using another API implementation, in this case the AppendToMapping method. If you don't want this behavior, use CommandMapper instead, which will only trigger when a property changes or an action occurs.

Note that when we handle with Mapper and CommandMapper , we are adding this behavior for all controls in the project that use that handler. In this case, all Image controls will trigger this code. In some cases, this is not what you want, and if you're next in a more specific way, using a PlatformBehavior would be a good fit.

So, now that we have everything set up, we can use our control in our 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>

Using Platform Behavior
PlatformBehavior is a new API created on top of .NET MAUI to make it easier to customize the task of a control when you need to access a platform specific API in a safe way (safe because it ensures Handlerand PlatformVieware not null) . It has two methods to override: OnAttachedTo and OnDetachedFrom. This API is used to replace the API in EffectXamarin.Forms and takes advantage of the multi-target architecture.

In this example, we will implement the platform-specific API using: partial class

//FileName : ImageTintColorBehavior.cs

public partial class ImageTintColorBehavior
{
    
    
    public static readonly BindableProperty TintColorProperty =
        BindableProperty.Create(nameof(TintColor), typeof(Color), typeof(TintColorBehavior), propertyChanged: OnTintColorChanged);

    public Color? TintColor
    {
    
    
        get => (Color?)GetValue(TintColorProperty);
        set => SetValue(TintColorProperty, value);
    }
}

The above code will be compiled by all platforms we target.

Now let's look at the code for the Android platform:

//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
}

Here is the code for the iOS platform:

//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
}

As you can see, we don't need to care if the Handler is null because that's handled for us by . PlatformBehavior<T, U>

We can specify the types of platform-specific APIs covered by this behavior. You do not need to specify the type of platform view (eg use ) if you want to apply the control to more than one type; you may wish to apply your control in multiple controls, in which case platformView would be on and on.PlatformBehavior<T>BehaviorPlatformBehavior<View>AndroidPlatformBehavior<UIView>iOS

And the usage is even better, you just need to call the 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: will be called when the PlatformBehavior is disconnected, i.e. when the event is fired. The API does not call the method automatically, the developer needs to handle it by himself. OnDetachedFromHandlerVirtualViewUnloadedBehaviorOnDetachedFrom

Conclusion
In this blog post, we discussed various ways of customizing controls and interacting with platform-specific APIs. There's no way right, wrong all of these are valid solutions, you just have to see which one fits your situation better. I'd say that in most cases you'll want to use PlatformBehavior as it's designed to work with multi-target approaches and ensures that resources are cleaned up when controls are no longer used. To learn more, check out the documentation on custom controls.

Guess you like

Origin blog.csdn.net/u014249305/article/details/125886769