Customize controls with handlers with .NET MAUI

Photo by Omid Armin on Unsplash

Customize controls with handlers with .NET MAUI

Creating an Image Entry

It is very common during the development of mobile applications to create or customize controls. In this post we will see how we can do this with handlers in .NET MAUI.

Ways to create and customize .NET MAUI Controls

There is more than one way to create and customize controls with .NET MAUI. The following list shows five different ways to acomplish it.

  1. Using Custom Renderers (Xamarin.Forms Architecture).
  2. Using Custom Handlers.
  3. Using ContentView.
  4. Using TemplatedView (Templated Controls).
  5. Using GraphicsView (Drawn controls)

Handlers

A key concept of .NET MAUI handlers is mappers. Each handler typically provides a property mapper, and sometimes a command mapper, that maps the cross-platform control's API to the native view's API. For example, on iOS a handler maps a .NET MAUI Button to an iOS UIButton. On Android, the Button is mapped to an AppCompatButton:

button-handler.png

Handlers can be customized to augment the appearance and behavior of a cross-platform control beyond the customization that's possible through the control's API.

Handlers can be accessed through a control-specific interface provided derived from IView interface, it means that we can use handlers in any control that implements the interface IView.

Creating an ImageEntry control

The .NET MAUI Entry is a single-line text input control that implements the IEntry interface. On iOS, the EntryHandler maps the Entry to an iOS UITextField. On Android, the Entry is mapped to an AppCompatEditText, and on Windows the Entry is mapped to a TextBox.

Depending on the control that we are creating, we can use the interface IView to be more generic or something more specific such as Entry or Button classes. The same idea applies for the handlers, we can create a custom handler that inherits from ViewHandler or just use a specific handler like EntryHandler.

The ImageEntry control that we will create is an Entry that has some properties that a common Entry does not have such as Image. The idea is to allow the developers to define an image to be displayed inside of the control and some other properties.

namespace Article_002.Controls
{
    public class ImageEntry : Entry
    {
        public ImageEntry()
        {
            this.HeightRequest = 50;
        }

        public static readonly BindableProperty ImageProperty =
            BindableProperty.Create(nameof(Image), typeof(string), typeof(ImageEntry), string.Empty);

        public static readonly BindableProperty ImageHeightProperty =
             BindableProperty.Create(nameof(ImageHeight), typeof(int), typeof(ImageEntry), 40);

        public static readonly BindableProperty ImageWidthProperty =
            BindableProperty.Create(nameof(ImageWidth), typeof(int), typeof(ImageEntry), 40);

        public static readonly BindableProperty ImageAlignmentProperty =
            BindableProperty.Create(nameof(ImageAlignment), typeof(ImageAlignment), typeof(ImageEntry), ImageAlignment.Left);


        public int ImageWidth
        {
            get { return (int)GetValue(ImageWidthProperty); }
            set { SetValue(ImageWidthProperty, value); }
        }

        public int ImageHeight
        {
            get { return (int)GetValue(ImageHeightProperty); }
            set { SetValue(ImageHeightProperty, value); }
        }

        public string Image
        {
            get { return (string)GetValue(ImageProperty); }
            set { SetValue(ImageProperty, value); }
        }

        public ImageAlignment ImageAlignment
        {
            get { return (ImageAlignment)GetValue(ImageAlignmentProperty); }
            set { SetValue(ImageAlignmentProperty, value); }
        }
    }

    public enum ImageAlignment
    {
        Left,
        Right
    }
}

Creating the Custom Handlers

The next step is to create the handlers, for this post only the handlers for Android and iOS will be created. Different from Xamarin.Forms that was necessary to use the ExportRenderer attribute for registering the Renderer, in .NET MAUI the handler registration is slightly different, we need to use AppHostBuilder and the AddHandler method to register the Handler.

The following codes are the implementation of the handler for Android and iOS.

using Android.Content;
using Android.Content.Res;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Widget;
using AndroidX.AppCompat.Widget;
using AndroidX.Core.Content;
using AndroidX.Core.Graphics;
using Article_002.Controls;
using Microsoft.Maui.Controls.Compatibility.Platform.Android;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;

namespace Article_002.Platforms.Android.Renderes
{
    public class ImageEntryRenderer : EntryHandler
    {
        private ImageEntry element;

        protected override AppCompatEditText CreatePlatformView()
        {
            var editText = new AppCompatEditText(Context);
            element = VirtualView as ImageEntry;

            if (!string.IsNullOrEmpty(element.Image))
            {
                switch (element.ImageAlignment)
                {
                    case ImageAlignment.Left:
                        editText.SetCompoundDrawablesWithIntrinsicBounds(GetDrawable(element.Image), null, null, null);
                        break;
                    case ImageAlignment.Right:
                        editText.SetCompoundDrawablesWithIntrinsicBounds(null, null, GetDrawable(element.Image), null);
                        break;
                }
            }

            editText.CompoundDrawablePadding = 25;
            editText.Background.SetColorFilter(Colors.White.ToAndroid(), PorterDuff.Mode.SrcAtop);

            return editText;
        }

        private BitmapDrawable GetDrawable(string imageEntryImage)
        {
            int resID = Context.Resources.GetIdentifier(imageEntryImage, "drawable", this.Context.PackageName);
            var drawable = ContextCompat.GetDrawable(Context, resID);
            var bitmap = drawableToBitmap(drawable);

            return new BitmapDrawable(Bitmap.CreateScaledBitmap(bitmap, element.ImageWidth * 2, element.ImageHeight * 2, true));
        }

        public Bitmap drawableToBitmap(Drawable drawable)
        {
            if (drawable is BitmapDrawable) {
                return ((BitmapDrawable)drawable).Bitmap;
            }

            int width = drawable.IntrinsicWidth;
            width = width > 0 ? width : 1;
            int height = drawable.IntrinsicHeight;
            height = height > 0 ? height : 1;

            Bitmap bitmap = Bitmap.CreateBitmap(width, height, Bitmap.Config.Argb8888);
            Canvas canvas = new Canvas(bitmap);
            drawable.SetBounds(0, 0, canvas.Width, canvas.Height);
            drawable.Draw(canvas);

            return bitmap;
        }
    }
}
using Article_002.Controls;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using System.Drawing;
using UIKit;

namespace Article_002.Platforms.iOS.Renderes
{
    public class ImageEntryRenderer : EntryHandler
    {
        private ImageEntry element;

        protected override MauiTextField CreatePlatformView()
        {
            var textField = new MauiTextField();
            element = VirtualView as ImageEntry;

            if (!string.IsNullOrEmpty(element.Image))
            {
                switch (element.ImageAlignment)
                {
                    case ImageAlignment.Left:
                        textField.LeftViewMode = UITextFieldViewMode.Always;
                        textField.LeftView = GetImageView(element.Image, element.ImageHeight, element.ImageWidth);
                        break;
                    case ImageAlignment.Right:
                        textField.RightViewMode = UITextFieldViewMode.Always;
                        textField.RightView = GetImageView(element.Image, element.ImageHeight, element.ImageWidth);
                        break;
                }
            }

            textField.BorderStyle = UITextBorderStyle.Line;
            textField.Layer.MasksToBounds = true;

            return textField;
        }

        private UIView GetImageView(string imagePath, int height, int width)
        {
            var uiImageView = new UIImageView(UIImage.FromFile(imagePath))
            {
                Frame = new RectangleF(0, 0, width, height)
            };
            UIView objLeftView = new UIView(new System.Drawing.Rectangle(0, 0, width + 10, height));
            objLeftView.AddSubview(uiImageView);

            return objLeftView;
        }
    }
}

In .NET MAUI to have access to the native implementation of the control we need to override the method CreatePlatformView, so this way we can instantiate the native control and change the properties that we need/want.

In summary, we are defining the image alignment and loading the image by name using the property that was created.

Something that is also different in .NET MAUI is the way that we work with images. It is not necessary to have the images in the platform projects, everything is located in the Resources folder.

Registering the Handlers

Now it is time to register the handlers. We can use the method ConfigureMauiHandlers from the class MauiAppBuilder to add the handlers.

using Article_002.Controls;

namespace Article_002;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            })
                    .ConfigureMauiHandlers((handlers) => {
#if ANDROID
                        handlers.AddHandler(typeof(ImageEntry), typeof(Platforms.Android.Renderes.ImageEntryRenderer));

#elif IOS                    
                        handlers.AddHandler(typeof(ImageEntry), typeof(Platforms.iOS.Renderes.ImageEntryRenderer));
#endif
                    });

        return builder.Build();
    }
}

With the compile directives will help to apply the correct handler to the specific platform.

Using the ImageEntry

With everything in place we can use the control in our application, for this we just need to add the namespace in the XAML of the page so the control can be recognized.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:Article_002.Controls"
             BackgroundColor="{StaticResource Primary}"
             x:Class="Article_002.MainPage">

    <ScrollView>
        <VerticalStackLayout Spacing="10">


            <local:ImageEntry TextColor="{StaticResource Black}" 
                        PlaceholderColor="{StaticResource White}" 
                        Image="user" 
                        Placeholder="Email" 
                        HorizontalOptions="FillAndExpand"/>

            <local:ImageEntry TextColor="{StaticResource Black}" 
                    PlaceholderColor="{StaticResource White}"  
                    Image="password" 
                    Placeholder="Password" 
                    HorizontalOptions="FillAndExpand"/>

            <Button HeightRequest="50" 
                         TextColor="{StaticResource White}" 
                         Text="Login"  
                         BackgroundColor="{StaticResource primary}"
                         HorizontalOptions="FillAndExpand"/>

            <Label  Text="Forgot password" 
                        HorizontalOptions="Center" 
                        TextColor="{StaticResource White}"/>

            <Label Margin="0,0,0,20" Text="I don't have an account" VerticalOptions="EndAndExpand"
                        HorizontalOptions="Center" 
                        TextColor="{StaticResource White}">
            </Label>
        </VerticalStackLayout>
    </ScrollView>

</ContentPage>

We should see the results like this for Android and iOS.

Screen Shot 2022-11-04 at 8.39.49 PM.png

Wrapping Up

This is how we can create or customize a control in .NET MAUI using custom handlers. You can find the full code on my GitHub.