Customizing Controls in .NET MAUI

Pedro Jesus

Note: This is a Guest Blog Post by Microsoft MVP, Pedro Jesus. Pedro works as a Software Engineer at Nareia and is a core maintainer of the .NET MAUI Community Toolkit

Today, I want to talk about and show you the ways that you can completely customize controls in .NET MAUI. Before looking at .NET MAUI let’s move back a couple years, back to the Xamarin.Forms era. Back then, we had a couple of ways to customize controls: We had Behaviors that are used when you don’t need to access the platform-specific APIs in order to customize controls; and we had Effects if you need to access the platform-specific APIs.

Let’s focus a little bit on the Effects API. It was created due to Xamarin’s lack of multi-target architecture. That means we can’t access platform-specific code at the shared level (in the .NET Standard csproj). It worked pretty well and can save you from creating Custom Renderers.

Today, in .NET MAUI, we can leverage the power of the multi-target architecture and access the platform-specific APIs in our shared project. So do we still need Effects? No, because we have access to all code and APIs from all platforms that we target.

So let’s talk about all the possibilities to customize a control in .NET MAUI and some dragons that you may found in the way. For this, we’ll be customizing the Image control adding the ability to tint the image presented.

Note: .NET MAUI still supports Effects if you want to use it, however it is not recommended

Source code reference comes from .NET MAUI Community Toolkit’s IconTintColor.

Customizing an Existing Control

To add additional features to an existing control, we extend it and add the features that we need.

Let’s create a new control, class ImageTintColor : Image and add a new BindableProperty that we will leverage to change the tint color of the 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)
    {
        // ...
    }
}

Folks familiar with Xamarin.Forms will recognize this; it’s pretty much the same code that you will write in a Xamarin.Forms application.

The .NET MAUI platform-specific API work will happen on the OnTintColorChanged delegate. Let’s take a look at it.

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

Because .NET MAUI uses multi-targeting, we can access the platform specifics and customize the control the way that we want. The ImageExtensions.ApplyColor and ImageExtensions.ClearColor methods are helper methods that will add or remove the tint from the image.

One thing that you maybe noticed is the null check for Handler and PlatformView. This is the first dragon that you may find on your way. When the Image control is created and instantiated and the PropertyChanged delegate of the BindableProperty is called, the Handler can be null. So, without that null check, the code will throw a NullReferenceException. This may sound like a bug, but it’s actually a feature! This allows the .NET MAUI engineering team to keep the same lifecycle that controls have on Xamarin.Forms, avoiding some breaking changes for applications that will migrate from Forms to .NET MAUI.

Now that we have everything set up, we can use our control in our ContentPage. In the 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 Property and PropertyMapper

Another way to customize a control is using AttachedProperties, it’s a flavor of BindableProperty when you don’t need to have it tied to a specific custom control.

Here’s how we can 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 the boilerplate that we have on Xamarin.Forms for the AttachedProperty, but as you can see we don’t have the PropertyChanged delegate. In order to handle the property change, we will use the Mapper in the ImageHandler. You add the Mapper at any level, since the members are static. I choose to do it inside the TintColorMapper class, as you can see 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 pretty much the same as showed before, just implemented using another API, in this case the AppendToMapping method. If you don’t want this behavior, use the CommandMapper instead, it will be triggered just when a property changed or an action happens.

Be aware that when we handle with Mapper and CommandMapper, we’re adding this behavior for all controls that use that handler in the project. In this case all Image controls will trigger this code. In some cases this isn’t what you want, if you something more specific the next way, using PlatformBehavior will fit perfectly.

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

PlatformBehavior is a new API created on .NET MAUI to make easier the task to customize controls when you need to access the platform-specifics APIs in safe way (safe because it ensures that the Handler and PlatformView aren’t null). It has two methods to override: OnAttachedTo and OnDetachedFrom. This API exists to replace the Effect API from Xamarin.Forms and to take advantage of the multi-target architecture.

In this example, we will use partial class to implement the platform-specific APIs:

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

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

Now let’s see 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
}

And here’s 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 about if the Handler is null, because that’s handled for us by PlatformBehavior<T, U>.

We can specify the type of platform-specific API that this Behavior covers. If you want to apply the control for more than one type, you don’t need to specify the type of the platform view (e.g. use PlatformBehavior<T>); you probably want to apply your Behavior in more than one control, in that case the platformView will be an PlatformBehavior<View> on Android and an PlatformBehavior<UIView> on 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: The PlatformBehavior will call the OnDetachedFrom when the Handler disconnect from the VirtualView, in other words, when the Unloaded event is fired. The Behavior API doesn’t call the OnDetachedFrom method automatically, you as a developer needs to handle it by yourself.

Conclusion

In this blog post we discussed various ways to customize your controls and interact with the platform-specific APIs. There’s no right or wrong way, all those are valid solutions, you just need to see which will suit better to your case. I would say that for most cases you want to use the PlatformBehavior since it’s designed to work with the multi-target approach and makes sure to clean-up the resources when the control is not used anymore. To learn more, check out the documentation on custom controls.

23 comments

Discussion is closed. Login to edit/delete existing comments.

  • Jan Seriš 0

    When will issues like IsVisible not working, changing size of content during runtime (after initialization) not working (the content is cut when growing) etc.?
    With these issues, we cannot make any useful MAUI app on Android.

    • Christoph Fink 0

      I’m having the same problems on Android 🙁

      • Christoph Fink 0

        …updating VS to the newest Preview solved it for me 😎.
        But I need to agree with Marcin: MAUI is great, but not yet “production ready” IMO…
        I think/hope with .NET 7 it will be “ready”.

    • hamid 0

      My upvote to this.

    • Marcin J 0

      100% agree and the same applies to iOS.

      So many basic bugs, for rexample CollectionView was not showing label text (from VM binding) on iOS (displayed OK on Android) but the same code worked on ListView.
      I have a SearchBar and on iOS is displayed in dark mode which I don’t have enabled (all other controls are in light mode).

      At this stage I need to give up and write the app in Xamarin Forms and wait for MAUI to be production ready (I can’t believe that MAUI is generally available and not still in preview).

      • Isaac Rabin 0

        When MAUI will be available for VS 2022 GA release? I’ve to maintain multiple versions of VS already and don’t want to maintain another preview version.

  • Nicholas Halka 0

    The .Net environment is getting so good.. Can’t wait for Full Maui to develop. As someone who has always stayed current with checking out preview builds I would say one of the major disadvantages with Maui In its current state is the inability to publish a Blazor Desktop app in a portable configuration. Supposedly some people have been able to get it to work but its not easy and very easily breaks. Don’t underestimate the power of those who open source a lot or fiddle around for fun. Those people don’t want to deal with the windows store installation process. Those people are also the ones most likely to influence newer development for whichever small to medium size company they work for.

  • Ashish Khanal 0

    Awesome!

  • Sorin Iordache 0

    What is TintColorBehavior? I can’t run the code samples because of it.

    • Jaden Rogers 0

      I looked at the comments to see if someone had pointed that out also. It should be the same type as the parent class

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

      Should be

       public static readonly BindableProperty TintColorProperty =
              BindableProperty.Create(nameof(TintColor), typeof(Color), typeof(ImageTintColor), propertyChanged: OnTintColorChanged);

      As “ImageTintColor” is the declaring class not “TintColorBehavior”.

      • James MontemagnoMicrosoft employee 0

        I have updated the code, thanks!

        • Jaden Rogers 0

          Whoa its you! I Listen to your podcast all the time, hope Frank fishes out his drone 🙂 Keep up the good work.

  • Eder Cardoso 0

    Can you show us how to implement a custom handler for this using separate files for iOS and Android platform specific code.
    Also using a PropertyMapper for the new bindable property that you have created and registering that custom handler on MauiProgram.cs

  • John 0

    Hi Pedro,
    Thank you for the article, but I have a few questions regarding your PlatformBehavior example.

    First, you refer to ImageTintColorBehavior and then later IconTintColorBehavior, its clear you renamed the classes part way through writing the article, can you update so that others can understand it better?

    Second, View.Behaviors does not support adding PlatformBehavior decendants, it expects Behavior instances. So the following fails to compile:

    view.Behaviors.Add(new ViewLongPressedBehavior());

    Can you explain how are we supposed to add platform behaviors to views via code?

    Thanks
    John

    • Pedro Jesus 0

      Hey John,

      I’m sorry about that, I updated the name on all snippets, hope that makes more sense now.

      About the second question, I just tried here, and I was able to use this snippet to add a PlatformBehavior using C#.
      Can you try again, with the latest .NET MAUI version?

  • M T 0

    for me below code does not compile as i cannot find correct assemblies that contain methods 🙁 and VS does not help either…

    ImageExtensions.ApplyColor((Android.Widget.ImageView)control.Handler.PlatformView, tintColor);
    
    ImageExtensions.ClearColor((Android.Widget.ImageView)control.Handler.PlatformView);

    But i followed linked github sources and replaces it with below respectively

    ((ImageView)control.Handler.PlatformView).SetColorFilter(
                    new PorterDuffColorFilter(
                        tintColor.ToPlatform(),
                        PorterDuff.Mode.SrcIn ?? throw new NullReferenceException()));
    
    ((ImageView)control.Handler.PlatformView).ClearColorFilter();
    • Pedro Jesus 0

      Hey M T, so the code here’s just a snippet. It doesn’t show all the implementation, the focus of this blog post is to give you an overview of how you can customize your controls in .NET MAUI.

      After changing the code, it does compile for you?

  • 然 张 0

    Can i use inkcanvas(wpf) on maiu

  • Wheel Brain 1

    That means we can’t access platform-specific code at the shared level
    But this is a feature, not a bug!

    I prefer when platform specific codes are in different projects.
    We have the Shared code, then the .Android, then the .iOS, etc.
    Instead of putting all together using compiler #if/#elif directives. Horrible.
    Located in different projects = clearer code, easier for multi person work (somebody works on Android, other iOS, etc)

  • Sojan Chandy Therakom 0

    Can we import third party c# dll in maui

Feedback usabilla icon