Customizing Controls in .NET MAUI
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 theOnDetachedFrom
when theHandler
disconnect from theVirtualView
, in other words, when theUnloaded
event is fired. TheBehavior
API doesn’t call theOnDetachedFrom
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
Can we import third party c# dll in maui
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)
Can i use inkcanvas(wpf) on maiu
for me below code does not compile as i cannot find correct assemblies that contain methods 🙁 and VS does not help either…
But i followed linked github sources and replaces it with below respectively
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?
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:
Can you explain how are we supposed to add platform behaviors to views via code?
Thanks
John
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?
Hi Pedro,
Using the updated example code I was able to make this work, but I had to update my .csproj to support filename based multi targeting, using info from here:
https://docs.microsoft.com/en-us/dotnet/maui/platform-integration/configure-multi-targeting
So yes I now have a working ViewLongPressedBehavior, thanks Pedro!
Regards
John
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
Hello Eder, thanks for your comment.
While a go I did a presentation about that on the Monkey Fest 2022 you can see the talk here
https://youtu.be/pnkHTyRYiTY?list=PLQPLL9Pbjp33KmpV3JpiPOdVre66O54Ty
Where I cover a lot of scenarios on how to customize a control.
What is TintColorBehavior? I can’t run the code samples because of it.
I looked at the comments to see if someone had pointed that out also. It should be the same type as the parent class
Should be
As “ImageTintColor” is the declaring class not “TintColorBehavior”.
I have updated the code, thanks!
Whoa its you! I Listen to your podcast all the time, hope Frank fishes out his drone 🙂 Keep up the good work.
Awesome!
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.
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.
This should be all fixed up, please ensure Visual Studio and the .NET MAUI workloads are up to date: https://github.com/dotnet/maui/releases
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.
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).
My upvote to this.
I’m having the same problems on Android 🙁
…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”.