Windows Phone
Xamarin と MvvmCross による MVVM アプリのビルド
モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) パターンは、XAML アプリに最適な参照パターンになっています。XAML アプリとは、Windows Presentation Foundation (WPF)、Windows 8、Windows Phone、および Silverlight のアプリです。MVVM パターンは WPF の初期に導入され、懸念事項やテスト容易性などを切り分けます。このパターンが最も優れているのは、XAML を使用しないテクノロジであっても使用できる点にあります。実際、ASP.NET や JavaScript などでもこのパターンを使用できます。
Xamarin は、C# コードで Android や iOS のアプリを開発できるようにします。これらのアプリには固有の開発モデルがありますが、MvvmCross というフレームワークがあれば、これらのプラットフォームでも MVVM パターンを使用できます。今回は、この MvvmCross を理解するのに必要な知識と、Android や iOS のアプリに MvvmCross を使用する方法について説明します。
MVVM の概要
最近では、たくさんの記事で MVVM が扱われているので、ここでは MVVM パターンを簡単に復習するだけにします。要約すると、MVVM は、モデル (画面に表示し操作するデータ)、ビュー (プレゼンテーション コンポーネントや UI)、およびビューモデル (データ バインドを使用して、モデルを取得し、ビューに表示して、ユーザー操作に応答) の 3 つの部分から構成されます。図 1 に示したのは MVVM をグラフィカルに表現したものです。
図 1 MVVM パターンの概要
マイクロソフトのテクノロジを使って開発するときは、MVVM が提供する再利用性を簡単に確認できますが、マイクロソフト以外のテクノロジ、たとえば、Android や iOS の場合はどうでしょう。
もちろん、独自のパターンや手法を実装してもかまいませんが、それではデータ バインドやテストの容易性といった MVVM の優れた特徴を利用できなくなるかもしれません。MVVM の最大のメリットの 1 つは、簡単にテストできるビューモデルです。そのため、クロスプラットフォーム コードをビューモデルに含めることも可能になります。ビューモデルを使用しなければ、クロスプラットフォーム コードは、コントローラーのようなプラットフォーム固有のクラスに含めることになります。
Xamarin と MvvmCross を使用すると、先ほどの疑問に答えることができます。マイクロソフト以外のプラットフォームでも同じ方法で MVVM を使用できるようになります。他のプラットフォームで MVVM を使用する方法を説明する前に、Xamarin について少し触れておきます。
Android/iOS のアプリ向け Xamarin
Xamarin は、すべてのネイティブ API に完全にアクセスできる、パフォーマンスの高いコンパイル済みコードを提供するツール セットです。Xamarin は、デバイス固有のエクスペリエンスを実現するネイティブ アプリケーションを作成できるようにします。Objective-C や Java で実行可能な処理はすべて、Xamarin と C# を使用して実行できます。
アプリの開発には Xamarin Studio を使用しますが、Visual Studio など、C# 開発に使用できる他のツールも使用できます。たとえば、Team Foundation Server (ソース管理用) や、Resharper、GhostDoc のようなプラグインなども使用できます。
開発者から見ると、Xamarin には Xamarin.Mac、Xamarin.iOS (MonoTouch.dll)、および Xamarin.Android (Mono.Android.dll) の 3 つ製品があります。これらはすべて、Microsoft .NET Framework のオープン ソース バージョンである Mono を基盤として開発されています。実際のところ、Mono は当初、Xamarin の共同設立者であり現 CTO の Miguel De Icaza によって作成されたものです。
iOS では、C# で作成されたアプリが専用のコンパイラによってネイティブ ARM コードに直接コンパイルされます。Android の場合は .NET でのコンパイルと実行と同様で、ソースコードが中間言語 (IL) にコンパイルされます。デバイスでコードを実行するときは、2 回目のコンパイル (ジャスト イン タイムで実行) が行われ IL コードがネイティブ コードにコンパイルされます。Android のアプリは Java で開発されるため、この動作が適しています。Java の内部アーキテクチャと、.NET Framework のアーキテクチャはよく似ています。図 2 は、iOS と Android のコンパイル プロセスを図で表現したものです。
図 2 Xamarin によるネイティブ コンパイル
多くの場合、メモリ管理やリソースの割り当てなどを考える必要はありません。これらはすべて、Xamarin が提供するランタイムによって管理されます。ただし、場合によっては、Objective-C との相互運用など、何が行われるかを認識しておく必要があります。これにより、保持サイクルや、マネージ クラスが実際には大量のリソースをラップする状況 (iOS の UIImage など) が生み出される可能性があります。詳細については、bit.ly/1iRCIa2 (英語) を参照してください。
iOS アプリには特別な考慮事項があります。Mac 版の Xamarin Studio には iOS 開発に必要なものがすべて含まれていますが、Windows PC の Visual Studio ユーザーは依然として Xamarin ツールをインストールした Mac が必要です。そのため、ネットワーク経由でアプリをコンパイルし、iOS Simulator や iOS デバイスでテストすることになります。
Xamarin は、C# や F# を使用して iOS と Android のアプリをビルドできるようにしますが、伝統的なモデル - ビュー - コントローラー パターンを使用しません。テストや保守を容易にし、簡単に移植できるようにするには、これらのプラットフォームでも MVVM パターンを使用する方法が必要です。ここで MvvmCross の出番です。
Xamarin アプリ向けの MvvmCross
MvvmCross は、Stuart Lodge が開発したオープン ソースのクロス プラットフォーム MVVM フレームワークで、Windows Phone、Windows 8、iOS、Android、および WPF アプリに使用できます。MvvmCross は、これまで MVVM パターンを使用できなかった iOS や Android などのプラットフォームで、MVVM パターンを使用できるようにします。
ビューでのデータ バインドもサポートします。この強力な機能によって、懸案事項を簡単に分離できます。ビューはビューモデルを使用して、アプリで適切な動作を実現します。MvvmCross は専用プロジェクトのビューモデルを検索して、他のプロジェクトで簡単に参照および再利用できるようにします。
このことは、MvvmCross について説明するうえで最も重要なポイントです。ポータブル クラス ライブラリ (PCL) のビューモデルを検索することで、他のプロジェクトに参照として追加することができます。もちろん、MvvmCross で興味深いのはこれだけではありません。プラグイン アーキテクチャ、依存関係の注入 (DI) なども用意されています。
Android/iOS で MvvmCross を使用する
MvvmCross を使用するのは簡単で、NuGet パッケージをいくつかプロジェクトに追加するだけです。パッケージを追加したら、このアプリケーションを起動する前に実行しておく手順がいくつかあります。この手順は iOS と Android とで若干異なりますが、よく似ています。Core プロジェクトに、ビューモデルと App クラスを含めます。このクラスではサービスを初期化し、起動時に開始するビューモデルを定義します。
public class App : MvxApplication
{
public override void Initialize()
{
this.CreatableTypes()
.EndingWith("Service")
.AsInterfaces()
.RegisterAsLazySingleton();
this.RegisterAppStart<HomeViewModel>();
}
}
iOS や Android のアプリでは、Setup.cs ファイルを作成する必要があります。このファイルは Core プロジェクトを参照し、ランタイムがアプリのインスタンスを作成する方法を認識できるようにします。
public class Setup : MvxAndroidSetup
{
public Setup(Context applicationContext) : base(applicationContext)
{
}
protected override IMvxApplication CreateApp()
{
return new Core.App();
}
}
そのため、setup ファイルは app ファイルを使用して、アプリを作成します。app ファイルは、RegisterAppStart メソッドを使用して起動するときに特定のビューモデルを読み込むようにランタイムに指示します。
ビューはアプリごとに固有になります。iOS と Android で違いがあるのはここでの処理のみです。Android では、クラスは MvxActivity (Activity から継承する Android の標準アクティビティ) から継承します。iOS では、ビューは MvxViewController (UIViewController から継承する iOS の標準 ViewController) から継承します。
[Activity(ScreenOrientation = ScreenOrientation.Portrait)]
public class HomeView : MvxActivity
{
protected override void OnViewModelSet()
{
SetContentView(Resource.Layout.HomeView);
}
}
MvvmCross は、ビューが関連付けられているビューモデルを把握する必要がありますが、既定では名前付け規則によりこれが可能になります。また、ビューの ViewModel プロパティをオーバーライドするか、MvxViewFor 属性を使用すれば、関連付けを簡単に変更できます。
[Activity(ScreenOrientation = ScreenOrientation.Portrait)]
[MvxViewFor(typeof(HomeViewModel))]
public class HomeView : MvxActivity
{ ... }
iOS と Android では、ビューを異なる方法で設計します。iOS では、ビューを C# コードで定義します。Android でも C# コードを使用できますが、AXML 形式 (Android の UI の記述に使用する XML 形式) を使用する方が便利です。iOS と Android のこのような違いにより、各プラットフォームでのデータ バインドの定義も異なります。
iOS では、BindingDescriptionSet を作成して、ビューとビューモデルのリンクを表現します。この BindingDescriptionSet では、バインドを適用する前に、どのコントロールをどのプロパティにバインドするかを指定します。
var label = new UILabel(new RectangleF(10, 10, 300, 40));
Add(label);
var textField = new UITextField(new RectangleF(10, 50, 300, 40));
Add(textField);
var set = this.CreateBindingSet<HomeView,
Core.ViewModels.HomeViewModel>();
set.Bind(label).To(vm => vm.Hello);
set.Bind(textField).To(vm => vm.Hello);
set.Apply();
Android で AXML を使用している場合は、新しい XML 属性の MvxBind を使用して、データ バインドを実行できます。
<TextView xmlns:local="http://schemas.android.com/apk/res-auto"
android:text="Text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tripitem_title"
local:MvxBind="Text Name"
android:gravity="center_vertical"
android:textSize="17dp" />
MvxBind 属性は、バインドするコントロールのプロパティと、ソースとして使用するビューモデルのプロパティを指定するパラメーターを受け取ります。XAMLの 開発者は、MvvmCross の既定のバインド モードが TwoWay であることに注意してください。XAML の既定のバインド モードは OneWay です。MvvmCross フレームワークは、MvxBindingAttributes.xml で使用可能なカスタム XML 属性をいくつか認識します (図 3 参照)。
図 3 MvxBindingAttributes.xml ファイルの内容
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-stylable name="MvxBinding">
<attr name="MvxBind" format="string"/>
<attr name="MvxLang” format="string"/>
</declare-styleable>
<declare-stylable name="MvxControl">
<attr name="MvxTemplate" format="string"/>
</declare-styleable>
<declare-styleable name="MvxListView">
<attr name="MvxItemTemplate" format= "string"/>
<attr name="MvxDropDownItemTemplate" format="string"/>
</declare-stylable>
<item type="id" name="MvxBindingTagUnique">
<declare-styleable name="MvxImageView">
<attr name="MvxSource" format="string"/>
</declare-stylable>
</resources>
このファイルの内容は単純ですが、非常に重要です。このファイルは、AXML ファイルで使用可能な属性を示しています。したがって、バインド操作に MvxBind 属性や MvxLang 属性を使用できることが分かります。また、新しいコントロール (MvxImageView、MvxListView) もいくつか使用できます。コントロールにはそれぞれ専用のカスタム属性があります (図 4 参照)。
図 4 新しいコントロールのカスタム属性
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="2">
<Mvx.MvxListView
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
local:MvxBind="ItemsSource Trips;ItemClick SelectTripCommand"
local:MvxItemTemplate="@layout/tripitemtemplate" />
</LinearLayout>
XAML の開発者には見慣れたものです。ItemsSource プロパティと ItemClick プロパティは、データ ソースの一部のプロパティ (ここではビューモデル) にバインドされています。MvxItemTemplate は、ListView の項目ごとにインターフェイスを定義します。
iOS の構文は大きく異なると想像しているかもしれませんが、実際はよく似ています。AXML ファイルで使用する構文は "Fluent" バインドと呼ばれ、C# バージョンにもマップできる単純なテキスト形式のバインドです。前の例で、一覧の項目を選択するために使用するコマンドは、ICommand オブジェクトです。
public ICommand<Trip> SelectTripCommand { get; set; }
このインターフェイスは、MvvmCross で MvxCommand クラスを使用して次のように実装します。
private void InitializeCommands()
{
this.SelectTripCommand = new MvxCommand<Trip>(
trip => this.ShowViewModel<TripDetailsViewModel>(trip),
trip => this.Trips != null && this.Trips.Any() && trip != null);
}
MVVM パターンの使用時によく起きるのが、型の変換に関する問題です。この問題は、UI が直接利用できない型を使用するプロパティを定義すると発生します。たとえば、バイト配列のイメージ プロパティを、イメージ コントロールの source プロパティに使用することがあります。XAML では、ビューとビューモデルの値をマップする IValueConverter インターフェイスを使用すれば、この問題を解決できます。
MvvmCross にも、IMvxValueConverter インターフェイス、Convert メソッド、ConvertBack メソッドがあるため、プロセスはよく似ています。このインターフェイスを使用して、XAML と同じように変換を実行できます。ただし、それぞれのテクノロジの目的は異なっていることは考慮します。このインターフェイスには XAML と同じ問題があります。パラメーターとしてオブジェクトとして受け取り、それを値として返すため、キャストが必要です。このキャストを最適化するため、MvvmCross には MvxValueConverter ジェネリック クラスが用意されています。このクラスでは、入力の型と戻り値の型のパラメーターを受け取ります。
public class ByteArrayToImageConverter :
MvxValueConverter<byte[], Bitmap>
{
protected override Bitmap Convert(byte[] value, Type targetType,
object parameter, CultureInfo culture)
{
if (value == null)
return null;
var options = new BitmapFactory.Options { InPurgeable = true };
return BitmapFactory.DecodeByteArray(value,
0, value.Length, options);
}
}
コンバーターは簡単に参照できます。iOS では、次のように Fluent 構文で WithConversion メソッドを使用します。
var set = this.CreateBindingSet<HomeView,
Core.ViewModels.HomeViewModel>();
set.Bind(label).To(vm => vm.Trips).WithConversion("ByteArrayToImage");
set.Apply();
Android では、次のように AXML ファイルでコンバーターを直接参照します。
<ImageView
local:MvxBind="Bitmap Image,Converter=ByteArrayToImage"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
コンバーターはリフレクションを使用して名前で検索されます。既定では、フレームワークが、名前に "converter" を含む型を検索します。また、Setup クラスの FillValueConverters メソッドをオーバーライドすることで、コンバーターを手動で登録することもできます。
MvvmCross には、単純かつ軽量の DIcontainer が用意されています。シングルトンとしての登録や動的登録などの複数のパターンを使用して、クラスやインターフェイスをコンテナー内に登録することができます。
Mvx.RegisterType<ISQLiteConnectionFactory, SQLiteConnectionFactory>();
Mvx.RegisterSingletong<ISQLiteConnectionFactory, SQLiteConnectionFactory>();
コンテナー内の型の解決には、2 とおりの方法があります。1 つは、Mvx.Resolve メソッドを使用して、型を明示的に解決する方法です。また、コンストラクターの挿入もサポートします。これにより、MvvmCross を使用してリフレクションを実行し、オブジェクト作成中にパラメーターを自動的に解決できるようになります。
private readonly ISQLiteConnectionFactory _sqlFactory;
public DataAccessLayerService(ISQLiteConnectionFactory sqlFactory)
{
this._sqlFactory = sqlFactory;
}
コンストラクターの挿入は、サービスやビューモデルにも使用できます。Core プロジェクトに配置されるアプリケーション用に開発されたサービスは、すべて既定でプラットフォームを問わずに使用できるため、このことは理解しておく必要があります。
プラットフォームを問わないように見えるものの、プラットフォーム固有の実装を行うサービスを利用することもできます。たとえば、カメラで写真を撮影したり、ユーザーの所在地の座標を取得したり、データベースを使用することができます。コンストラクターの挿入を使用すると、プラットフォーム固有の実装が行われたインターフェイスをビューモデルで受け取ることができます。
このような挿入やプラットフォーム固有のコードのメカニズムを詳細に定義するため、MvvmCross にはプラグイン システムが用意されています。このシステムを使用すると、実行時に新しい機能を作成および挿入できます。各プラグインはインターフェイスによって公開されるサービスで、プラットフォームごとに具体的な実装を用意します。プラグイン システムはインターフェイスと実装を登録します。アプリケーションは DI を使ってプラグインを利用します。また、プラグインの開発者は DI を使用することで、テスト中や開発中に簡易 "モック" 実装を提供できます。
開発者から見ると、プラグインの作成はインターフェイスの作成と同じくらい簡単です。インターフェイスとプラグインのローダーを実装するクラスを作成します。プラグイン ローダーは、IMvxPluginLoader インターフェイスを実装するクラスです。EnsureLoaded メソッドが呼び出されると、(Mvx.RegisterType を使用して) プラグイン インターフェイスと実装が登録されます。
既にたくさんのプラグインが公開されています。これらのプラグインによって、ファイル アクセス、電子メール、JSON 変換などの機能が実現されます。これらの機能のほとんどは NuGet を使用して確認できます。プラグインによっては、すべてのプラットフォームの実装を含まないものもあるので注意が必要です。プラグインの使用時には、このようなプラグインの詳細に注意を払う必要があります。プラグインが目的のプラットフォームをサポートしていない場合でも、新しいプラグインを独自に作成するよりも、パターンに従って、サポート対象外のプラットフォームを実装する方が簡単な場合があります。この場合は、他のユーザーもその実装を使用できるように、使用した実装をプラグインの所有者に提供することを検討してください。
MvvmCross は有用なフレームワークです。モバイル アプリケーション (Android や iOS で実行するアプリも含む) の開発時には使用することを検討してください。MVVM パターンをデータ バインドやプラグインと併用することで、保守がきわめて容易で、移植可能なコードを作成できる強力なシステムとなります。
Thomas Lebrun は、Infinite Square のコンサルタントです。同社は、フランスでのマイクロソフトのパートナーで Windows 8、Windows Phone、Windows Presentation Foundation (WPF)、Silverlight、Surface などのテクノロジに取り組んでいます。彼は、WPF と MVVM パターンに関する書籍を 2 冊執筆しており、コミュニティ イベントで定期的に講演しています。blog.thomaslebrun.net (英語) で公開されている彼のブログを読んだり、Twitter (twitter.com/thomas_lebrun、英語) で彼をフォローしたりすることができます。
この記事のレビューに協力してくれたマイクロソフト技術スタッフの Jared Bienz に心より感謝いたします。