MAUIのコントロールをカスタマイズする方法

MAUIはまだプレビュー版で、ほとんど情報がなく、MAUIのコントロールをカスタマイズする場合はどうするのかなと思い調べてみました。

# カスタマイズ方法
コントロールをカスタマイズする方法としては3つあるようです。
1. カスタムレンダラー
2. 独自ハンドラー
3. マッパーのカスタマイズ

# カスタムレンダラー
Xamarin.Formsと同じように、カスタムレンダラーでコントロールをカスタマイズできます。MAUIのプロジェクトは、各プラットフォームのコードはフォルダで分かれており、iOSなら、`Platforms/iOS`、Androidなら、`Platforms/Android`にコードを追加します。

**iOS**

```C#
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Compatibility.Platform.iOS;
using Microsoft.Maui.Controls.Platform;
using UIKit;

namespace MauiApp1.Platforms.iOS
{
    public class MyEntryRenderer : EntryRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
        {
            base.OnElementChanged(e);

            if (Control != null)
            {
                Control.BackgroundColor = UIColor.Red;
            }
        }
    }
}
```

**Android**

```C#
using Android.Content;
using Android.Graphics;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Compatibility.Platform.Android;
using Microsoft.Maui.Controls.Platform;

namespace MauiApp1.Platforms.Android
{
    internal class MyEntryRenderer : EntryRenderer
    {
        public MyEntryRenderer(Context context) : base(context)
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
        {
            base.OnElementChanged(e);

            if (Control != null)
            {
                Control.SetBackgroundColor(Color.Red);
            }
        }
    }
}
```

Xamarin.Formsで、カスタムレンダラーを使ったことがある人なら、「あれっ？」と思うかもしれませんが、上記のコードでは`ExportRenderer`属性を記述していません。MAUIでは、カスタムレンダラーの登録には、`ExportRenderer`属性を使わないようです。（記述しても登録できません。）
MAUIでは、`MauiApp`インスタンス作成時に登録を行います。`MauiApp`は`MauiAppBuilder`を使って作成しますが、その際に`ConfigureMauiHandlers`メソッドを呼び出し、そのコールバックの引数である`IMauiHandlersCollection`の`AddCompatibilityRenderer`メソッドで登録を行います。各プラットフォームに対するコードの切り替えは、条件付きコンパイルで行います。

```c#
using Microsoft.Maui;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Controls.Compatibility;
using Microsoft.Maui.Controls.Hosting;

namespace MauiApp1
{
	public static class MauiProgram
	{
		public static MauiApp CreateMauiApp()
		{
			var builder = MauiApp.CreateBuilder();
			builder
				.UseMauiApp<App>()
				.ConfigureFonts(fonts =>
				{
					fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
				})
				.ConfigureMauiHandlers(handler =>
                {
#if __IOS__
					handler.AddCompatibilityRenderer<MyEntry, Platforms.iOS.MyEntryRenderer>();
#elif __ANDROID__
					handler.AddCompatibilityRenderer<MyEntry, Platforms.Android.MyEntryRenderer>();
#endif
				});

			return builder.Build();
		}
	}
}
```

ちなみに、`MyEntry`は、`Entry`クラスを継承したクラスです。

```C#
using Microsoft.Maui.Controls;

namespace MauiApp1
{
    public class MyEntry : Entry
    {
    }
}
```

# 独自ハンドラー
Xamarin.Formsでは、ネイティブのコントロールを操作するクラスはレンダラーでしたが、MAUIでは、ハンドラーという名前のクラスで行われます。`Entry`コントロールなら、`EntryHandler`クラスになります。このハンドラーを継承した独自のハンドラーを作成することでもカスタマイズができそうです。
`EntryHandler`を継承した独自ハンドラーの例が以下になります。

**iOS**

```C#
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform.iOS;
using UIKit;

namespace MauiApp1.Platforms.iOS
{
    public class MyEntryHandler : EntryHandler
    {
        protected override MauiTextField CreateNativeView()
        {
            var nativeView = base.CreateNativeView();

            nativeView.BackgroundColor = UIColor.Red;

            return nativeView;
        }

        protected override void ConnectHandler(MauiTextField nativeView)
        {
            base.ConnectHandler(nativeView);
        }

        protected override void DisconnectHandler(MauiTextField nativeView)
        {
            base.DisconnectHandler(nativeView);
        }
    }
}
```

**Android**

```C#
using Android.Graphics;
using AndroidX.AppCompat.Widget;
using Microsoft.Maui.Handlers;

namespace MauiApp1.Platforms.Android
{
    public class MyEntryHandler : EntryHandler
    {
        protected override AppCompatEditText CreateNativeView()
        {
            var nativeView = base.CreateNativeView();
            
            nativeView.SetBackgroundColor(Color.Red);
            
            return nativeView;
        }

        protected override void ConnectHandler(AppCompatEditText nativeView)
        {
            base.ConnectHandler(nativeView);
        }

        protected override void DisconnectHandler(AppCompatEditText nativeView)
        {
            base.DisconnectHandler(nativeView);
        }
    }
}
```
この例では、`CreateNativeView`をオーバーライドして、そこでプロパティの設定を行っています。あと、カスタマイズできそうな所としては、`ConnectHandler`がありますが、ここは名前が表すようにイベントの登録を行うためのメソッドのようなので、イベントを登録するならここに記述します。ちなみに、イベントの登録の解除は、`DisconnectHandler`で行うことになります。

ハンドラーの登録は、カスタムレンダラーと同じように`ConfigureMauiHandlers`を使います。ハンドラーでは、`AddHandler`を呼び出して登録を行います。

```C#
using Microsoft.Maui;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Controls.Hosting;

namespace MauiApp1
{
	public static class MauiProgram
	{
		public static MauiApp CreateMauiApp()
		{
			var builder = MauiApp.CreateBuilder();
			builder
				.UseMauiApp<App>()
				.ConfigureFonts(fonts =>
				{
					fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
				})
				.ConfigureMauiHandlers(handler =>
                {
#if __IOS__
					handler.AddHandler<MyEntry, Platforms.iOS.MyEntryHandler>();
#elif __ANDROID__
					handler.AddHandler<MyEntry, Platforms.Android.MyEntryHandler>();
#endif
				});

			return builder.Build();
		}
	}
}
```

# マッパーのカスタマイズ
ハンドラーは、コントロールのプロパティ値変更に対する、実際のネイティブのコントロールで行う処理を、マッパーというクラス（`IPropertyMapper`を実装したクラス）で保持しています。このマッパーには独自の処理を追加することが可能となっています。追加するには、マッパーの`AppendToMapping`を呼び出します。
以下が、`App`クラスで追加を行っている例です。

```C#
using Microsoft.Maui;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
using Application = Microsoft.Maui.Controls.Application;

namespace MauiApp1
{
	public partial class App : Application
	{
		public App()
		{
            InitializeComponent();

            EntryHandler.EntryMapper.AppendToMapping(nameof(IView.Background), (handler, view) =>
            {
                if (view is MyEntry)
                {
#if __IOS__
                    handler.NativeView.BackgroundColor = Colors.Red.ToNative();
#elif __ANDROID__
                    handler.NativeView.SetBackgroundColor(Colors.Red.ToNative());
#endif
                }
            });

            MainPage = new MainPage();
		}
	}
}
```

ハンドラーからは、ネイティブのコントロール（`NativeView`）にアクセスできるので、そこからカスタマイズすることができます。
`AppendToMapping`の第一引数は、マッピングのキーで、`Background`を設定しています。このキーの名前の付け方には少し注意が必要です。もし、コントロールのプロパティに存在しない名前を付ければ、コントロール初期化時に追加した処理が呼び出されだけです。しかし、プロパティに存在する名前の場合、このキーで既に処理が登録されているので、既存の処理との兼ね合いを考えなければいけません。
今回キーに設定した`Background`は、プロパティに存在する名前なので、既に処理が登録されています。この既存の処理が追加によってどうなるかというと、`AppendToMapping`で追加した場合は、既存処理の後に追加した処理が実行されるようになります。なので、この場合、xamalなどで`Background`プロパティを設定しても、追加した処理で背景色を書き換えてしまうといった動作になります。また、後からコントロールのプロパティの値を変更した場合でも、この追加処理が実行されます。
ちなみに、他にも追加する方法があり、`PrependToMapping`を使えば、既存処理の前に追加処理を行う動作になります。また、`ModifyMapping`を使えば、既存処理を引数で受けることができます。

# おわりに
今回、MAUIのコントロールのカスタマイズ方法を調べたことで、MAUIの構成を少し理解することができました。MAUIが今後どうなっていくのかはまだわかりませんが、動向は追っていきたいと思います。