ちょうど Xamarin.iOS のアップデートがあって、Xamarin.iOS10 と monotouch が混乱している状態で苦労したのですが、一応動くところまでできたので記事を書いておきます。
目標
Xamarin.iOS(storyboard) と MvvmCross と Xamarin.Froms を混在させたアプリを作ります。単純に混在させるというよりも、アプリの歴史的な経緯から、
- Xamarin.iOS で storyboard で作っているアプリがあって、
- 去年あたりに MvvmCross を使って、MVVM 対応したアプリになって、
- 今年ぐらいから、Xamarin.Forms に対応したいけどどうしようか?
のようなストーリーを考えています。まあ、MVVM 対応するのは MvvmCross でもよいし、MvvmLight でも Prism でも良いわけですが、そこに Xamarin.Froms の XAML をどうねじ込むか、ってのが問題になりますよね。最初から、Xamarin.Forms で作り直してしまう方法もあるけど労力的に大変だし、そもそも Xamarin.Forms のコントロールは非力なので、そのまま移植できないパターンも多い。DepnencyService とか作ればいいけど、面倒だったら、もともと storyboard と Xamarin.iOS の組み合わせで数ページだけ作るのが簡単ではないか?というパターンです。
アプリの想定
こんな風に、Master-Detail で作っていたアプリに対して、MvvvmCross や Xamarin.Forms のページを追加していきます。

これによって、既存のページはそのままにして、新しいページを MvvmCross や Xamarin.Forms で作れればよいかなと。
プロジェクト構成

storyboard を含むのが、MvxXForms.UI.Touch プロジェクトで Detail ページ用にそれぞれのクラスを設定しています。Mater-Detail の Master がリストの場合は詳細ページは同じページを使うことが多いのですが、Mater が固定ページ(メニューページの代わり)に使っている場合には、項目をクリックしたときにそれぞれの詳細ページが表示されるので、こういう構成にしてあります。Master ページにボタンを並べて画面遷移する場合も似た感じになります。
Xamarin.iOS+storybardのみ場合
最初は、MvxXForms.UI.Touch プロジェクト のようなプロジェクトがあって、まだ MVVM 化されていない状態を考えます。

storyboard は、
– Navigation Controller
– Master ページ
– オレンジ色の Detail ページ
だけの状態になります。Master から Detail へ遷移させる場合は、
Creating an Unwind Segue | Xamarin
http://developer.xamarin.com/recipes/ios/general/storyboard/unwind_segue/
な感じで Ctrl キーを押しながらマウスのドラッグで線が引けます。
サンプルコードでは、RowSelected 内でデータを引き渡すために小細工をしていますが、storyboard segue を使えば画面遷移だけならばノンコーディングでいけます。
MvvmCross のページを追加する
MVVM 化するために、MvxXForms.Core プロジェクトを追加します。ViewModel 自体は、先の MvxXForms.UI.Touch に追加してしまってもよいのですが、先行き Android と共有させることを考えて PCL プロジェクトで作っておきます。
int と string を持つ TipViewModel クラスを定義して、
public class TipViewModel : MvxViewModel
{
public TipViewModel()
{
}
int _pageNum;
public int PageNum
{
get { return _pageNum; }
set { _pageNum = value; RaisePropertyChanged(() => PageNum); }
}
string _Name;
public string Name
{
get { return _Name; }
set { _Name = value; RaisePropertyChanged(() => Name); }
}
}
中身が空っぽな App クラスを作っておきます。App クラスを MvxXForms.UI.Touch プロジェクトから参照しなければよいのですが、まあ、これは初期化のためのお約束コードということで。
public class App : MvxApplication
{
public App()
{
}
}
MvxXForms.UI.Touch プロジェクトに戻って、UIApplicationDelegate を MvxApplicationDelegate に変更。
FinishedLaunching メソッドをオーバーライドして、MvvmCross の初期化を行います。
public partial class AppDelegate : MvxApplicationDelegate
{
// class-level declarations
public override UIWindow Window
{
get;
set;
}
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
var presenter = new MvxTouchViewPresenter(this, Window);
var setup = new Setup(this, presenter);
setup.Initialize();
return true;
}
}
あとは、Detail ページに対応する ViewController を MvxViewController から継承させて、set.Bind 等でバインドを行えば ok です。
public partial class Detail2ViewController : MvxViewController
{
public Detail2ViewController(IntPtr handle)
: base(handle)
{
}
public new TipViewModel ViewModel
{
get { return (TipViewModel)base.ViewModel; }
set { base.ViewModel = value; }
}
public override void DidReceiveMemoryWarning()
{
// Releases the view if it doesn't have a superview.
base.DidReceiveMemoryWarning();
// Release any cached data, images, etc that aren't in use.
}
public override void ViewDidLoad()
{
this.Request = new MvxViewModelRequest(typeof(TipViewModel), null, null, new MvxRequestedBy());
base.ViewDidLoad();
// Perform any additional setup after loading the view, typically from a nib.
var set = this.CreateBindingSet<Detail2ViewController, TipViewModel>();
set.Bind(labelPageNum).To(vm => vm.PageNum);
set.Bind(labelName).To(vm => vm.Name);
set.Apply();
// マスターからのデータ引き渡し
this.ViewModel = MasterViewController._datavm;
}
}
storyboard の Detail ページと Detail2ViewController の結び付けは、プロパティウィンドウで Class を変更します。このあたりは Xcode と同じですね。

MasterViewController._datavm なところは、storyboard segue を使うと、内部的に一気に ViewController が作られてまうので、ViewModel プロパティを設定するタイミングがないため、こうやっています。Master ページで Cell をクリックしたときに、下記な方法でグローバル変数で渡します。ちょっとダサいんですが、仕方がありません。
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
var data = new MyData();
switch ( indexPath.Row )
{
...
case 1:
/// MvvmCross を使って vm 経由でデータを渡す
/// 本来は data 経由のほうがいいけど、これはサンプルで
/// あらかじめ storyboard segue でつなげておく
_datavm = new TipViewModel()
{
PageNum = 2,
Name = "use MvvmCross"
};
break;
...
}
storyboard segue を使わずに画面遷移をする
じゃあ、明示的に遷移先の ViewController を作って ViewModel を設定する方法でもよいだろう、というのが次の方法です。
目的の ViewController の「storyboard id」に、あらかじめ「Detail3ViewController」という名前を付けておいて(これはクラス名と異なっていても構いません)、Storyboard.InstantiateViewController メソッドで作成します。これを、NavigationController.ShowViewController メソッドで表示すれば ok です。
/// <summary>
/// 行をクリックしたとき
/// </summary>
/// <param name="tableView"></param>
/// <param name="indexPath"></param>
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
var data = new MyData();
switch ( indexPath.Row )
{
...
case 2:
/// storyboard のページを直接開く
/// Storyboard ID を ViewController に設定しておく
/// storyboard segue を使わないパターン
var vc = (Detail3ViewController)Storyboard.InstantiateViewController("Detail3ViewController");
this.NavigationController.ShowViewController(vc, this);
_datavm2 = new TipViewModel2()
{
PageNum = 3,
Name = "Mvx + direct storyboard"
};
break;
遷移先の ViewController を MvxViewController を継承するようにして、ViewModel に対応させてもそのまま使えます。
ちょっと注意しなければいけないのは、MvvmCross では ViewModel と ViewController が 1対1 じゃないと駄目なようです。実行時に TipViewModel が二つ以上の ViewController に設定されている、とエラーがでます。なので、仕方がないので TipViewModel2 という同じ中身のクラス(継承しているだけ)を使っているのですが。同じ ViewModel を複数の View に対応しても良いと思うのですが、ちょっとこの動きはよくわかりません。
Xamrin.Forms のページを呼び出す
Xamarin.Forms の XAML ページを Master ページから呼び出せるようにします。
MvxXForms.Form プロジェクトを別に作っていますが、たぶん、MvxXForms.UI.Touch に含ませてしまっても大丈夫だと思います。
DetailXFPage.xaml の中身を手書きします(Xamarin Studio を使うと、少しだけコード補完が効いて楽です)。
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MvxXForms.Form.DetailXFPage"
Title="xamarin.forms page"
BackgroundColor="Yellow"
>
<StackLayout Padding="10,80,10,10">
<Label x:Name="label1" Text="Xamarin.Forms page" />
<Label Text="PageNum" />
<Label Text="{Binding PageNum, StringFormat='{0}'}" BackgroundColor="Lime" />
<Label Text="Name" />
<Label Text="{Binding Name}" BackgroundColor="Lime"/>
</StackLayout>
</ContentPage>
Binding が XAML の中に記述できます。
Xamarin.Forms のプロジェクトでも App クラスがあるのですが、これは GetMainPage メソッドのように PCL プロジェクト内で作成した Page オブジェクトを返すための static メソッドです。なので、同じように、DetailXFPage を new して返すだけのメソッドを作っておきます。
public class App
{
public static Page GetDetailPage()
{
return new DetailXFPage();
}
}
Xamarin.Forms のページを呼び出すときは、先の storyboard segue を使わない方法と同じように、NavigationController.ShowViewController メソッドを使います。
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
var data = new MyData();
switch ( indexPath.Row )
{
...
case 3:
// Xamarin.Forms ページを開く
var page = MvxXForms.Form.App.GetDetailPage();
var vc2 = page.CreateViewController();
var vm = new TipViewModel()
{
PageNum = 4,
Name = "xamarin froms page"
};
page.BindingContext = vm;
this.NavigationController.ShowViewController(vc2, this);
break;
ContentPage の BindingContext プロパティに ViewModel のデータを設定すればバインドが完了します。
Xamarin.Forms の初期化のために、AppDelegate クラスに Forms.Init() を追加しておきます。
これで、Xamarin.Forms と MvvmCross が混在できます。
public partial class AppDelegate : MvxApplicationDelegate
{
// class-level declarations
public override UIWindow Window
{
get;
set;
}
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
Forms.Init();
var presenter = new MvxTouchViewPresenter(this, Window);
var setup = new Setup(this, presenter);
setup.Initialize();
return true;
}
}
実行してみる

こんな風に、Master のページから各種のページに遷移ができます。
サンプルコード
MxSingleApp の中の MixMvxForms
https://github.com/moonmile/MxSingleApp
参考先
Xamarin.iOS ナビゲーションコントローラ – SIN@SAPPOROWORKSの覚書
http://furuya02.hatenablog.com/entry/2014/07/03/035352
Xamarin.iOSでStoryboardとXamarin.Formsを併用するには? – Build Insider
http://www.buildinsider.net/mobile/xamarintips/0006