kokoro.ioアプリが引っかかったXamarin.Formsの罠を振り返る (前編)
これはkokoro.io Advent Calendar 2017 2日目の記事です。また謎ブログエンジンのテスト投稿でもあるので、フォーマットがおかしい箇所はsupermomongaに文句を言ってください。
自己紹介
突然supermomongaに「合宿やるぞ!」と呼び出されてkokoro.ioのコントリビューターになったものの、Rubyを触る気は全くなかったのでなし崩しでクライアントアプリとかを書いています。
そもそもXamarinとは
時は20世紀。Microsoft社はまだそれほどネタ臭のしていなかったJavaに自社の既存言語の知見を取り込んで独自の拡張を仕込もうとしました。そしてMicrosoft Java Virtual MachineとJ++言語が出来上がりましたが、これらはJavaの標準に対する攻撃とみなされ訴訟に発展します。結局のところMicrosoftはMSJVMを放棄せざるをえず、.NET FrameworkとC#が生まれることになります。
.NET FrameworkとC#は「人生はC++でプログラミングをするには短すぎる」というスローガンを掲げる奇特なスペイン人の興味を引きました。ミゲル・デ・イカザはMonoと称する黒須プラットフォームの.NET互換ライブラリの開発を行います。最終的にMicrosoftに買収されるまでにMonoの開発会社は4度変わりましたが、Xamarinは3番目にあたります。
Xamarinのプロダクトは次第に成熟し、ついにはXamarin.AndroidやXamarin.iOSといったモバイルスタックのラッパーを提供するまでになります。前置きが長くなりましたがXamarin.FormsはこれらのラッパーおよびMicrosoft純正のWindows Phone向けライブラリの上でC#製のクロスプラットフォームアプリを実現するためのものです。
Xamarin.Formsの基礎知識
Xamarin.Formsは移植性を担保するために、UIコンポーネントを各プラットフォームで共通の抽象的なElement
とプラットフォーム別に実装されているRenderer
に分割しています。例えばLabel
要素を表示する場合は実行する環境によって異なるLabelRenderer
のインスタンスが生成され、iOS用であればCocoa
Touch UITextView
が、UWP向けならTextBlock
がレンダラーによって構成されるといった具合になります。
Element
の主要な派生クラスとしてはView
とPage
、あとCell
があります。Page
の用途は名前から分かると思いますが、UIツリーの中でも最上位に位置しています。Page
には機能に応じた派生型が用意されていますが、基本となるのはContentPage
で他にいくつかのPage
のコンポジションを実現するPage
型があります。
一方ContentPage
は内部にView
を持ちます。View
はページ上に配置されているコントロールに相当し、前述のLabel
の他にはButton
やImage
などの基本的なUIコンポーネントが用意されています。もちろんView
自体を子要素としてもつView
であるLayout
型もあり、要素の配置方法に応じていくつかの派生型が定義されています。
XAML
さてXamarin.Formsでの画面デザインはContentPage
インスタンスにView
を詰めていくことわけですが、もちろんマークアップによる定義方法が用意されています。たとえばkokoro.ioアプリの初期のログインページは
<?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:vm="clr-namespace:KokoroIO.XamarinForms.ViewModels;"
x:Class="KokoroIO.XamarinForms.Views.LoginPage">
<ContentPage.BindingContext>
<vm:LoginViewModel />
</ContentPage.BindingContext>
<ContentPage.Content>
<StackLayout
Spacing="20"
Padding="15">
<Label
Text="Mail Address"
FontSize="Medium" />
<Entry
FontSize="Medium"
Keyboard="Email"
Text="{Binding MailAddress}" />
<Label
Text="Password"
FontSize="Medium" />
<Entry
FontSize="Medium"
IsPassword="True"
Text="{Binding Password}" />
<Button
Text="Log in"
FontSize="Medium"
Command="{Binding LoginCommand}" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
とXMLで記述されています。まずxmlns
群に注目してほしいのですが、これらのXML名前空間はプロジェクトで読み込まれているCLR名前空間に対応しています。たとえば
xmlns:vm="clr-namespace:KokoroIO.XamarinForms.ViewModels"
は現在のアセンブリーに存在するKokoroIO.XamarinForms.ViewModels
名前空間を表しており、後続のマークアップで<vm:LoginViewModel />
のように記述すると該当名前空間に存在するLoginViewModel
のインスタンスが生成される運びとなっております。
つまりLoginPage.xaml
の最上位要素はXamarin.Formsの標準名前空間であるhttp://xamarin.com/schemas/2014/forms
に存在するContentPage
のインスタンスを定義しているわけです。さらにx:Class="KokoroIO.XamarinForms.Views.LoginPage"
とも指定されているので、結局ContentPage
の派生型であるLoginPage
のインスタンスが生成されることになります。
さてクラスはxmlns
と要素名で指定できることが分かりましたが、インスタンスのプロパティはどのようになっているかというと、LoginPage.xaml
では以下の2パターンが出現します。シンプルな方法としては<Label Text="Mail Address" FontSize="Medium" />
とXML属性を指定することができますが、より複雑なオブジェクトに対しては<{クラス}.{プロパティ}>
の要素でツリーを構成することができます。
<ContentPage.BindingContext>
<vm:LoginViewModel />
</ContentPage.BindingContext>
いずれの方式でもプロパティ値としては該当のプロパティに代入できる値の他、マークアップ拡張と呼ばれるインスタンスを使用できます。たとえばText="{Binding MailAddress}"
はこの要素のBindingContext
(=LoginViewModel
)のMailAddress
プロパティとText
プロパティを双方向にデータバインドするという宣言です。
Xamarin.Formsで採用されているこのXAML(eXtensible Application Markup Language)は実際のところXamarin社のオリジナルではなく、MicrosoftのWPF/Silverlight/UWPといった比較的新しいUIフレームワークで採用されたものです。WPFは非常に高機能なスタックでWindows FormsなどのクラシックなMS製RADを利用していた大多数のIT土方には理解不能なほど複雑でしたが、わかる人には快適な環境としてエンスーの支持を受けてきました。WPFの文法とWindows Formsの名前を拝借したXamarin.Formsはどのような塩梅であったか、次回以降kokoro-io-appのコミットログとともに振り返っていくことにしましょう。