Note: This is a Guest Blog Post by Microsoft MVP, Pedro Jesus. Pedro works as a Software Engineer at ArcTouch 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
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(TintColorBehavior), 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(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 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 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 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.
The post Customizing Controls in .NET MAUI appeared first on .NET Blog.
source https://devblogs.microsoft.com/dotnet/customizing-dotnet-maui-controls/
Comments
Post a Comment