個人的なメモ

Tomohiro Suzuki @hiro128_777 のブログです。Microsoft MVP for Developer Technologies 2017- 本ブログと所属組織の公式見解は関係ございません。

.NET MAUI、 Xamarn.Forms からの改良ポイント

.NET MAUI リリース

2022年5月23日(現地時間)ついに .NET MAUI が GAされました。Microsoft は「.NET MAUI は、.NET Multi-platform App UI の略称で、モバイル、タブレット、デスクトップにまたがるネイティブデバイスのアプリケーションを構築するためのフレームワークです。」と紹介しています。
 
2022年6月26日現在では、残念なことに .NET MAUI 自体は GA されたのですが、開発環境である Visual Studio は Windows 版も Mac 版も最新のプレビュー版での対応となっています。こちらは、ほどなく安定版の Visual Studio でも対応されるでしょう。
 
.NET MAUI は Xamarin.Forms から全く違う製品名に変わりましたが、何が違うのでしょうか。わかりやすく言えば、.NET MAUI は Xamarin.Forms の改良版です。では、具体的に .NET MAUI は Xamarin.Forms と比べどこが改良されたのか詳しく見ていきましょう。
 
 

Xamarin.Forms と .NET MAUIの主な違い

まず、Xamarin.Forms と .NET MAUI の主な違いをまとめてみました。色々違いがありますが、.NET 6 対応になりさまざまな点で改良されています。

Xamarin.Forms .NET MAUI
プロジェクトの構造 非 SDK スタイル(Franken-proj)
プラットフォームごとに個別のプロジェクトを使用
SDK スタイル
単一のプロジェクトで、複数プラットフォームをターゲットにできる
ツールチェーン .NETFramework .NET CLI
リソース管理 プラットフォームごとに個別管理
プラットフォーム固有のデバイスの解像度に応じたイメージを準備する必要がある
単一のプロジェクト内で一元管理
SVG を準備すれば各プラットフォームの解像度のPNGに変換でされる
スタートアップ 独自(App クラス) Generic Host 対応
ホットリロード 完全な XAML ホットリロード
(SDK5.x および Visual Studio 2019 16.9以降)
完全な .NET ホットリロード
完全な XAML ホットリロード
UI コントロールアーキテクチャ レンダラー ハンドラー

 
次に、.NET MAUI の改良点の中で特に注目したいポイントをご紹介します。
 
 

.NET MAUI のプロジェクト構成

.NET MAUI のプロジェクトは以下のようになっています。Xamarin.Forms のような「単一の共有プロジェクト + 複数のプラットフォーム固有プロジェクト」の構成ではなく、単一のプロジェクトで、複数プラットフォームをターゲットにできるようになりました。
 

 
 

Xamarin.Forms のアーキテクチャーに起因する問題点

Xamarin.Forms は登場当時、画期的なフレームワークでした。ですが、利用が進むにつれてアーキテクチャーに起因する問題点も顕在化していきました。
 

Xamarin.Forms のレンダラーアーキテクチャー

以下の図のようにレンダラーは共有プロジェクトの UI コントロール実装に依存しています。
 

 
 

Xamarin.Forms レンダラーアーキテクチャーの問題点

  • レンダラーが Xamarin.Forms の UI コンポーネントと密結合している。
  • アセンブリスキャンとリフレクションというコストの大きい処理を使用して UI コントロールのレンダラーを取得するため遅い。
  • 共有プロジェクトからネイティブ UI コントロールをカスタマイズする場合、たった1つのプロパティの変更であっても、共有プロジェクトとネイティブプロジェクトにまたがるレンダリングの仕組みを理解した上で、定型的な多くのコードを記述する必要があり、非常に手間がかかる。

 
 

.NET MAUI の最大の変更点

Xamarin.Forms からの最大の変更点は、レンダラーアーキテクチャーからハンドラーアーキテクチャーへの変更です。ハンドラーアーキテクチャの採用によってプラットフォーム固有のレンダリングの責務を、抽象化 UI フレームワークの実装から分離しました。
 

.NET MAUI で導入されたハンドラーアーキテクチャー

以下の図のようにハンドラーはインターフェースにのみ依存し、抽象化 UI コントロールの実装には依存しません。
また、共有コード内で複数のプラットフォームのネイティブ UI コントロールを直接操作できます。複数プラットフォーム対応のために条件付きコンパイルを使用しなければならないのは美しくないですが、残念ながらこれより良い手段は現在のところありません。

 

 
これによって以下に述べるようなアドバンテージが生まれました。
 

.NET MAUI ハンドラーアーキテクチャーのアドバンテージ

  • ハンドラーはアセンブリスキャンが不要となり速度が向上した。
  • 共有コード内でもでもネイティブ UI コントロールに直接アクセスしてプロパティを変更できる。
  • ハンドラーは、スタートアップコードの Generic Host 内に直接記述も可能で、簡単に実装および使用できるため、特定のコントロールまたはアプリ全体で使用されるすべてのコントロールのカスタマイズが簡単に実装できる。

 

(参考)Generic Host

Generic Host は、アプリの起動やシャットダウンのようなライフタイム管理やアプリの構成やロギング、DIのようなアプリのビジネスロジック自体とは関係ない基盤となる機能をカプセル化するオブジェクトです。これによって、アプリの基盤に関わる機能とビジネスロジックを分離できクリーンな構造になります。
 
Generic Host の詳細はこちらをご覧ください。
docs.microsoft.com
 
 

.NET MAUI のスタートアップコードの例

.NET MAUI では Generic Host に対応したため、スタートアップの処理が Xamarin.Forms とは大きく異なっています。
では、.NET MAUI のスタートアップコードのサンプルを見ていきましょう。
 
なお、以下コードは説明のために調整されたものですので、ベストプラクティスではありませんのでご注意ください。
 
まずは、コード全体を見ていきましょう。
Generic Host 内では、

  • 起動するアプリクラスの指定
  • フォントの登録
  • Dependency injection
  • ハンドラーを利用した UI のカスタマイズ

を行なっています。
 
スタートアップコードはMauiProgram.csに記述します。

 
MauiProgram.cs

using MauiUICustomizeSample.Controls;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;

namespace MauiUICustomizeSample;

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");
            });

        // Dependency injection : AppSlell クラスを DI コンテナに登録
        builder.Services.AddTransient<AppShell>();

        // 全ての Label のカスタマイズ
        LabelHandler.Mapper.AppendToMapping(nameof(IView.Background), (handler, view) =>
        {
            if (view is Label)
            {
#if IOS
                handler.PlatformView.BackgroundColor = Colors.MediumSpringGreen.ToPlatform();
#elif ANDROID        
                handler.PlatformView.SetBackgroundColor(Colors.MediumSpringGreen.ToPlatform());
#endif
            }
        });

        // 特定のインスタンスの Button のカスタマイズ
        ButtonHandler.Mapper.AppendToMapping(nameof(IView.Background), (handler, view) =>
        {
            if (view is MyButton)
            {
#if IOS
                handler.PlatformView.BackgroundColor = Colors.LightCoral.ToPlatform();
                handler.PlatformView.SetTitleColor(Colors.White.ToPlatform(), UIKit.UIControlState.Normal);
                handler.PlatformView.Layer.CornerRadius = 7;
#elif ANDROID
                handler.PlatformView.SetBackgroundColor(Colors.LightCoral.ToPlatform());
#endif
            }
        });

        return builder.Build();
    }
}

 
 
次に個別の処理について詳しく見ていきます。
 

MAUI 用の Generic Host のビルダーを作成

MauiAppCreateBuilderメソッドで MauiAppBuilder のインスタンスが作成されます。

var builder = MauiApp.CreateBuilder();

 
 

起動するアプリクラス(このサンプルでは App クラス)を指定

MauiAppBuilderUseMauiAppメソッドの型パラメーターに起動するアプリクラスの型(App)を指定します。

builder.UseMauiApp<App>()

 
App クラスはApp.xaml.csで定義されています。

 
App.xaml.cs のコードは以下のようになっています。

namespace MauiUICustomizeSample;

public partial class App : Application
{
	public App(AppShell appShell)
	{
		InitializeComponent();
		MainPage = appShell;
	}
}

 
 

フォントの登録

プロジェクト内の /Resources/Fonts 内に配置されたフォントを登録します。

 
MauiAppBuilderConfigureFontsメソッド内のデリゲートで登録を行います。

.ConfigureFonts(fonts =>
{
    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
    fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});

 
 

AppSlell クラスを DI コンテナに登録

AppSlell クラスを DI コンテナに登録し、インスタンスが注入されるようにします。

builder.Services.AddTransient<AppShell>();

 
これによって、App.xaml.cs のコンストラクタの appShell パラメータにインスタンスが注入されます。

 
App.xaml.cs

namespace MauiUICustomizeSample;

public partial class App : Application
{
	public App(AppShell appShell)
	{
		InitializeComponent();
		MainPage = appShell;
	}
}

 
 

ハンドラーを利用した UI のカスタマイズ

サンプルでは、「特定の種類の UI コントロール全てをカスタマイズする場合」と「ある UI コントロールの特定のインスタンスのみをカスタマイズする場合」の2つのパターンの UI カスタマイズを行なっています。両者とも Xamarin.Forms の場合と比較して驚くほど簡単にカスタマイズができるようになっています。
 

特定の種類の UI コントロール全てをカスタマイズする場合

こちらでは、Label コントロール全ての背景色をMediumSpringGreenに変更します。
 

 
MauiProgram.cs

        // 全ての Label のカスタマイズ
        LabelHandler.Mapper.AppendToMapping(nameof(IView.Background), (handler, view) =>
        {
            if (view is Label)
            {
#if IOS
                handler.PlatformView.BackgroundColor = Colors.MediumSpringGreen.ToPlatform();
#elif ANDROID        
                handler.PlatformView.SetBackgroundColor(Colors.MediumSpringGreen.ToPlatform());
#endif
            }
        });

AppendToMappingActionデリゲート内で、UI コントロールの型が Labelの場合に、カスタマイズしたい色を設定します。

handler.PlatformViewには、iOS の場合 UILabel、Android の場合 AppCompatTextViewが割り当てられていますので、それぞれのネイティブ UI コントロールのプロパティを変更することで色を変更できます。設定する色はToPlatformメソッドによってそれぞれのプラットフォームのネイティブカラーに変換されます。
 
ここで注目したいことは、たった数行のコードで共有コード内のUILabel.BackgroundColorや、AppCompatTextView.SetBackgroundColorといったプロパティやメソッドが使用できていることです。

Xamarin.Forms でこのようなことをするには、各プラットフォームプロジェクト内でクラスを定義し定型的なコードを書く必要がありましたので、比較にならないほどシンプルになっているのがわかります。
 

ある UI コントロールの特定のインスタンスのみをカスタマイズする場合

こちらでは、Button コントロール特定のインスタンスの背景色をLightCoralに変更します。
 
Button のサブクラス MyButton を作成します。

MyButton.cs

namespace MauiUICustomizeSample.Controls
{
	public class MyButton : Button
	{
		public MyButton()
		{
		}
	}
}

 
MauiProgram.csMyButton に対してカスタマイズを適用します。

 
MauiProgram.cs

        // 特定のインスタンスの Button のカスタマイズ
        ButtonHandler.Mapper.AppendToMapping(nameof(IView.Background), (handler, view) =>
        {
            if (view is MyButton)
            {
#if IOS
                handler.PlatformView.BackgroundColor = Colors.LightCoral.ToPlatform();
                handler.PlatformView.SetTitleColor(Colors.White.ToPlatform(), UIKit.UIControlState.Normal);
                handler.PlatformView.Layer.CornerRadius = 7;
#elif ANDROID
                handler.PlatformView.SetBackgroundColor(Colors.LightCoral.ToPlatform());
#endif
            }
        });

AppendToMappingActionデリゲート内で、UI コントロールの型が MyButtonの場合に、カスタマイズしたい色を設定します。
 
handler.PlatformViewには、iOS の場合 UIButton、Android の場合 MaterialButtonが割り当てられていますので、それぞれのネイティブ UI コントロールのプロパティを変更することで色を変更できます。設定する色はToPlatformメソッドによってそれぞれのプラットフォームのネイティブカラーに変換されます。
 
カスタマイズしたコントロールを XAML 内で使用するには、xmlns:button="clr-namespace:MauiUICustomizeSample.Controls" のように名前空間を指定します。
 
MyButtonのみ背景色がカスタマイズされます。
 

 
MainPage.xaml

<?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:button="clr-namespace:MauiUICustomizeSample.Controls" 
             x:Class="MauiUICustomizeSample.MainPage">
			 
    <ScrollView>
        <VerticalStackLayout 
            Spacing="25" 
            Padding="30,0" 
            VerticalOptions="Center">

            <Image
                Source="dotnet_bot.png"
                SemanticProperties.Description="Cute dot net bot waving hi to you!"
                HeightRequest="200"
                HorizontalOptions="Center" />
                
            <Label 
                Text="Hello, MAUI!"
                SemanticProperties.HeadingLevel="Level1"
                FontSize="32"
                HorizontalOptions="Center" />
            
            <Label 
                Text="Welcome to .NET Multi-platform App UI"
                SemanticProperties.HeadingLevel="Level2"
                SemanticProperties.Description="Welcome to dot net Multi platform App UI"
                FontSize="16"
                HorizontalOptions="Center" />

            <HorizontalStackLayout 
                Spacing="10"  
                HorizontalOptions="Center">
                
                <Button 
                    x:Name="CounterBtn"
                    WidthRequest="120"
                    Text="Click me"
                    SemanticProperties.Hint="Counts the number of times you click"
                    Clicked="OnCounterClicked"
                    HorizontalOptions="Start" />

                <button:MyButton
                    x:Name="CustomizedBtn"
                    WidthRequest="120"
                    Text="Customized"
                    HorizontalOptions="End" />

            </HorizontalStackLayout>
        </VerticalStackLayout>
    </ScrollView>
 
</ContentPage>

 
iOS での実行結果は以下のようになります。
ラベルの全体の背景色と、右側のボタンの背景色がカスタマイズされています。
 

 
ハンドラーを使用したコントロールのカスタマイズの詳細はこちらをご覧ください。
docs.microsoft.com
 
 

まとめ

これまでご説明しましたように、.NET MAUI では、Xamarin.Forms の面倒だった部分が非常に扱いやすくなり、直感的でシンプルなコードで記述できるよう改善されました。また、アセンブリスキャンやリフレクションなどコストの高い処理を回避することで起動速度も向上しています。
 
つまり「 .NET MAUI = よりシンプルで使いやすくなった Xamarin.Forms」と言えます。というわけで、早速使ってみましょう!