kokoro.ioアプリが引っかかったXamarin.Formsの罠を振り返る (前編)

by pgrho 2017-12-02 00:39

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の主要な派生クラスとしてはViewPage、あとCellがあります。Pageの用途は名前から分かると思いますが、UIツリーの中でも最上位に位置しています。Pageには機能に応じた派生型が用意されていますが、基本となるのはContentPageで他にいくつかのPageのコンポジションを実現するPage型があります。

Elementクラス図

一方ContentPageは内部にViewを持ちます。Viewはページ上に配置されているコントロールに相当し、前述のLabelの他にはButtonImageなどの基本的な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のコミットログとともに振り返っていくことにしましょう。