.NET6 MAUI preview4でMAUIアプリの作り方を見てみる

## .NET6 preview4

2021/05/25に.NET6 preview4が出ましたね！
それと同時にMAUI preview4が出ました。

さっそくサンプルのWeatherTwentyOneを見つつ、全体の作りと、preview3であれ？と思ったところを見てみます。

## プラットフォーム毎の起動パターン

基本的にはXamarinの起動パターンを踏襲しています。

### iOS / MacCatalyst

`Main`から`UIApplication.Main`を使ってUIKitのアプリケーションを起動します。
そして`[Register("")]`されている名前のDelegateを呼び出します。
…この辺、まんまXamarinですね。

```csharp
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate<Startup>
{
}
```

呼び出されるDelegateは`MauiUIApplicationDelegate<>`を継承したクラスで、設定されているテンプレートのクラスをスタートアップにして呼び出しているようです。

### Android

`[Activity(MainLauncher = true)]`でデフォルトActivityを指定。
`[Application]`でアプリケーションクラスを指定します。
やっぱりXamarinです。

```csharp
[Activity(MainLauncher = true)]
public class MainActivity : MauiAppCompatActivity
{
}

[Application]
public class MainApplication : MauiApplication<Startup>
{
	public MainApplication(IntPtr handle, JniHandleOwnership ownership) : base(handle, ownership)
	{
	}
}
```

`[Application]`のクラスは`MauiApplication<>`を継承したクラスで、設定されているテンプレートのクラスをスタートアップにして呼び出しているようです。

### WinUI

WinUIの場合、少しジレンマがあるようです。

起動に使用する`App.xaml`と`App.xaml.cs`は共通部分に使いたいです。
しかも、XAMLでプリケーションを指定したいのだがスタートアップクラスをテンプレートとして指定したい。
ということで、`MiddleApp`みたいなのをいったん作った後、XAMLで指定するようです。
（サンプルでも「イケてないね」みたいなコメント書いてあります…）

```xml
<local:MiddleApp
    x:Class="WeatherTwentyOne.WinUI.WindowsApp"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WeatherTwentyOne.WinUI">
</local:MiddleApp>
```

```csharp
public partial class WindowsApp : MiddleApp
{
}

public class MiddleApp : MauiWinUIApplication<Startup>
{
}
```

アプリケーションのクラスは`MauiWinUIApplication<>`を継承したクラスで、設定されているテンプレートのクラスをスタートアップにして呼び出しているようです。

### Startup

各プラットフォームで指定したStartupクラスは、起動時の共通設定を行うようになっています。
ASP.NetCoreのような感じですね。

```csharp
public class Startup : IStartup
{
	public void Configure(IAppHostBuilder appBuilder)
	{
		appBuilder
			.UseCompatibility()
			.UseMauiApp<App>()
			.ConfigureServices(services =>
			{
#if WINDOWS
				services.AddSingleton<ITrayService, WinUI.Windows.TrayService>();
				services.AddSingleton<INotificationService, WinUI.Windows.NotificationService>();
#elif MACCATALYST
				services.AddSingleton<ITrayService, MacCatalyst.TrayService>();
				services.AddSingleton<INotificationService, MacCatalyst.NotificationService>();
#endif
			})
			.ConfigureFonts((fonts)=>
			{
				fonts.AddFont("fa-solid-900.ttf", "FontAwesome");
				fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
				fonts.AddFont("OpenSans-SemiBold.ttf", "OpenSansSemiBold");
			});
	}
}
```

サービス登録などもここで行います。
（UnityやDrylocのようなDIものではなく、Xamarinのプラットフォーム毎のサービス）

この後、この中の`UseMauiApp<>`で指定したMAUI本来のアプリケーションを起動させるようです。

### App.xaml, App.xaml.cs

MAUIのアプリケーション起動内では、ウインドウを作ります。

```csharp
public partial class App : Microsoft.Maui.Controls.Application
{
	protected override IWindow CreateWindow(IActivationState activationState)
	{
		Microsoft.Maui.Controls.Compatibility.Forms.Init(activationState);

		return new Microsoft.Maui.Controls.Window(
			new NavigationPage(
				new HomePage()
			)
		);
	}
}
//（Windowを作るところだけを抜粋）
```

Windowは、Windowクラスを使って作ればよいです。
そして、コンストラクタ引数で`Page`クラスを指定します。

### preview3では…（余談）

preview3の`IWindow`では、`IView`を保持して、継承したビューを表示するようになっていました。
なので、

```csharp
public class MainPage : ContentPage
{
	public IView View => Content;
}

public class Window : IWindow
{
	public IPage Page { get; }
	public IView View => Page.View;
}
```

のような実装になっていたので、「あれ？Pageはどこいった？あれNSViewControllerに変換できただろう？」と思ったものです。
（基本macOSでやってたもので…）

というのも、`MauiUIApplicationDelegate`の実装で、

```csharp
			Window = new UIWindow
			{
				RootViewController = window.View.ToUIViewController(mauiContext)
			};
```

という、Viewを`UIViewController`に変換している部分があったため…。
preview4のWindowの実装（Window.Impl.cs）で、下記のような指定になっているので、ああ、ちゃんとPageが指定できるようになっているんだな、とは思いましたが。

```csharp
		public Page? Page
		{
			get => _page;
			set
			{
				if (_page != null)
					InternalChildren.Remove(_page);

				_page = value;

				if (_page != null)
					InternalChildren.Add(_page);

				if (value is NavigableElement ne)
					ne.NavigationProxy.Inner = NavigationProxy;
			}
		}

		IView IWindow.View
		{
			get => Page ?? throw new InvalidOperationException("No page was set on the window.");
			set => Page = (Page)value;
		}
```

## 最後に

というわけで、無事画面周りの実装も

```text
Window
  Page
    View
```

と、階層構造になっていることが確認でき、一安心したpreview4でした。

じっさいの作成では、MAUIのテンプレートから作って必要なプラットフォームのみ作成、ページの作成にはXamarinで使っていたPageやViewを使えばいい、ということになりそうです。
ということで、テンプレートとして出力されるソースをいじるのは、App.xaml.csあたりからで良さげ。