Xamarin.FormsでAndroid・WPF・GTKアプリを開発した際のメモ

# 概要
　以前、パワーアップしたXamarin.Formsでクロスプラットフォームアプリ開発を体験してみた記事を書きました。
　　[Xamarin.FormsでWPF・GTKアプリも開発してみたかった](https://qiita.com/YSRKEN/items/3affcf805d3639b87588)
　しかし、ごく単純なサンプルですので、どうにもインパクトが弱いんじゃないかと考えました。そこで、より実用的なアプリ開発を例に、詳細な解説を行っていきます。

※今回使用したコードは、GiuHubで公開しています。
　[https://github.com/YSRKEN/CalcExpPoint](https://github.com/YSRKEN/CalcExpPoint)

※Linux向けのバイナリは、その性質上Monoを使ったものになります。ざっくり言えば、
　・Windowsだとexeをクリックするだけで動作する
　・LinuxだとMonoのランタイムをインストール後、exeをMono runtimeで開けば動作する
となります。[Monoのインストール方法はこちら](http://www.mono-project.com/download/stable/#download-lin)。

※Xamarin.MacやXamarin.iOSをスルーしているのは、単に**ハードウェアを持っていないから**です。[Xamarin Live Player](https://qiita.com/saken649/items/acf4eb04e475b25b2880)を使えばWindows+iPhoneで表示をプレビューできますが、やはり両者をセットで購入しておくのが理想でしょう。~~いちいち専用ハードを要求するからAppleは嫌いですわ~~

# 作成手順

## Step1. リポジトリを作成する

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/689d7409-21c9-e0a2-0ea0-fa4494463f44.png)

　別に必須ではありませんが、プログラミングする際にGit等でバージョン管理することは**息をするように**行いますよね？

## Step2. プロジェクトを作成する

　[上記記事](https://qiita.com/YSRKEN/items/3affcf805d3639b87588)などを参考にしながら、プロジェクトを構築します。この際、下の画像のようなフォルダ構成にしようと頑張りますと、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/53f44bb3-93fb-dbee-0532-0780fc2363a5.png)

**共有プロジェクトで指定できるソリューションが見つからず詰む**というサイテーな結果になることがありますので注意しましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/58367110-7719-b3be-d2cb-0738e770c7b1.png)

## Step3. お約束
　Mac持ち勢はこれにXamarin.MacとXamarin.iOSを並べられるんですよね？　羨ましい……

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/acfe94b6-ea95-e598-0b41-6bf8ac4e2862.png)

## Step4. UI設計
　なまじ「様々なウィンドウサイズに対応」するのがスマホアプリですので、WPFなどととは色々と勝手が違います。基本的にXAMLなので知見が生かせないわけではないですが……。
　たぶんLive PlayerでXAMLをプレビューしながら組むのが一番楽なのですが、重要なのは「プロジェクト名.Android」ではなく「プロジェクト名.WPF」や「プロジェクト名.GTK」の方を選択して編集することです(Shared ProjectのXAMLを弄っている場合、どれを編集しても同じ結果になります)。なぜか**Android側のソースではIntelliSenseが効きません**ので……

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/81c153d8-b0f6-793f-8400-4e20956b7af9.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/610ac15f-7de8-40f3-07d0-73b5bd1fddc1.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/844fbffa-6e87-b40f-f77a-fc075c724e98.png)

　そんなこんなで組み上げたGUIがこちらです。1つのXAMLで3種類のプラットフォームに対するGUIが構築できていることが分かります。ただ、**スマホ向けにUI設計すると他がかなり不自然になる**のは否めませんので、現実的にはXAMLをそれぞれのプラットフォーム向けに書き分けるのがベストでしょう(~~やり方は知らない~~)

```MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
	xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
	xmlns:local="clr-namespace:CalcExpPoint"
	x:Class="CalcExpPoint.MainPage">

	<Grid Margin="10,10,10,10">
		<Grid.RowDefinitions>
			<RowDefinition Height="auto"/>
			<RowDefinition Height="auto"/>
			<RowDefinition Height="auto"/>
			<RowDefinition Height="auto"/>
		</Grid.RowDefinitions>
		<Grid.ColumnDefinitions>
			<ColumnDefinition/>
			<ColumnDefinition/>
		</Grid.ColumnDefinitions>
		<Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
			Text="経験値計算機"
			VerticalOptions="Center"
			HorizontalOptions="Center"
			FontSize="Large"/>
		<Label Grid.Row="1" Grid.Column="0" Margin="5,5,5,5"
			Text="現在のレベル"
			VerticalOptions="Center"
			HorizontalOptions="End"/>
		<Entry Grid.Row="1" Grid.Column="1" Margin="5,5,5,5"
			Text="1" WidthRequest="50"
			VerticalOptions="Center"
			HorizontalOptions="Start"/>
		<Label Grid.Row="2" Grid.Column="0" Margin="5,5,5,5"
			Text="目標のレベル"
			VerticalOptions="Center"
			HorizontalOptions="End"/>
		<Entry Grid.Row="2" Grid.Column="1" Margin="5,5,5,5"
			Text="165" WidthRequest="50"
			VerticalOptions="Center"
			HorizontalOptions="Start"/>
		<Button Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2"
			Text="計算！"
			VerticalOptions="Center"
			HorizontalOptions="Center"/>
	</Grid>
</ContentPage>
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/b3ac3587-4ee2-28f4-6edf-2685b2c2d9a7.png)

## Step5. ViewModelの構築
　Data Bindingのため、[ReactiveProperty](https://github.com/runceel/ReactiveProperty)を活用します。どういったライブラリなのかについては、次のページなどを参考にしてください。
　　[【C#】ReactiveProperty全然分からねぇ！って人向けのFAQ集【修正済】 - Qiita](https://qiita.com/YSRKEN/items/5a36fb8071104a989fb8)

　まず、ViewModelを構築します。`ReactiveProperty`型および`ReactiveCommand`型はちゃんとpublicなPropertyにする必要がある必要があることと、**とりあえず**`INotifyPropertyChanged`を実装する必要があることに注意しましょう。

```MainViewModel.cs
using Reactive.Bindings;
using System;
using System.ComponentModel;

namespace CalcExpPoint
{
	class MainViewModel : INotifyPropertyChanged
	{
		public ReactiveProperty<int> NowLevel { get; } = new ReactiveProperty<int>(1);
		public ReactiveProperty<int> EndLevel { get; } = new ReactiveProperty<int>(165);
		public ReactiveProperty<string> OutputMessage { get; } = new ReactiveProperty<string>("");
		public ReactiveCommand CalcCommand { get; } = new ReactiveCommand();
		//
		public MainViewModel()
		{
			CalcCommand.Subscribe(_ => {
				// コマンドを定義する
			});
		}

		public event PropertyChangedEventHandler PropertyChanged;
	}
}
```

　次に、そのViewModelを`BindingContext`に代入します。

```MainPage.xaml.cs
using Xamarin.Forms;

namespace CalcExpPoint
{
	public partial class MainPage : ContentPage
	{
		public MainPage()
		{
			InitializeComponent();
			this.BindingContext = new MainViewModel();
		}
	}
}
```

　最後に、それらをXAMLに関連付けます。`ReactiveProperty Hoge`を`Hoge.Value`のようにBindingすることと、`<ContentPage.BindingContext>`で紐づけすることが重要です。

```MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
	xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
	xmlns:local="clr-namespace:CalcExpPoint"
	x:Class="CalcExpPoint.MainPage">

	<ContentPage.BindingContext>
		<local:MainViewModel/>
	</ContentPage.BindingContext>

	<Grid Margin="10,10,10,10">
		<Grid.RowDefinitions>
			<RowDefinition Height="auto"/>
			<RowDefinition Height="auto"/>
			<RowDefinition Height="auto"/>
			<RowDefinition Height="auto"/>
		</Grid.RowDefinitions>
		<Grid.ColumnDefinitions>
			<ColumnDefinition/>
			<ColumnDefinition/>
		</Grid.ColumnDefinitions>
		<Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
			Text="経験値計算機"
			VerticalOptions="Center"
			HorizontalOptions="Center"
			FontSize="Large"/>
		<Label Grid.Row="1" Grid.Column="0" Margin="5,5,5,5"
			Text="現在のレベル"
			VerticalOptions="Center"
			HorizontalOptions="End"/>
		<Entry Grid.Row="1" Grid.Column="1" Margin="5,5,5,5"
			Text="{Binding NowLevel.Value, Mode=TwoWay}"
			WidthRequest="50"
			VerticalOptions="Center"
			HorizontalOptions="Start"/>
		<Label Grid.Row="2" Grid.Column="0" Margin="5,5,5,5"
			Text="目標のレベル"
			VerticalOptions="Center"
			HorizontalOptions="End"/>
		<Entry Grid.Row="2" Grid.Column="1" Margin="5,5,5,5"
			Text="{Binding EndLevel.Value, Mode=TwoWay}"
			WidthRequest="50"
			VerticalOptions="Center"
			HorizontalOptions="Start"/>
		<Button Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2"
			Text="計算！"
			VerticalOptions="Center"
			HorizontalOptions="Center"
			Command="{Binding CalcCommand}"/>
		<Label Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2"
			Text="{Binding OutputMessage.Value, Mode=OneWay}"
			HorizontalOptions="Center"
			VerticalOptions="Start"/>
	</Grid>
</ContentPage>
```

**2018/03/19追記：**
　現在、Xamarin Live Playerで上記コードを実行すると、**[次のようなエラーが出て実行できません](https://twitter.com/YSRKEN/status/975501757349416960)**。

```
Visualization Error
Failed to resolve type {Name = "T";
GenericArgs = [||];
ArrayRank = 0;} (NInterpretException)
```

　[ググっても原因は見つからず](https://forums.xamarin.com/discussion/103330/visualization-error)、私としても困り果てています。Androidエミュレーターでは正常に起動したので、ReactivePropertyとXamarin Live Playerの相性が悪いのだと思わｒ……**エミュも正常に動くんだからXamarin Live Playerのバグに決まってるだろ常考**。
　確かに[Xamarin Live Playerは一部Reflectionが動作しない](https://qiita.com/saken649/items/acf4eb04e475b25b2880#出来ないこと)ですが、ReactiveProperty無しでコードを書くのは今では考えられないので辛いorz

## Step6. ロジックを実装

　後は思い思いにロジックを実装します。今回は必要経験値を計算したいので、エラー対策を含めこんなコードでいいんじゃないでしょうか。

```Library.cs
using System.Collections.Generic;

namespace CalcExpPoint
{
	static class Library
	{
		// 達成に必要な経験値(Lv.1～Lv.165)
		private static List<int> levelExpList = new List<int> {
			0,
			0,
			100,
			300,
			//(中略)
			7320000,
			7820000,
		};
		// あるレベルにおける総経験値を返す
		private static int LevelExp(int level) => levelExpList[level];
		// レベルA→レベルBになるために必要な経験値を返す
		public static int WantExpByLevelDifference(int levelA, int levelB)
			=> LevelExp(levelB) - LevelExp(levelA);
	}
}
```

```MainViewModel.cs
public MainViewModel()
{
	CalcCommand.Subscribe(_ => {
		if(NowLevel.Value <= 0 || NowLevel.Value > 165)
		{
			OutputMessage.Value = "エラー：現在のレベルは1以上165以下の整数で入力してください";
		}else if (EndLevel.Value <= 0 || EndLevel.Value > 165)
		{
			OutputMessage.Value = "エラー：目標のレベルは1以上165以下の整数で入力してください";
		}
		else {
			OutputMessage.Value = $"必要経験値：{Library.WantExpByLevelDifference(NowLevel.Value, EndLevel.Value)}";
		}
	});
}
```

## Step7. 本番ビルド後にそれぞれの環境で動かす

　WPF版はこんな感じ(Windows 10 64bit版の上で動作)。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/2ecf9aa5-7674-d53b-708f-cd8c514f0991.png)

　GTK版はこんな感じ(同上・Monoで動作)。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/9687aab2-91f6-6a57-dfaa-de89ad873643.png)

　Android版はこんな感じ(Nox Player・Android 4.2の上で動作)。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/b499c466-03de-a329-d3aa-c2aa2e0c3577.png)