Xamarin.Forms+F# で Navigation.PushAsync を使うときの注意

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 拡張を作ってから呼び出すといい、というか注意書きということです。

カテゴリー: F#, Xamarin パーマリンク