.NET+ReactiveUIでXamForms,WPF,WinFormsのクロスプラットフォーム開発やってみた

# はじめに

この投稿では、「.NETを利用したクロスプラットフォーム開発」でReactiveUIを用いるサンプルを提供します。「.NETを利用したクロスプラットフォーム開発」に触れたことのない方に、『なるほど、大体こんなもんか』と感じていただけることを目的に書きました。

**免責事項：**
*この投稿に対応するソースコードも公開しており、自由に利用していただけますが、利用することにより生じた損害等に対するすべての責任は、利用する側が負うものとします。*

**謝辞：**

この投稿を作成するにあたり、以下のサイトを参考にさせていただきました。

* [reactiveui.net](https://reactiveui.net/)

## 背景

来る.NET 5の時代に思いを馳せていると、ふと、「現状ではどこまでのクロスプラットフォーム開発ができるのか？」を試してみたくなりました。

.NET 5では、以下のプラットフォームが統合されるようですね。
> *引用: [再統合された .NET:.NET 5 に関する Microsoft の計画 - Microsoft Docs](https://docs.microsoft.com/ja-jp/archive/msdn-magazine/2019/july/csharp-net-reunified-microsoft%E2%80%99s-plans-for-net-5)*
![mt833477.0719_michaelis_figure2_hires(ja-jp,msdn.10).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/8917027c-85f6-38a0-88e5-38af9b834703.png)

すごいボリュームですね。全部を試すとなると時間が足りないので、絞ります。
今回は、以下を対象にクロスプラットフォームアプリケーションを作ってみたいと思います。

* **DESKTOP** WPF: 現状 → .NET Framework / .NET Core
* **DESKTOP** Windows Forms: 現状 → .NET Framework / .NET Core
* **DESKTOP** UWP: 現状 → ~~.NET Core~~ Xamarin.Forms.UWP
* **MOBILE** Xamarin: 現状 → Xamarin.Forms.Android / Xamarin.Forms.iOS

## 前提

「クロスプラットフォーム開発」の定義としては、部分的になりますが、以下を意識しました。

* フレームワーク/デバイス固有の部分を最小まで取り除き、それ以外を1本のコードで機能を実現する
* 共通のコードからフレームワーク/デバイス固有の部分にアクセスするための方法がある

「フレームワーク/デバイス固有の部分を最小まで取り除く」ために、以下を採用します。

* UIアーキテクチャパターンはMVVMを採用。ライブラリはXamarin.Forms,WinForms,WPFに対応する ReactiveUI を選択。
* フレームワーク/デバイス固有の部分にアクセスするための方法として、ReactiveUIが依存するSplatを利用。

その結果、以下のライブラリ/バージョンを選定しました。

* .NET Standard 2.0 (.NET Frameworkが対応している最後のバージョンがこれ)
* .NET Framework 4.8
* .NET Core 3.1
* [UWP 6.2.8](https://www.nuget.org/packages/Microsoft.NETCore.UniversalWindowsPlatform/6.2.8) (Windows10, varsion 1803) ※1
* [Xamarin.Forms 4.2.0.910310](https://www.nuget.org/packages/Xamarin.Forms/4.2.0.910310)
* [ReactiveUI 11.0.1](https://www.nuget.org/packages/reactiveui/11.0.1)

※1 興味本位で「Visual Studio App Centerとの連携」も試した結果、UWPのApp Centerでのビルドが、Windows Target versionを1803まで落とさないと成功しなかったため、そのバージョンに引きずられる形で・・・ほかのバージョンが決まりました。なお本記事では「Visual Studio App Centerとの連携」には触れていません（それはまた別の機会に）。

## 作るもの

### 要件

* iOS, Android, UWPに対応するXamarin.FormsのView。
* WPFのViewを、.NET Coreと.NET Frameworkの両方で。
* WinForms(Windows Forms)のViewを、.NET Coreと.NET Frameworkの両方で。
* 機能は何でもよい→過去の勉強会ネタ「iTextで使って名刺PDFを生成するJax-RS」のクライアントを、.NET Standardで。
* フレームワーク/デバイス固有の部分の解決の例として、アラートダイアログの表示を実装。（ほんとはローカル保存したPDFを別アプリに送信したかったが・・・時間切れ）

### MVVMパターンの実現イメージ

次のソースコードの関係性によって、MVVMパターンを作っていきます。拡張子の表記があるものが開発対象です。
![NET xPlatform Application Structure(1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/5265987b-b6ae-94f1-790d-72c50c89792d.png)

### 補足：画面遷移の方式

ReactiveUIでは、ViewModelを利用したナビゲーションを行います。それは、ViewにViewModelを対応付け、画面遷移の際にViewModelを指定すれば、Viewが切り替わる仕掛けです。

ReactiveUIでは、この画面遷移方式に対応するために、ViewModelコンテナ```RoutingState```が提供されています。これとView側のコントロール```RoutedViewHost```または```RoutedControlHost```をバインドすることにより、ViewModelによる画面遷移が実現されます。View側の実装が2つあるのは、Xamarin.Forms, WPF, WinFormsでの違いを吸収するためです。

これを実装すると、以下の図のようなイメージになります。
![View navigation image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/1d0e171c-8853-e52d-cfd0-7a7eeff411ad.png)

### 補足：MVVMにおける```Model```の方式

Model層も、ViewModel層と同じく遷移可能な形をとりました。ReactiveUIの設計に倣い、ModelHostをシングルトンで常駐させ、ModelRoutingStateでModelの遷移を管理します。

*※ちなみに今回、このModelHostの命名が一番悩みました。ViewModelの場合はScreenというわかりやすい概念があるのに対し、Modelの場合は・・・。ModelHostとScreenHostの関係は(1-N)でなければならないと考えています。何かいい名前のアイディアをお持ちの方、ぜひご教授ください。。。*

### なお・・・

この記事の内容は、以下のGitHubリポジトリでソースコードを公開しています。
かなり長編の記事ですが、ReactiveUIに明るい方なら「実装」の章以降はあまり読む意味はありません。リポジトリのソースコードを直接お読みください。

* GitHubリポジトリ [atsuteru/dotnet-cross-apps ](https://github.com/atsuteru/dotnet-cross-apps/tree/qiita-1.0)
* 学びを与えてくれる Issue、Pull Request、大歓迎！

# 本編

では、いよいよ実装に移っていきましょう。

## 実装

これから説明する内容は、以下にソースを公開していますので、よろしければ併せ見てください。

* GitHubリポジトリ [atsuteru/dotnet-cross-apps ](https://github.com/atsuteru/dotnet-cross-apps/tree/qiita-1.0)

ソリューションを開くと、こんな感じになっています。
<img width="235" alt="ソリューションエクスプローラー.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/341445be-dd6c-f65c-cd7b-5dd8bca24909.png">

表にすると、こんな感じです。開発する箇所は、太字の部分。全部で13個のプロジェクトを作ります。
<img width="674" alt="ソリューションの構成.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/678f0dcd-3933-9abf-5238-799054f13cc8.png">

なお、以下に見せるソースコードは、読みやすさを優先し、using句、namespaceブロックは省略しています。もし実装時、もし「Nuget追加しているのにクラスの参照がうまくいかないなー」「エラーの波線が消えないー」という時は、以下のusing句を足してみると大体うまくいきます。※以下は、VisualStudioが「候補」として出してくれない時があるので。。。

```c#
using System;
using System.Reactive
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using ReactiveUI;
```

では以下、順番に実装の説明を行います。

### 1) Services

「iTextで使って名刺PDFを生成するJax-RS」=BusinessCard機能のクライアントを作ります。

*※ぶっちゃけ・・・ここは「クロスプラットフォーム開発」に何の関係もありません（いや一応.NET Standardは使っているが、うーん。。。）*

#### Servicesの実装

##### (1) Servicesのすべてのプラットフォーム向けインタフェースの定義

プロジェクト: MyApp.Services

インタフェース：IBusinessCardService
説明：BusinessCard機能のインタフェースを定義。

```c#
    public interface IBusinessCardService
    {
        Task<byte[]> GeneratePDF(GenerateParameter parameter);
    }
```

クラス：GenerateParameter
説明：PDF生成用メソッドのパラメータを定義。ただのPOCOなので掲載は割愛。

##### (2) Servicesのすべてのプラットフォーム向け実装

プロジェクト: MyApp.Services.BusinessCard

クラス：BusinessCardService
説明：外部のBusinessCard機能を呼び出す、REST-APIクライアント。
※内容の説明は、本編と関係がないので省略します。
※なお以下に含まれるURLは、私が必要な時以外は停止しています。

```c#
    public class BusinessCardService : IBusinessCardService
    {
        protected string URL { get; }
        protected int TimeoutSeconds { get; }
        protected string CardTemplate { get; }

        public BusinessCardService()
        {
            URL = "https://business-card-webservice.herokuapp.com/api/businesscard/generate/as/pdf";
            TimeoutSeconds = 10;
            CardTemplate = "templates/business_card.mustache.html";
        }

        Task<byte[]> IBusinessCardService.GeneratePDF(GenerateParameter parameter)
        {
            using (var api = new HttpClient()
            {
                BaseAddress = new Uri(URL),
                Timeout = new TimeSpan(0, 0, TimeoutSeconds)
            })
            {
                var query = HttpUtility.ParseQueryString(string.Empty);
                query.Add("template", CardTemplate);
                query.Add("name", parameter.Name);
                query.Add("company", parameter.Organization);

                var response = api.GetAsync("?" + query.ToString()).Result;
                if (!response.IsSuccessStatusCode)
                {
                    var exception = new ApplicationException("Business card generation error");
                    exception.Data.Add("StatusCode", response.StatusCode);
                    exception.Data.Add("ReasonPhrase", response.ReasonPhrase);
                    exception.Data.Add("Detail", response.Content.ReadAsStringAsync().Result);
                    throw exception;
                }

                return response.Content.ReadAsByteArrayAsync();
            }
        }
    }
```

### 2) Dependencies

Dependencyは直訳すると「依存性」ですが、ここでは「プラットフォーム/フレームワークに依存性がある実装部分」という意味で言葉を使用します。

本実装ではその例として、アラートメッセージの表示機能を実装します。任意のタイトルとメッセージを表示し、OKボタン押下により指定した処理が実行される仕様とします。

*※「iOS,Android、アラートメッセージは実装違うね！いい例だね！」と思って始めたのですが・・・Xamarin.FormsがDisplayAlertを実装していたという大誤算。でもView層に要求を飛ばすため、MessageBusの良いサンプルになるし、まぁいっか、となったのが、ここの実情です。。。*


#### Dependenciesの実装

実装のバリエーションは、共通のインタフェース1種類+個別の実装3種類。

* 共通のインタフェース(.NET Standard) ...アラートメッセージ表示機能のインタフェースを定義。
* WinForms(.NET Framework/.NET Core) ...Sysytem.Windows.Forms.MessageBox を利用して実装。
* WPF(.NET Framework/.NET Core) ...Sysytem.Windows.Forms.MessageBox を利用して実装。
* Xamarin.Forms(Xamarin.UWP, Xamarin.iOS, Xamarin.Android) ...Xamarin.Forms.Page.DisplayAlertメソッドを利用して実装。

クロスプラットフォーム対応としての実装は、以下の通り。

|クラス|役割|
|:--|:--|
|IMessageDialog|機能を利用するためのインタフェース。すべてのプラットフォーム向け。|
|MessageDialog|機能を実装したクラス。各プラットフォーム向けに実装する。|


##### (1) Dependenciesのすべてのプラットフォーム向けインタフェースの定義

プロジェクト：MyApp.Dependencies (.NET Standard 2.0)
インタフェース：IMessageDialog.cs
説明：メッセージダイアログ処理インタフェース

```c#
    public interface IMessageDialog
    {
        void ShowAlertMessage(string title, string message, Action onOkAction);
    }
```

##### (2) WinForms向けのDependenciesの実装

プロジェクト：MyApp.WinForms, MyApp.WinForms.NetFramework
クラス：MessageDialog.cs
説明：WinForms用メッセージダイアログ表示処理の実装
補足：System.Windows.Forms.MessageBoxクラスを利用。

```c#
    public class MessageDialog : IMessageDialog
    {
        void IMessageDialog.ShowAlertMessage(string title, string message, Action onOkAction)
        {
            MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Warning);
            onOkAction?.Invoke();
        }
    }
```

##### (3) WPF向けのDependenciesの実装

プロジェクト：MyApp.WPF, MyApp.WPF.NetFramework
クラス：MessageDialog.cs
説明：WPF用メッセージダイアログ表示処理の実装
補足：System.Windows.MessageBoxクラスを利用。

```c#
    public class MessageDialog : IMessageDialog
    {
        void IMessageDialog.ShowAlertMessage(string title, string message, Action onOkAction)
        {
            MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Warning);
            onOkAction?.Invoke();
        }
    }
```

##### (4) Xamarin.Forms向けのDependenciesの実装

プロジェクト：MyApp.XamForms
クラス：MessageDialog.cs
説明：Xamarin.Forms用メッセージダイアログ表示処理の実装
補足：Xamarin.Forms.Page.DisplayAlertメソッドを利用。ReactiveUIのMessageBusを使用してView側に要求を送信します。ちなみに・・・ここで使用するMessageBusはメインスレッド上で生成しているため、要求された処理は常にメインスレッド上で実行されます。

```c#
    public class MessageDialog : IMessageDialog
    {
        void IMessageDialog.ShowAlertMessage(string title, string message, Action onOkAction)
        {
            // View側へメッセージダイアログ表示要求を送信
            MessageBus.Current.SendMessage(new MessageDialogRequest()
            {
                Title = title,
                Message = message
            });
            onOkAction?.Invoke();
        }

        public class MessageDialogRequest
        {
            public string Title { get; set; }
            public string Message { get; set; }
        }
    }
```

```c#
    public partial class HomeView : ReactiveContentPage<HomeViewModel>
    {
        protected void HandleViewModelBound(CompositeDisposable d)
        {
            // メッセージダイアログ表示要求の受信を開始
            MessageBus.Current.Listen<MessageDialogRequest>().Subscribe(async (x) => await HandleMessageDialogRequest(x)).DisposeWith(d);
        }

        protected async Task HandleMessageDialogRequest(MessageDialogRequest request)
        {
            // メッセージダイアログ表示要求に基づき、メッセージダイアログを表示する
            await DisplayAlert(request.Title, request.Message, "OK");
        }

```

### 3) Model

Model、それは「処理要件を実現するための手続きを司るもの」。
ユースケースに書かれるようなレベルの手続きを実装し、入出力等の処理の実装はServices/Dependenciesに任せます。なお、手続きの実行結果は、他の層に対しては*通知*する形で伝えるところがポイント。

2種類のModelを作成します。

* 「ホスト」としての役割を持つModel。今回のModelHostです。
* 「遷移可能」としての役割を持つModel。今回のApplicationStarterとBusinessCardGeneratorです。

#### Modelの実装

Modelの役割毎にインタフェースを用意します。
今回はそれら役割に対応する抽象クラスも用意しますので、その関係を先にまとめておきます。

インタフェース: IModelHost
役割: 各Modelのホストとしての役割を可能にする。モデル遷移の親オブジェクト。
実装: ModelHost, ModelRoutingState

インタフェース: IRoutableModel
役割: ModelHost内でのModel遷移を可能にする。モデル遷移の子オブジェクト。
実装: RouteModelBase

よって、実装対象は、以下の通り。

|クラス|役割|
|:--|:--|
|IModelHost|各Modelのホストとしての役割を可能にする|
|ModelHost|IModelHostとしての役割の実装|
|ModelRoutingState|ModelHost内のModel遷移機能の実装|
|IRoutableModel|遷移可能なModelとしての役割を可能にする|
|RouteModelBase|IRoutableModelとしての役割の実装|
|ApplicationStarter|アプリケーション起動時の手続きの実装|
|BusinessCardGenerator|ビジネスカード作成に関する手続きの実装|

クラス図です。
![include.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/896e6666-21c8-86df-3894-467ed3c20b43.png)

##### (1) すべてのプラットフォーム向けの実装

プロジェクト: MyApp.Model

インターフェース：IModelHost
説明：各Modelのホストとしての役割を可能にする

```c#
    public interface IModelHost
    {
        ModelRoutingState Router { get; }
    }
```

クラス：ModelHost
説明：IModelHostとしての役割を実装する
補足：starterFactoryは、アプリケーション起動用の最初のModelを受け取るための引数。

```c#
    public class ModelHost: IModelHost
    {
        public ModelRoutingState Router { get; }

        public ModelHost(Func<IModelHost, IRoutableModel> starterFactory)
        {
            Router = new ModelRoutingState();
            Router.Navigate(starterFactory.Invoke(this));
        }
    }
```

クラス：ModelRoutingState
説明：ModelHost内のModel遷移機能を実装する

```c#
    public class ModelRoutingState
    {
        public IRoutableModel Current { get; protected set; }

        public void Navigate(IRoutableModel model)
        {
            Current = model;
        }
    }
```

インターフェース：IRoutableModel
説明：遷移可能なModelとしての役割を可能にする

```c#
    public interface IRoutableModel
    {
        IModelHost Host { get; }
    }
```

クラス：RouteModelBase
説明：IRoutableModelとしての役割を実装する

```c#
    public abstract class RouteModelBase : IRoutableModel
    {
        public IModelHost Host { get; }

        public RouteModelBase(IModelHost host)
        {
            Host = host;
        }
    }
```

クラス：ApplicationStarter
説明：アプリケーション起動時の手続きを実装する

```c#
    public class ApplicationStarter : RouteModelBase
    {
        public ApplicationStarter(IModelHost host) : base(host)
        {
        }
        // アプリケーション初期化手続きを「観測可能な」オブジェクトとして公開する
        public IObservable<InitializeResponse> Initialize(InitializeRequest request)
        {
            return Observable
                .FromAsync(() => InitializeAsync(request));
        }
        // アプリケーション初期化手続きを定義する
        protected virtual Task<InitializeResponse> InitializeAsync(InitializeRequest request)
        {
            // BusinessCardGeneratorに遷移
            Host.Router.Navigate(new BusinessCardGenerator(Host));
            return Task.FromResult(new InitializeResponse());
        }
    }
```

クラス：BusinessCardGenerator
説明：ビジネスカード作成に関する手続きを実装する
補足：Generateの手続きは、```SubscribeOn(ThreadPoolScheduler.Instance)```の指定により別スレッドで進められるように定義。

```c#
    public class BusinessCardGenerator : RouteModelBase
    {
        public BusinessCardGenerator(IModelHost host) : base(host)
        {
        }
        // ビジネスカード生成手続きを「観測可能な」オブジェクトとして公開する
        public IObservable<GenerateResponse> Generate(GenerateRequest request)
        {
            return Observable
                .FromAsync(() => GenerateAsync(request))
                .SubscribeOn(ThreadPoolScheduler.Instance); //別スレッド(スレッドプール利用)の宣言
        }
        // ビジネスカード生成手続きを定義する
        protected virtual Task<GenerateResponse> GenerateAsync(GenerateRequest request)
        {
            string result = null;
            try
            {
                // ビジネスカード生成をサービスに依頼
                var pdfData = Locator.Current.GetService<IBusinessCardService>()
                    .GeneratePDF(new GenerateParameter()
                    {
                        Name = request.Name,
                        Organization = request.Organization
                    })
                    .Result;
                // ビジネスカード生成結果をファイルに書き出す
                var filePath = Path.Combine(
                    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 
                    Path.GetTempFileName());
                File.WriteAllBytes(filePath, pdfData);
                result = filePath;
            }
            catch (Exception ex)
            {
                // ビジネスカード生成失敗時はアラートメッセージ表示を依頼
                Locator.Current.GetService<IMessageDialog>()
                    .ShowAlertMessage("Generation error", ex.Message, () =>
                    {
                        result = ex.Message;
                    });
            }

            return Task.FromResult(new GenerateResponse() { Result = result });
        }
        // ビジネスカードの新規生成に戻る手続きを「観測可能な」オブジェクトとして公開する
        public IObservable<InitializeResponse> Initialize(InitializeRequest request)
        {
            return Observable
                .FromAsync(() => InitializeAsync(request));
        }
        // ビジネスカードの新規生成に戻る手続きを定義する　※今は特にやることがない
        protected virtual Task<InitializeResponse> InitializeAsync(InitializeRequest request)
        {
            return Task.FromResult(new InitializeResponse());
        }
    }
```

### 4) ViewModel

ViewModel、すなわち「ModelとViewの仲介者」。
入力をViewから受け取ってModelの入力とし、また結果をModelから受け取ってViewに出力させます。

2種類のViewModelを作成します。

* 「スクリーンホスト」としての役割を持つViewModel。今回のMainViewModelです。
* 「画面遷移可能」としての役割を持つViewModel。今回のHomeViewModelとResultViewModelです。

#### ViewModelの実装

ViewModelは、役割毎にインタフェースを実装しなければなりません。
今回はそれら役割に対応する抽象クラスを用意していますので、その関係を先にまとめておきます。

インタフェース: IActivatableViewModel
役割: Viewの画面遷移に合わせた処理の呼び出しを可能にする
実装: ActivatableViewModel

インタフェース: IScreen
役割: スクリーンホストとしての役割を可能にする。画面遷移の親オブジェクト。
実装: ScreenHostableViewModel

インタフェース: IModelAccessableScreen
役割: スクリーンホストからModelへのアクセスを可能にする。
実装: ScreenHostViewModelBase

インタフェース: IRoutableViewModel
役割: スクリーンホスト内での画面遷移を可能にする。画面遷移の子オブジェクト。
実装: RoutableViewModel, RouteViewModelBase

よって、実装対象は、以下の通り。

|クラス|役割|
|:--|:--|
|ActivatableViewModel|Viewの画面遷移に合わせた処理の呼び出しを可能にする|
|ScreenHostableViewModel|スクリーンホストとしての役割を可能にする|
|IModelAccessableScreen|スクリーンホストからModelへのアクセスを可能にする|
|ScreenHostViewModelBase|Modelへのアクセスを可能な役割を実装する|
|RoutableViewModel|画面遷移を可能にする|
|RouteViewModelBase|画面遷移なViewModelの共通処理を実装する|
|MainViewModel|アプリケーションのメインウィンドウのViewModel。スクリーンホスト。|
|HomeViewModel|ビジネスカード作成の入力フォームのViewModel。画面遷移可能。|
|ResultViewModel|ビジネスカード作成の処理結果表示画面のViewModel。画面遷移可能。|

クラス図です。
![include.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109362/e0592b0c-8061-b085-701e-a347482ef667.png)

##### (1) すべてのプラットフォーム向けの実装

プロジェクト: MyApp.ViewModel

クラス：ActivatableViewModel
説明：Viewの遷移に連動して呼び出される機能を実装。今回は全ViewModelの基底クラスとして利用。

```c#
    public abstract class ActivatableViewModel: ReactiveObject, IActivatableViewModel
    {
        public ViewModelActivator Activator { get; }

        public ActivatableViewModel()
        {
            Activator = new ViewModelActivator();

            this.WhenActivated(disposables =>
            {
                HandleActivation(disposables);
                Disposable.Create(() => HandleDeactivation()).DisposeWith(disposables);
            });
        }

        protected virtual void HandleActivation(CompositeDisposable d)
        {
        }

        protected virtual void HandleDeactivation()
        {
        }
    }
```

クラス：ScreenHostableViewModel
説明：RoutedViewHost/RoutedControlHostのスクリーンホストとしての役割を実装。

```c#
    public abstract class ScreenHostableViewModel : ActivatableViewModel, IScreen
    {
        public RoutingState Router { get; }

        public ScreenHostableViewModel()
        {
            Router = new RoutingState();
        }
    }
```

インターフェース：IModelAccessableScreen
説明：スクリーンホストからModelへのアクセスを可能にする。

```c#
    public interface IModelAccessableScreen: IScreen
    {
        IModelHost Model { get; }
    }
```

クラス：ScreenHostViewModelBase
説明：IModelAccessableScreenとしての役割を実装する

```c#
    public abstract class ScreenHostViewModelBase : ScreenHostableViewModel, IModelAccessableScreen
    {
        public IModelHost Model { get; }

        public ScreenHostViewModelBase(IModelHost model)
        {
            Model = model;
        }
    }
```

クラス：RoutableViewModel
説明：画面遷移に対応するViewModelの共通処理を定義する。
補足：```IRoutableViewModel.UrlPathSegment```は現在の遷移位置を表すプロパティ。Xamarin.Formsではデフォルトでアプリケーションタイトルとして表示される。今回はWPF/WinFormsでもウィンドウタイトルの一部として利用。

```c#
    public abstract class RoutableViewModel : ActivatableViewModel, IRoutableViewModel
    {
        string IRoutableViewModel.UrlPathSegment => GetUrlPathSegment();

        private IScreen _hostScreen;

        IScreen IRoutableViewModel.HostScreen { get => _hostScreen; }

        public RoutableViewModel(IScreen hostScreen)
        {
            _hostScreen = hostScreen;
        }

        protected virtual string GetUrlPathSegment()
        {
            return GetType().Name.Replace("ViewModel", string.Empty);
        }
    }
```

クラス：RouteViewModelBase
説明：画面遷移なViewModelの共通処理を実装する
補足：Modelに別スレッドで進められる手続きがあるため、非同期処理中のボタン活性制御用に```IsCommandExecutable```プロパティと、その観測用オブジェクト```CommandExecutable```を定義。

```c#
    public abstract class RouteViewModelBase : RoutableViewModel
    {
        public IModelAccessableScreen Screen { get; }

        protected IObservable<bool> CommandExecutable { get; }

        [Reactive]
        public bool IsCommandExecutable { get; protected set; }

        public RouteViewModelBase(IModelAccessableScreen hostScreen) : base(hostScreen)
        {
            Screen = hostScreen;

            CommandExecutable = this.ObservableForProperty(x => x.IsCommandExecutable)
                .Select(x => x.Value);
        }
    }
```

クラス：MainViewModel
説明：スクリーンホストとしての役割を実装。
補足：WPF/WinFormsではMainWindow/MainFormのViewModelとしての役割も兼任。

* プロパティの```[Reactive]```は、値がセットされた場合にINotifyPropertyChanged.PropertyChangedイベントを発火させるための宣言。PropertyChangedが発火されないと、Viewに値が反映されない。

```c#
    public class MainViewModel: ScreenHostViewModelBase
    {
        [Reactive]
        public string ApplicationTitle { get; set; }

        public MainViewModel(IModelHost model): base(model)
        {
            ApplicationTitle = AppDomain.CurrentDomain.FriendlyName;
            // アプリケーション初期化手続きを実行
            ((ApplicationStarter)Model.Router.Current)
                .Initialize(new InitializeRequest())
                .Select(x =>
                {
                    return new HomeViewModel(this);
                })
                .Select(Router.Navigate.Execute)
                .Subscribe();
        }

        protected override void HandleActivation(CompositeDisposable d)
        {
            // 画面遷移時にアプリケーションタイトルを更新するための定義(WinForms/WPF用)
            this.WhenAnyObservable(_ => _.Router.NavigationChanged)
                .Subscribe(x =>
                {
                    ApplicationTitle = string.Format("{0} [{1}]", AppDomain.CurrentDomain.FriendlyName, Router.GetCurrentViewModel()?.UrlPathSegment);
                })
                .DisposeWith(d);
        }
    }
```

クラス：HomeViewModel
説明：初期画面のViewModel。BusinessCardを作成するための入力を受け、BusinessCard生成手続きを実行。また処理終了の通知を受け、結果画面への遷移を行う。
補足：```SubmitCommand.ThrownExceptions.Subscribe```の定義を追加すれば、手続きから例外が戻された場合の処理も記述可能。

```c#
    public class HomeViewModel: RouteViewModelBase
    {
        [Reactive]
        public string Name { get; set; }

        [Reactive]
        public string Organization { get; set; }

        public ReactiveCommand<Unit, GenerateResponse> SubmitCommand { get; protected set; }

        public HomeViewModel(IModelAccessableScreen hostScreen) : base(hostScreen)
        {
        }

        protected override void HandleActivation(CompositeDisposable d)
        {
            IsCommandExecutable = false;
            // 「ビジネスカードの作成」ボタンにビジネスカード作成手続きの実行を関連付け。
            // この手続きは非同期で行われるため、ボタンの活性制御のためのプロパティを指定
            SubmitCommand = ReactiveCommand
                .CreateFromObservable(() =>
                {
                    return ((BusinessCardGenerator)Screen.Model.Router.Current)
                        .Generate(new GenerateRequest()
                        {
                            Name = Name,
                            Organization = Organization
                        });
                }, CommandExecutable);
            // 手続きの実行完了後、ResultViewへ遷移するための定義
            SubmitCommand
                .Select(response =>
                {
                    return new ResultViewModel(Screen)
                    {
                        Name = Name,
                        Organization = Organization,
                        Result = response.Result
                    };
                })
                .Select(x => Screen.Router.Navigate.Execute(x).Subscribe())
                .Subscribe()
                .DisposeWith(d);

            IsCommandExecutable = true;
        }
    }
```

クラス：ResultViewModel
説明：結果表示画面のViewModel。結果を表示し、初期画面への遷移を行う。

```c#
    public class ResultViewModel: RouteViewModelBase
    {
        [Reactive]
        public string Name { get; set; }

        [Reactive]
        public string Organization { get; set; }

        [Reactive]
        public string Result { get; set; }

        public ReactiveCommand<Unit, InitializeResponse> BackCommand { get; protected set; }

        public ResultViewModel(IModelAccessableScreen hostScreen) : base(hostScreen)
        {
        }

        protected override void HandleActivation(CompositeDisposable d)
        {
            // 「新しいビジネスカードを作成する」ボタンに対応する手続きの実行を関連付け。
            BackCommand = ReactiveCommand
                .CreateFromObservable(() =>
                {
                    return ((BusinessCardGenerator)Screen.Model.Router.Current)
                        .Initialize(new InitializeRequest());
                });
            // 手続きの実行完了後、HomeViewへ復帰するための定義
            BackCommand
                .Select(x => Screen.Router.NavigateBack.Execute().Subscribe())
                .Subscribe()
                .DisposeWith(d);
        }
    }
```

### 5) View

Viewとして作る部分は2つ。「デザイン」と「ViewModelとのバインド」です。

* 「デザイン」は、WinFormsなら\*.Designer.csでデザインし、動的なUIは\*.csにも記述できます。
WPF, Xamarin.Formsなら、\*.xamlにデザインし、動的なUIは\*.csにも記述できます。
* 「ViewModelとのバインド」は、\*.csに記述します。※\*.xamlに記述することも可能ですが、デザイナーとプログラマーの職責分担の観点から、\*.csに記述することを個人的に好みます。

#### Viewの実装

実装のバリエーションは、以下3種類。
UWP, Android, iOSはXamarin.FormsによるクロスプラットフォームUIを利用します。

* WinForms(.NET Framework/.NET Core) 
* WPF(.NET Framework/.NET Core) 
* Xamarin.Forms(Xamarin.UWP, Xamarin.Android, Xamarin.iOS) 

クロスプラットフォーム対応としての実装は、以下の通り。

|クラス|役割|
|:--|:--|
|MainWindow|アプリケーションのメインウィンドウ。画面遷移のホストコントロールを配置する。|
|HomeView|画面遷移のホストコントロール上で最初に表示される画面。ビジネスカード作成の入力フォーム。|
|ResultView|ビジネスカード作成の処理結果を表示する画面。初期画面に戻ることができる。|

また、プラットフォーム毎に利用できる継承元クラスが異なるため、先にまとめておきます。

|クラス|WinForms(ReactiveUI.WinForms)|WPF(ReactiveUI.WPF)|WPF(ReactiveUI.XamForms)|
|:--|:--|:--|:--|
|MainWindow|Form, IViewFor\<MainViewModel\>|ReactiveWindow\<MainViewModel\>|(実装無し)|
|HomeView|ReactiveUserControl\<HomeViewModel\>|ReactiveUserControl\<HomeViewModel\>|ReactiveContentPage\<HomeViewModel\>|
|ResultView|ReactiveUserControl\<ResultViewModel\>|ReactiveUserControl\<ResultViewModel\>|ReactiveContentPage\<ResultViewModel\>|

では以下、WinForms, WPF, Xamarinで実装方法が異なる点を中心に、説明していきます。
なおViewのデザイン部分は、「デザイン＋コントロールの名前付け」しかしていないので、説明は省略します。

##### (1) WinForms向けのViewの実装

プロジェクト：MyApp.WinForms, MyApp.WinForms.NetFramework

クラス：MainForm
説明：WinFormsアプリケーションのMainWindow。
補足：.NET Framework版と.NET Core版での実装に、違いはありません。
注１：MainFormのデザインにRoutedControlHostを貼り付け、名前を"RoutedControlHost"としています。
注２：```IsViewModelBound ```の実装は、画面遷移で画面を離れる際に、余計に呼び出される現象を回避するための対策です。

```c#
    // WinFormsでは、Formを継承し、IViewFor<MainViewModel>インタフェースを自分で実装します。
    public partial class MainForm : Form, IViewFor<MainViewModel>
    {
        protected bool IsViewModelBound { get; private set; }
        public MainViewModel ViewModel { get; set; }
        object IViewFor.ViewModel { get => ViewModel; set => ViewModel = value as MainViewModel; }

        public MainForm()
        {
            InitializeComponent();

            ViewModel = new MainViewModel(Locator.Current.GetService<IModelHost>());
            RoutedControlHost.Router = ViewModel.Router;

            this.WhenActivated((d) =>
            {
                if (IsViewModelBound)
                {
                    return;
                }
                HandleViewModelBound(d);
                IsViewModelBound = true;
            });
        }

        protected void HandleViewModelBound(CompositeDisposable d)
        {
            // ViewModelとのバインド
            this.Bind(ViewModel, vm => vm.ApplicationTitle, v => v.Text).DisposeWith(d);
        }
    }
```

クラス：HomeView
説明：初期画面として、BusinessCardの作成条件入力フォームと、作成ボタンを配置する画面です。
補足：.NET Framework版と.NET Core版での実装に、違いはありません。

```c#
    public partial class HomeView : ReactiveUserControl<HomeViewModel>
    {
        protected bool IsViewModelBound { get; private set; }

        public HomeView()
        {
            InitializeComponent();

            this.WhenActivated((d) =>
            {
                if (IsViewModelBound)
                {
                    return;
                }
                HandleViewModelBound(d);
                IsViewModelBound = true;
            });
        }

        protected void HandleViewModelBound(CompositeDisposable d)
        {
            // ViewModelとのバインド
            this.Bind(ViewModel, vm => vm.Name, v => v.NameTextBox.Text).DisposeWith(d);
            this.Bind(ViewModel, vm => vm.Organization, v => v.OrganizationTextBox.Text).DisposeWith(d);
            this.BindCommand(ViewModel, vm => vm.SubmitCommand, v => v.GenerateButton).DisposeWith(d);
        }
    }
```

クラス：ResultView
説明：WinForms版の結果表示画面です。
補足：.NET Framework版と.NET Core版での実装に、違いはありません。

```c#
    public partial class ResultView : ReactiveUserControl<ResultViewModel>
    {
        /*... HomeViewと同じなので中略 ...*/

        protected void HandleViewModelBound(CompositeDisposable d)
        {
            // ViewModelとのバインド
            this.OneWayBind(ViewModel, vm => vm.Name, v => v.NameTextBox.Text).DisposeWith(d);
            this.OneWayBind(ViewModel, vm => vm.Organization, v => v.OrganizationTextBox.Text).DisposeWith(d);
            this.OneWayBind(ViewModel, vm => vm.Result, v => v.ResultTextBox.Text).DisposeWith(d);
            this.BindCommand(ViewModel, vm => vm.BackCommand, v => v.BackButton).DisposeWith(d);
        }
    }
```

##### (2) WPF向けのViewの実装

プロジェクト：MyApp.WPF, MyApp.WPF.NetFramework

クラス：MainWindow
説明：WPFアプリケーションのMainWindow。補足にある以外の実装は、WinFormsと同じです。
補足：.NET Framework版と.NET Core版での実装に、違いはありません。
注１： MainWindowのデザイン全体にRoutedViewHostタグを配置し、名前を"RoutedViewHost"としています。
注２：~~```IsViewModelBound ```の実装は、画面遷移で画面を離れる際に、余計に呼び出される現象を回避するための対策です。~~ *ReactiveUI 12.1.1 でこの問題は解消されました。*

```c#
    public partial class MainWindow : ReactiveWindow<MainViewModel>
    {
        public MainWindow()
        {
            InitializeComponent();

            ViewModel = new MainViewModel(Locator.Current.GetService<IModelHost>());
            RoutedViewHost.Router = ViewModel.Router;

            this.WhenActivated((d) =>
            {
                HandleViewModelBound(d);
            });
        }

        protected void HandleViewModelBound(CompositeDisposable d)
        {
            // ViewModelとのバインド
            this.Bind(ViewModel, vm => vm.ApplicationTitle, v => v.Title).DisposeWith(d);
        }
    }
```

クラス：HomeView
説明：WPF版の初期画面です。継承元のクラスはWinFormsと同名ですが、提供元は違います。
補足：.NET Framework版と.NET Core版での実装に、違いはありません。

```c#
    public partial class HomeView : ReactiveUserControl<HomeViewModel>
    {
        public HomeView()
        {
            InitializeComponent();

            this.WhenActivated((d) =>
            {
                HandleViewModelBound(d);
            });
        }

        protected void HandleViewModelBound(CompositeDisposable d)
        {
            this.Bind(ViewModel, vm => vm.Name, v => v.NameTextBox.Text).DisposeWith(d);
            this.Bind(ViewModel, vm => vm.Organization, v => v.OrganizationTextBox.Text).DisposeWith(d);
            this.BindCommand(ViewModel, vm => vm.SubmitCommand, v => v.GenerateButton).DisposeWith(d);
        }
    }
```

クラス：ResultView
説明：WPF版の結果表示画面です。
補足：.NET Framework版と.NET Core版での実装に、違いはありません。

```c#
    public partial class ResultView : ReactiveUserControl<ResultViewModel>
    {
        public ResultView()
        {
            InitializeComponent();

            this.WhenActivated((d) =>
            {
                HandleViewModelBound(d);
            });
        }

        protected void HandleViewModelBound(CompositeDisposable d)
        {
            this.OneWayBind(ViewModel, vm => vm.Name, v => v.NameTextBox.Text).DisposeWith(d);
            this.OneWayBind(ViewModel, vm => vm.Organization, v => v.OrganizationTextBox.Text).DisposeWith(d);
            this.OneWayBind(ViewModel, vm => vm.Result, v => v.ResultTextBox.Text).DisposeWith(d);
            this.BindCommand(ViewModel, vm => vm.BackCommand, v => v.BackButton).DisposeWith(d);
        }
    }
```

##### (3) Xamarin.Forms向けのViewの実装

プロジェクト：MyApp(Xamarin.Forms)

クラス：App
説明：Xamarin.FormsアプリケーションのMainWindowの代わりになるコード。
注１：```RoutedViewHost```を```MainPage```プロパティに設定します。
注２：「ScreenHost」の役割を持つ```MainViewModel```を、```IScreen```として```Splat.Locator```に登録する必要があります。

```c#
        public App()
        {
            InitializeComponent();

            var screen = new MainViewModel(Locator.Current.GetService<IModelHost>());
            Locator.CurrentMutable.RegisterConstant(screen, typeof(IScreen));
            MainPage = new RoutedViewHost() { Router = screen.Router };
        }
```

クラス：HomeView
説明：Xamarin.Forms版の初期画面です。
注１：Xamarin.Formsでは、```IsViewModelBound ```の実装は不要です。（画面遷移で画面を離れる際にの余計な呼び出しは発生しません）

```c#
    public partial class HomeView : ReactiveContentPage<HomeViewModel>
    {
        public HomeView()
        {
            InitializeComponent();

            this.WhenActivated((d) =>
            {
                HandleViewModelBound(d);
            });
        }

        protected void HandleViewModelBound(CompositeDisposable d)
        {
            /*...WinForms版と全く同じなので、中略...*/
        }
    }
```

クラス：ResultView
説明：Xamarin.Forms版の結果表示画面です。

```c#
    public partial class ResultView : ReactiveContentPage<ResultViewModel>
    {
        /*...HomeViewと全く同じなので、中略...*/

        protected void HandleViewModelBound(CompositeDisposable d)
        {
            /*...WinForms版と全く同じなので、中略...*/
        }
    }
```

### 6) EntryPoint

EntryPointとは、アプリケーションが起動する際に一番最初に実行されるソースコードの部分です。
プラットフォーム/フレームワーク固有の処理であるため、プロジェクトはその数だけ作成します。
クロスプラットフォーム対応としては、Dependencies、Servicesの登録と、ViewとViewModelの対応付けを行います。

なお、Xamarin.Formsを利用するプロジェクト(UWP,Android,iOS)は、プラットフォームのEntryPointからViewフレームワーク(Xamarin.Forms)のEntryPointが独立しているため、プロジェクトの呼び出しの階層が１段、深くなっています。

では以下、WinForms, WPF, Xamarin.Formsで実装方法が異なる点を中心に、説明していきます。

#### EntryPointの実装

実装のバリエーションは、以下5種類+1。.NET Frameworkと.NET Coreは同一コードのコピーです。+1は、Xamarin.Formsフレームワーク共通の実装を切り出したもの。

* WinForms(.NET Framework/.NET Core)
* WPF(.NET Framework/.NET Core)
* Xamarin.Forms.iOS
* Xamarin.Forms.Android
* Xamarin.Forms.UWP
* Xamarin.Forms

クロスプラットフォーム対応としての実装は、以下の通り。

|クラス|役割|
|:--|:--|
|EntryPoint|プログラムが起動して一番最初に呼ばれるクラス。Dependencies、Servicesの登録を行う。ViewとViewModelの対応付けを行う(Xamarin.Forms以外)|
|Xamarin.Forms EntryPoint|Xamarin.Formsの起動のために呼ばれるクラス。ViewとViewModelの対応付けを行う|

##### (1) WinForms アプリケーションのEntryPoint

プロジェクト：MyApp.WinForms, MyApp.WinForms.NetFramework
クラス：Program.cs
説明：WinFormsアプリケーションのエントリーポイント。
補足：.NET Framework版と.NET Core版での実装に、違いはありません。

```c#
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            // Regist Services
            Locator.CurrentMutable.RegisterLazySingleton<IBusinessCardService>(() => new BusinessCardService());
            // Regist Dependencies
            Locator.CurrentMutable.Register<IMessageDialog>(() => new MessageDialog());
            // Regist Model
            Locator.CurrentMutable.RegisterConstant<IModelHost>(new ModelHost(x => new ApplicationStarter(x)));
            // Regist ViewModels
            Locator.CurrentMutable.RegisterViewsForViewModels(typeof(MainForm).Assembly);

            Application.Run(new MainForm());
        }
```

##### (2) WPF アプリケーションのEntryPoint

プロジェクト：MyApp.WPF, MyApp.WPF.NetFramework
クラス：App.xaml.cs
説明：WPFアプリケーションのエントリーポイント。
補足：.NET Framework版と.NET Core版での実装に、違いはありません。

```c#
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            // Regist Services
            Locator.CurrentMutable.RegisterLazySingleton<IBusinessCardService>(() => new BusinessCardService());
            // Regist Dependencies
            Locator.CurrentMutable.Register<IMessageDialog>(() => new MessageDialog());
            // Regist Model
            Locator.CurrentMutable.RegisterConstant<IModelHost>(new ModelHost(x => new ApplicationStarter(x)));
            // Regist ViewModels
            Locator.CurrentMutable.RegisterViewsForViewModels(typeof(MainWindow).Assembly);

            base.OnStartup(e);
        }
    }
```

##### (3) Xamarin.Forms フレームワークのEntryPoint

プロジェクト：MyApp.Xamarin
クラス：App.xaml.cs
説明：Xamarin.Formsフレームワークのエントリーポイント。

```c#
    public partial class App : Application
    {
        public App()
        {
            InitializeComponent();

            // Regist ViewModels
            Locator.CurrentMutable.RegisterViewsForViewModels(GetType().Assembly);

            var screen = new MainViewModel(Locator.Current.GetService<IModelHost>());
            Locator.CurrentMutable.RegisterConstant(screen, typeof(IScreen));
            MainPage = new RoutedViewHost() { Router = screen.Router };
        }
    }
```

##### (4) Xamarin.Forms.Android アプリケーションのEntryPoint

プロジェクト：MyApp.Xamarin.Android
クラス：MainActivity.cs
説明：Android(Xamarin.Forms)アプリケーションのエントリーポイント。

```c#
    [Activity(Label = "MyApp", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            // Regist Services
            Locator.CurrentMutable.RegisterLazySingleton<IBusinessCardService>(() => new BusinessCardService());
            // Regist Dependencies
            Locator.CurrentMutable.Register<IMessageDialog>(() => new MessageDialog());
            // Regist Model
            Locator.CurrentMutable.RegisterConstant<IModelHost>(new ModelHost(x => new ApplicationStarter(x)));

            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;

            base.OnCreate(savedInstanceState);

            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
            global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
            // Xamarin.FormsのAppクラスをロードする
            LoadApplication(new App());
        }
    }
```

##### (5) Xamarin.Forms.iOS アプリケーションのEntryPoint

プロジェクト：MyApp.Xamarin.iOS
クラス：AppDelegate.cs
説明：iOS(Xamarin.Forms)アプリケーションのエントリーポイント。

```c#
    [Register("AppDelegate")]
    public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
    {
        public override bool FinishedLaunching(UIApplication app, NSDictionary options)
        {
            // Regist Services
            Locator.CurrentMutable.RegisterLazySingleton<IBusinessCardService>(() => new BusinessCardService());
            // Regist Dependencies
            Locator.CurrentMutable.Register<IMessageDialog>(() => new MessageDialog());
            // Regist Model
            Locator.CurrentMutable.RegisterConstant<IModelHost>(new ModelHost(x => new ApplicationStarter(x)));

            global::Xamarin.Forms.Forms.Init();
            // Xamarin.FormsのAppクラスをロードする
            LoadApplication(new App());
            return base.FinishedLaunching(app, options);
        }
    }
```

##### (6) Xamarin.Forms.UWP アプリケーションのEntryPoint

プロジェクト：MyApp.Xamarin.UWP
クラス：App.xaml.cs
説明：UWP(Xamarin.Forms)アプリケーションのエントリーポイント。
補足：Xamarin.FormsのAppクラスの呼び出しまでの手順が多いため、主要な部分だけ掲載しました。

```c#
    sealed partial class App : Application
    {
        public App()
        {
            // Regist Services
            Locator.CurrentMutable.RegisterLazySingleton<IBusinessCardService>(() => new BusinessCardService());
            // Regist Dependencies
            Locator.CurrentMutable.Register<IMessageDialog>(() => new MessageDialog());
            // Regist Model
            Locator.CurrentMutable.RegisterConstant<IModelHost>(new ModelHost(x => new ApplicationStarter(x)));

            this.InitializeComponent();
            this.Suspending += OnSuspending;
        }
    }
```

```c#
    public sealed partial class MainPage
    {
        public MainPage()
        {
            this.InitializeComponent();
            // Xamarin.FormsのAppクラスをロードする
            LoadApplication(new MyApp.App());
        }
    }
```

# 最後に

以上で、.NETの**現状**を対象にしたクロスプラットフォーム開発が完了しました。
思い付きで始めたにしては、思いのほか重たい内容になりました。。。~~どう考えてもクラスプラットフォーム開発よりMVVMのウェイトがヤバ~~
最後までお読みいただき、ありがとうございます。

しかしXamarin.FormsやReactiveUIを利用することによって、かなりのコードが共通化できることがわかる内容になったと思います。特にXamarin.FormsのUIの共通化はすごいですね。UWPまで対応しているなんて。

また、.NET Core 3.0から実装された WinFormsやWPFが、本当に.NET Frameworkと同じコードで動作したことには驚きました。.NET 5での統合は着々と進行している・・・！！

個人的には、いよいよ**.NET 5**の登場が待ち遠しくなりました。
まぁ実際SIで使用し始めるのは、LTSとなる.NET 6 (2021年11月予定) が出てからになるのでしょうが。。。

この記事が、クロスプラットフォーム開発を始めようとしている方の助けになれば幸いです。

# おまけ

さぁ、明日から Dart×Flutter でクロスプラットフォーム開発やりまっせー！！　（あれ？