はじめに
皆さん、こんにちは。
私は現在、MAUIを用いたアプリの開発・保守を行っており、そこで利用していたHandlerについて最初はよく分からず苦労していました。
そこで、自分の理解を深めるためにこの記事を書こうと思います。
MAUIはXamarinの後継として開発されており、Xamarin.FormsのRendererと比較しながら、どのように移行・実装していく必要があるのかを見ていきたいと思います。
Handlerとは
HandlerはMAUIで提供されている機能で、クロスプラットフォーム開発においてプラットフォームごとに特化した動作を実装するために用いられます。
クロスプラットフォーム開発では、共通化されたControl(Virtual View)が存在し、それが各プラットフォームのNative Viewにマッピングされて実装されています。
例えば、Buttonを考えてみましょう。ButtonはiOSではUIButton、AndroidではAppCompatButtonとして実装されています。
これらの実装は以下の箇所で確認できます。
ButtonHandler.Android.cs
ButtonHandler.iOS.cs
これらの宣言部分を見てみましょう。
ButtonHandler.Android
public partial class ButtonHandler : ViewHandler<IButton, MaterialButton>
ButtonHandler.IOS
public partial class ButtonHandler : ViewHandler<IButton, UIButton>
ViewHanler <VirtualView, PlatformView(Native View)>
の形式で宣言されています。
AndroidではNative ViewがMaterialButton、iOSではUIButtonとなっています。
つまり、Virtual View(共通部分)とPlatform View(プラットフォーム部分)の動作を紐づける役割を果たしています。
Handlerに必要な実装とは?
Handlerの実装にはいくつかの要素がありますが、ここでは特に重要なものをいくつか紹介します。
CreatePlatformView
Viewhandlerを継承する場合、最低限オーバーライドしなければならないのがCreatePlatformView()
です。
先述の通り、Virtual ViewとPlatform Viewを紐づけて動作させるため、どのPlatform Viewを返すのかを定義する必要があります。
以下にAndroidとiOSのHandlerの例を示します。
ButtonHandler.Android
protected override MaterialButton CreatePlatformView()
{
MaterialButton platformButton = new MauiMaterialButton(Context)
{
IconGravity = MaterialButton.IconGravityTextStart,
IconTintMode = Android.Graphics.PorterDuff.Mode.Add,
IconTint = TransparentColorStateList,
SoundEffectsEnabled = false
};
return platformButton;
}
ButtonHandler.IOS
protected override UIButton CreatePlatformView()
{
var button = new UIButton(UIButtonType.System);
SetControlPropertiesFromProxy(button);
return button;
}
Platform Viewはプラットフォーム依存のものを作成して返す必要があります。
ここでPlatform Viewをカスタマイズしたい場合は、このメソッド内で行うと良いでしょう。
例えば、Buttonの表示を細かく調整したい場合などに有効です。
ConnectHandler
ConnectHandlerはRendererでいうところの OnElementChanged に相当します。
名前の通り、実際のPlatform Viewが表示されるタイミングで初期化処理を行います。
ここで必要な処理を記述していきます。
ButtonHandler.Android
protected override void ConnectHandler(MaterialButton platformView)
{
ClickListener.Handler = this;
platformView.SetOnClickListener(ClickListener);
TouchListener.Handler = this;
platformView.SetOnTouchListener(TouchListener);
platformView.FocusChange += OnNativeViewFocusChange;
platformView.LayoutChange += OnPlatformViewLayoutChange;
base.ConnectHandler(platformView);
}
ButtonHandler.IOS
readonly ButtonEventProxy _proxy = new ButtonEventProxy();
protected override void ConnectHandler(UIButton platformView)
{
_proxy.Connect(VirtualView, platformView);
base.ConnectHandler(platformView);
}
Androidではリスナーの設定を行っており、iOSでも同様にイベントとの紐付けを行っています。
ConnectHandlerではこのようなイベントの設定を行うことが一般的です。
DisConnectHandler
DisconnectHandlerはRendererでいうところの Dispose に近い処理です。
画面から表示されなくなったタイミングで呼び出され、終了時に必要な処理を行います。
ButtonHandler.Android
protected override void DisconnectHandler(MaterialButton platformView)
{
ClickListener.Handler = null;
platformView.SetOnClickListener(null);
TouchListener.Handler = null;
platformView.SetOnTouchListener(null);
platformView.FocusChange -= OnNativeViewFocusChange;
platformView.LayoutChange -= OnPlatformViewLayoutChange;
ImageSourceLoader.Reset();
base.DisconnectHandler(platformView);
}
ButtonHandler.IOS
protected override void DisconnectHandler(UIButton platformView)
{
_proxy.Disconnect(platformView);
base.DisconnectHandler(platformView);
}
これらの実装では、ConnectHandlerで設定した内容を解除しています。
適切にリソースを解放するために、忘れずに実装しましょう。
Mapper
Mapperは関数ではありませんが、非常に重要な役割を担っています。
先述の通り、Virtual ViewとPlatform Viewをマッピングしていますが、細かな動作はプラットフォーム側で定義する必要があります。これは、Rendererで存在していた OnElementPropertyChanged をより使いやすくしたものと考えると分かりやすいでしょう。特定のプロパティの値が変更された際に対応する動作を一つ一つマッピングしていきます。
Handlerの共通部分の実装例を見てみましょう。
public static IPropertyMapper<IButton, IButtonHandler> Mapper = new PropertyMapper<IButton, IButtonHandler>(TextButtonMapper, ImageButtonMapper, ViewHandler.ViewMapper)
{
[nameof(IButton.Background)] = MapBackground,
[nameof(IButton.Padding)] = MapPadding,
[nameof(IButtonStroke.StrokeThickness)] = MapStrokeThickness,
[nameof(IButtonStroke.StrokeColor)] = MapStrokeColor,
[nameof(IButtonStroke.CornerRadius)] = MapCornerRadius
};
IButton.Background が変更された場合に MapBackground が呼び出されるようになっています。また、new PropertyMapper<IButton, IButtonHandler>(TextButtonMapper, ImageButtonMapper, ViewHandler.ViewMapper)
の部分では、基本のViewで必要な処理をマッピングしています。ViewHandler.ViewMapper がそれに当たります。
これを省略すると、通常のViewで動作していた部分が動作しなくなる可能性があるため、独自実装する場合でも必ず含めるようにしましょう。
具体的なプラットフォームごとの実装例を見てみましょう。
ButtonHandler.Android
public static void MapBackground(IButtonHandler handler, IButton button)
{
handler.PlatformView?.UpdateBackground(button);
}
public static void MapStrokeColor(IButtonHandler handler, IButton button)
{
handler.PlatformView?.UpdateStrokeColor(button);
}
public static void MapStrokeThickness(IButtonHandler handler, IButton button)
{
handler.PlatformView?.UpdateStrokeThickness(button);
}
public static void MapCornerRadius(IButtonHandler handler, IButton button)
{
handler.PlatformView?.UpdateCornerRadius(button);
}
public static void MapText(IButtonHandler handler, IText button)
{
handler.PlatformView?.UpdateTextPlainText(button);
}
public static void MapTextColor(IButtonHandler handler, ITextStyle button)
{
handler.PlatformView?.UpdateTextColor(button);
}
ButtonHandler.IOS
public static void MapStrokeColor(IButtonHandler handler, IButtonStroke buttonStroke)
{
handler.PlatformView?.UpdateStrokeColor(buttonStroke);
}
public static void MapStrokeThickness(IButtonHandler handler, IButtonStroke buttonStroke)
{
handler.PlatformView?.UpdateStrokeThickness(buttonStroke);
}
public static void MapCornerRadius(IButtonHandler handler, IButtonStroke buttonStroke)
{
handler.PlatformView?.UpdateCornerRadius(buttonStroke);
}
public static void MapText(IButtonHandler handler, IText button)
{
handler.PlatformView?.UpdateText(button);
// Any text update requires that we update any attributed string formatting
MapFormatting(handler, button);
}
これらの実装では、主にPlatform Viewの必要な動作を呼び出しています。
独自にカスタマイズしたい場合も、ここで処理を追加することで OnElementPropertyChanged と同様の挙動を実現できます。
カスタムHandlerを作成する際は、これらのポイントを押さえておくと良いでしょう。
自分でCustomHandlerを作りたい場合はどうするの?
先ほど説明した要素を意識すれば、基本的にはCustom Handlerを作成することが可能です。
公式ドキュメントにも詳細が記載されていますので、参考にすると理解が深まるでしょう。
https://learn.microsoft.com/ja-jp/dotnet/maui/user-interface/handlers/customize?view=net-maui-9.0
ここでは、一旦もともとのHandlerをカスタムするのではなく、一から作成するものとして説明をしていきたいと思います。
やることとしては、まずCustomViewを作成し、其のCustomViewに対するHandlerを作成し、そちらの動作を制御するイメージです。
では、VirtualViewとNativeViewを用意しましょう。
VirtualViewも今回は簡単なものとしては、Viewを継承したものとします。
一旦は特別なプロパティ等も設定せず、最小限のものとして作成します。
CustomView.cs
public class CustomView : View
{
}
これでCusutomView(VirtualView)の作成は完了しました。
では次にhandlerを用意します。
HandlerはVirtualViewに対応するPlatformViewを割り当ててあげる必要があります。
Viewの場合、AndroidではView、iOSではUIViewとなります。
CustomViewHandler.Android.cs
public class CustomViewHandler : ViewHandler<CustomView, View>
{
public CustomViewHandler() : base(ViewHandler.ViewMapper)
{
}
protected override View CreateNativeView()
{
return new View(Context);
}
}
CustomViewHandler.iOS.cs
public class CustomViewHandler : ViewHandler<CustomView, UIView>
{
public CustomViewHandler() : base(ViewHandler.ViewMapper)
{
}
protected override UIView CreateNativeView()
{
return new UIView();
}
}
このようになります。
コンストラクトでViewHandler.ViewMapperを渡していますが、これは基本のViewの動作をマッピングしているものです。
これを忘れていると、基本のViewの動作が行われなくなるので注意しましょう。
あくまで今回はサンプルのため、このようなシンプルな実装になっていますが、ここで色々とカスタムを行っていくことで、自分だけのHandlerを作成することが可能です。
最後に
今回はMAUIのHandlerについてまとめました。
MAUIはまだ発展途上の部分もあり、情報が少なく苦労することもあるかと思います。
この記事が皆さんの助けになれば幸いです。