Xamarin.Forms で、次のページに遷移させるときには NavigationPage クラスを使って置いて、遷移先のページに対して、
await this.Navigation.PushAsync(new SubPage());
なことをやります。NavigationPage を使うと複数ページを簡単に扱えるので、超簡単なスマートフォンアプリを作る分には楽ですよね。テスト的に作るときにいいです。
これを F# でやります。
■サンプルコード
FSharpNavigationPushAsync
http://github.com/moonmile/XamarinSamples/tree/master/FSharpNavigationPushAsync
簡単な遷移だけのサンプルコードです。
iOS/Android/Windows Phone で動くようにしてあります。フロントは面倒なので C# で書いてありますが、中身の PCL のところは F# になっています。フロント部分は、基本そのままなので C# でも良いかと。
以下、F# で Xamarin.Forms を扱う時の手順も含めて書いておきます。
■Xamarin.Forms のプロジェクトを作る
残念ながら F# には、Mobile Apps がないので、C# で作ります。
Xamarin.Forms を使う PCL は Visual Studio 2013 でしか作れません。Xamarin Studio では作れません。これを、元の C# の PCL とすり替えます。
NuGet で Xamarin.Forms を更新しておきます。
各プロジェクトのバージョンが違うと、実行エラーになります。Android/iOS の場合は、特に問題がでないのですが、Windows Phone の場合は実行時にコケルので曲者です。
■F# PCL を standalone でビルドする
F# 単体で動かす場合には、FSharp.Core を気にしなくていいのですが、Xamarin を使って Android/iOS/Windows Phone で共通のライブラリを作るときは、standalone スイッチをつけてビルドをします。
standalone をつけなくても、Android/iOS の場合は大丈夫なのですが、何故か Windows Phone のときだけうまくいきません。FSharp.Core のバージョン違いのようで、Windows Phone SL なので、これは仕方がないっぽいです。
まあ、standalone の注意は、以下を見てください。
恐ろしい standalone オプション – 2つのアンコール
http://hafuu.hatenablog.com/entry/20121214
複雑に絡みあいはじめると、変なことになりそうですが、F# で作った PCL を C# のフロントで使う場合にはこれで十分でしょう。
■Windows Phone の ProductID を直す
Xamarin.Forms のプロジェクトテンプレートのバグです。Windows Phone のプロジェクトの ProductID のところを手作業で直しておきます。Properties/WMAppManifest.xml を Visual Studio で開こうとすると、「マニフェストを読む込むことができません」というエラーがでます。
このまま、XML エディタを開いて、
<App xmlns="" ProductID="f653f4bf-b71d-4572-b073-bc2c48395070" Title="FSharpNavigationPushAsync.WinPhone" RuntimeType="Silverlight" Version="1.0.0.0" Genre="apps.normal" Author="FSharpNavigationPushAsync.WinPhone author" Description="Sample description" Publisher="FSharpNavigationPushAsync.WinPhone" PublisherID="f1f3a5b4-b6e0-4488-9fde-ac0b70a9f79e">
これを、以下のように {} が付くように直します。プロジェクトテンプレートの {} 忘れです。
<App xmlns="" ProductID="{f653f4bf-b71d-4572-b073-bc2c48395070}" Title="FSharpNavigationPushAsync.WinPhone" RuntimeType="Silverlight" Version="1.0.0.0" Genre="apps.normal" Author="FSharpNavigationPushAsync.WinPhone author" Description="Sample description" Publisher="FSharpNavigationPushAsync.WinPhone" PublisherID="{f1f3a5b4-b6e0-4488-9fde-ac0b70a9f79e}">
Visual Studio Mobile Apps – Blank App (Portable/Shared) – Xamarin Forums
http://forums.xamarin.com/discussion/18694/visual-studio-mobile-apps-blank-app-portable-shared
あたりを参考にしてください。
■F#版の App.fs
C# で書いてある、Hello Forms をそのまま F# で書き直すと、こんな感じになります。
type App() = static member GetMainPage() = new ContentPage( Content = new Label( Text = "Hello, Forms !", VerticalOptions = LayoutOptions.CenterAndExpand, HorizontalOptions = LayoutOptions.CenterAndExpand ))
この状態で、Android/iOS/Windows Phoneが動作することを確認してください。
■F#で PushAsync する
遷移先のページを作っておきます。
type SubPage() as this = inherit ContentPage() do this.Content <- new Label( Text="This is SubPage", VerticalOptions = LayoutOptions.CenterAndExpand, HorizontalOptions = LayoutOptions.CenterAndExpand )
遷移元(メインページ)は、こんな感じです。StackLayout.Children のところが入れ子にできないので、C# のように書けないのが残念なのですが(今度、拡張メソッドを作りましょう)、ひとつひとつ組み立てていきます。
type App() = (* static member GetMainPage() = new ContentPage( Content = new Label( Text = "Hello, Forms !", VerticalOptions = LayoutOptions.CenterAndExpand, HorizontalOptions = LayoutOptions.CenterAndExpand )) *) (* let pushNextPage(next) = let AwaitTaskVoid : (Task -> Async<unit>) = Async.AwaitIAsyncResult >> Async.Ignore page.Navigation.PushAsync(next) |> AwaitTaskVoid |> ignore *) static member GetMainPage() = let button = new Button( Text = "Go Next Page") let layout = new StackLayout() layout.Children.Add( new Label( Text = "Navigation.PushAsync")) layout.Children.Add( button ) let page = new ContentPage ( Content = layout ) button.Clicked.Add( fun(_) -> // C# の場合は // await page.Navigation.PushAsync(new SubPage()) // これで動作する /// 1.これで良さそうな気もするのだがこれは動かない // let t = button.Navigation.PushAsync( page ) // t.Start() // t.Wait() /// /// 2.スレッドを変えてみたが、Android/iOS だけ動く // 何故か Windows Phone では動かず // let tk = new Task(fun() -> // let t = page.Navigation.PushAsync(new SubPage()) // t.Start() // ) // tk.RunSynchronously() /// 3. async すると戻り値がvoidなので、ややこしいのだが、 /// この方式だと3プラットフォームで動く /// 参考 http://stackoverflow.com/questions/8022909/how-to-async-awaittask-on-plain-task-not-taskt /// async {} の場合、戻り値を持たない Task が使えないのでこうやるらしい。 /// Page.Navigation.PushAsync が戻り値を持たないのでややこしいだけ。 let AwaitTaskVoid : (Task -> Async<unit>) = Async.AwaitIAsyncResult >> Async.Ignore page.Navigation.PushAsync(new SubPage()) |> AwaitTaskVoid |> ignore ) new NavigationPage(page)
結論から言うと、PushAsync するときに、async の結果を捨てるための補助関数が必要になります。
F# では async/await を書けないので、async {} を使うわけですが、動きが微妙に違うのが曲者です。いや、どちらがいいという訳ではないのですが、一対一にコンバートできません。
/// 1.これで良さそうな気もするのだがこれは動かない let t = button.Navigation.PushAsync( page ) t.Start() t.Wait()
一見、これで動きそうな気もするのですが、iOS/Android の実装が異なるらしく動きません。Windows Phoneのほうも駄目なので、PushAsync が返している Task は別ものっぽいです。
/// 2.スレッドを変えてみたが、Android/iOS だけ動く // 何故か Windows Phone では動かず let tk = new Task(fun() -> let t = page.Navigation.PushAsync(new SubPage()) t.Start() ) tk.RunSynchronously()
描画スレッドの違いがおかしいらしいので、別に Task を作ると動くようになります。最初、Xamarin.Forms.XamlProvider は、この方式でやっていたのですが、昨晩 Windows Phone で動かしてみると実行エラーがでることがわかりました。
/// 3. async すると戻り値がvoidなので、ややこしいのだが、 /// この方式だと3プラットフォームで動く /// 参考 http://stackoverflow.com/questions/8022909/how-to-async-awaittask-on-plain-task-not-taskt /// async {} の場合、戻り値を持たない Task が使えないのでこうやるらしい。 /// Page.Navigation.PushAsync が戻り値を持たないのでややこしいだけ。 let AwaitTaskVoid : (Task -> Async<unit>) = Async.AwaitIAsyncResult >> Async.Ignore page.Navigation.PushAsync(new SubPage()) |> AwaitTaskVoid |> ignore
結果的に async {} に沿う形に直してやると Windows Phone でも動くようになります。async {} の場合は、戻り値を持たない Task を扱うことができないので、AwaitTaskVoid のような補助メソッドを用意してやります。
Async を拡張して、Async.AwaitTaskVoid にすると、それっぽくなります。
module AsyncExtentions = type Async with static member AwaitTaskVoid : (Task -> Async<unit>) = Async.AwaitIAsyncResult >> Async.Ignore open AsyncExtentions ... page.Navigation.PushAsync(new SubPage()) |> Async.AwaitTaskVoid |> ignore
調子に乗って、コンピュテーション式でやろうとして async do! を使うと動かないんですよねー。これは3機種とも動きません。
async { do! page.Navigation.PushAsync(new SubPage()) |> Async.AwaitTaskVoid } |> ignore
というわけで、F# で PushAsync するときは、Async.AwaitIAsyncResult 拡張を作ってから呼び出すといい、というか注意書きということです。