Xamarin.iOS+MvvmCrossでstoryboardを使う方法

MvvmCrossでプロジェクトテンプレートを作ると、丁寧な設定があって便利なのですが、Xamarin.iOSで作るときに Xcode5 に対応した storyboard のほうは出してくれません。やり方がよくわからないので、できるのかできないのかすら分からないので、必要があってきちんと調べてみました。

MVVMCross support for Xamarin.iOS Storyboards – Stack Overflow
http://stackoverflow.com/questions/22126929/mvvmcross-support-for-xamarin-ios-storyboards
slodge/eh ・ GitHub
https://github.com/slodge/eh
Databinding ・ MvvmCross/MvvmCross Wiki ・ GitHub
https://github.com/MvvmCross/MvvmCross/wiki/Databinding

なところにサンプルコードがあります。

slodge/eh にあるのは、PCL を使わない方法なので、PCL を使う方法示しておきます。やってることは一緒ですね。

■XamarinMvx.Coreの作成

XamarinMvx.CoreをPCLで作成する。MvxViewModel を継承した ViewModel で、バインディングする View の名前と揃えて置く。実は揃えない方法があるのだけど(iOSの場合は、揃えない方法でバインドする)、Androidと共有することを考えるとそろえておくのが無難。「MainView」にバインドするのであれば「MainViewModel」にしておく。

public class MainViewModel : MvxViewModel
{
    public int _X = 0;
    public int X
    {
        get { return _X; }
        set { _X = value; RaisePropertyChanged(() => X); }
    }
    public int _Y = 0;
    public int Y
    {
        get { return _Y; }
        set { _Y = value; RaisePropertyChanged(() => Y); }
    }
    public int _Ans = 0;
    public int Ans
    {
        get { return _Ans; }
        set { _Ans = value; RaisePropertyChanged(() => Ans); }
    }
}

App.cs はあってもなくてもいい気がするのだが、Android と共有するために以下のままにしておく。

public class App : Cirrious.MvvmCross.ViewModels.MvxApplication
{
    public override void Initialize()
    {
        CreatableTypes()
            .EndingWith("Service")
            .AsInterfaces()
            .RegisterAsLazySingleton();

		// TODO: MainViewModelをMainViewに結び付ける
        RegisterAppStart();
    }
}

RegisterAppStartを使うと、ひとつのViewModelがひとつのViewにしかバインドできないし、じゃあ複数のViewにバインドするときはどうするの?という疑問が残るけど、ここではそのままで。slodge/eh 自体が、複数の画面遷移を使っているので参考になると思う。

■XamarinMvx.iOSの作成

Androidの場合には、*.axmlに

local:MvxBind="Text Hello"

のようにバインドしていくのだが、iOSのstoryboardの場合は、Xcodeが *.storyboard を書き換えてしまうので直接編集すると上書きされて戻ってしまう。なので、コードを使ってバインドをする。
iOS用のプロジェクト(storyboardを使う方)を作った後に、NuGetでMvvmCrossを導入する。

AppDelegate.cs を開いて、AppDelegate の継承を MvxApplicationDelegate に直す。

public partial class AppDelegate : MvxApplicationDelegate

FinishedLaunching メソッドをオーバーライドして、アプリの起動時に setup.Initialize を呼び出すようにする。base.FinishedLaunching を呼び出してはいけない。

public override void FinishedLaunching(UIApplication application)
{
    var setup = new Setup(this, this.Window);
    setup.Initialize();
}

XamarinMvx.iOSViewController.cs(ViewControllerのファイル)を開いて、MvxViewController から継承するように変更する。

public partial class XamarinMvx_iOSViewController : MvxViewController

ViewDidLoad メソッドの最初におまじないの一行(MvxViewModelRequestの呼び出し)を書いた後にバインドする。

public override void ViewDidLoad ()
{
    this.Request = new MvxViewModelRequest(null, null, new MvxRequestedBy());
    base.ViewDidLoad();

	// Perform any additional setup after loading the view, typically from a nib.
    // TODO: こうすると、View, ViewModel の名前の制約を回避できる
    var set = this.CreateBindingSet<XamarinMvx_iOSViewController, MainViewModel>();
    // TODO: コードでバインドする
    set.Bind(this.editX).To(vm => vm.X);
    set.Bind(this.editY).To(vm => vm.Y);
    set.Bind(this.textAns).To(vm => vm.Ans);
    set.Apply();
}

MvxViewModelRequestのテンプレートに渡すのは ViewModel のクラス名。これを Request に設定しておく。この行がないと、base.ViewDidLoad で例外が発生する。
CreateBindingSet では、Viewクラス、ViewModelクラスを設定する。この場合は、Viewクラスが「XamarinMvx_iOSViewController」でViewModelクラスが「MainViewModel」になる。こんな風に、名前をそろえなくて良い。
これを変数で受けて、Bind していく。Apply で複数のバインドを適用する。まあ、大抵は複数のバインドをするのでこれでOK。

ちなみに、Android の場合も同じようにできる。

protected override void OnCreate(Bundle bundle)
{
    base.OnCreate(bundle);

    // Set our view from the "main" layout resource
    SetContentView(Resource.Layout.Main);

    // axml に local:MvxBind="Text X" を書かずにコードでバインドする方法
    // iOSの場合も同じ
    // この方式にすると、ViewとViewModelの名前を統一しなくてよい。
    var set = this.CreateBindingSet<MainView, MainViewModel>();
    set.Bind(FindViewById<EditText>(Resource.Id.editText1)).To(m => m.X);
    set.Bind(FindViewById<EditText>(Resource.Id.editText2)).To(m => m.Y);
    set.Bind(FindViewById<TextView>(Resource.Id.textView1)).To(m => m.Ans);
    set.Apply();
    // TODO: 説明を簡単にするため、テキストだけバインド
    MainViewModel vm = this.ViewModel as MainViewModel;
    Button button = FindViewById<button>(Resource.Id.button1);
    button.Click += (s, e) =>
    {
        vm.Ans = vm.X + vm.Y;
    };
}

*.axmlに書かないので、XAMLとは異なるけどバインド部分は統一できそう。ちなみに、FindViewById<EditText> のところは型指定しないと例外が発生する。

ボタンのクリックイベントは、IBActionを使ったほうが楽なので、こっちで。ICommandのほうはまた後で。

partial void clickCalc(MonoTouch.Foundation.NSObject sender)
{
    var vm = this.ViewModel as MainViewModel;
    vm.Ans = vm.X + vm.Y;
}

■実行

実行するとこんな感じ。

■サンプル

サンプルはこちら。
https://github.com/moonmile/XamarinMvx

ついでに、Android と Windows ストアプリも含めています。

■日本語の参考文献

Xamarin – マルチプラットフォーム MVVMフレームワーク「MvvmCross」を使う – Qiita
http://qiita.com/amay077/items/c4227663b5a5e540dc13
MvvmCross と Xamarin for Visual Studio で iOS, Android, Windows アプリを作る流れ – Yuta Watanabe’s Blog
http://yutawatanabe.hatenablog.com/entry/mvvmcross-xamarin-visual-studio
MvvmCross について – Xamarin 日本語情報
http://ytabuchi.hatenablog.com/entry/2014/03/03/121833

カテゴリー: Android, C#, WinRT, Xamarin, iOS パーマリンク