Xamarin.Forms の TypeProvider を作ろうとしたが断念したの巻

先日の F# Meet up であった TypeProvider の資料をもとに、Xamarin.Forms 用の TypeProvider を作ろうとしたのですが、ちょっと挫折…の記録です。

TypeProviderについて、勝手に補足 – ぐるぐる~
http://bleis-tift.hatenablog.com/entry/kos59125-typeprovider
type-providers.pptx – Microsoft PowerPoint Online
http://onedrive.live.com/view.aspx?resid=FD448A567D4BC37E!5132&ithint=file%2cpptx&app=PowerPoint&authkey=!ADhuGqIaXBhs0ak

TypeProvider 自体が悪いのではなくて、Xamarin.Forms が PCL で提供しているのと、TypeProvider 自体がネイティブ環境(Windows環境で動く)のと関係で、断念しています。何か回避策があったら教えてください~。

■XFormsPreviewer から移植する

もともと、TypeProvider もどきのロジックはあって、以下の中にある XFormsProvider がそれです。動的に XAML ファイルをロードして Xamarin.Forms.Page を返します。

XFormsPreviewer
http://github.com/moonmile/XFormsPreviewer

XAML に書かれている x:Name 部分は、FindByName メソッドで取り出しができるので実用的には問題ないのですが、型が決まっていないのがアレだし、いちいち FindByName で取り出すのも変な感じです。なので、TypeProvider の作り方がわかったら、いずれ移行する予定にしていました。

■ざっと移植する

XamarinFormsTypeProvider
http://github.com/moonmile/XamarinFormsTypeProvider

それなりに苦労しましたが1日ちょっとで移植できました。プロパティを順々に生成するところと、内部のデータの保持が結構肝なのですが、<a href=”https://github.com/fsprojects/FsXaml”>FsXaml</a> を参考にして作っています。

type MainPage = Moonmile.XamarinFormsTypeProvider.XAML<"MainPage.xaml">

こんな感じで、XAML のファイルを指定することで、MainPage というクラスが生成すると、結構「おおおッ」ってな感じになります。ファイルの更新を監視していない(FsXamlは監視してます)ので、先の XAML ファイル名の部分をちょっといじる(コメントアウトして元に戻すとか)して、再生成させると x:Name で指定した名前がそのままプロパティになります。
テスト用に Literal な XAML 文字列も XamarinFormsTypeProvider.XAML に渡すこともできます。

この時点で、結構動いて Android エミュレータで画面が表示できるところまで出来たので、これはいけるなーと思ったのです。ラベルの表示もできて、Text プロパティで変更できるところまで確認島した。

■Clicked.Add すると「静的リンクエラー」になる。

お次は、ボタンイベントを作ろうとして、

type MainPageEx(target:MainPage) =
    let mutable count = 0
    do
        target.btn1.Clicked.Add( fun e ->
            count <- count + 1
            target.btn1.Text <- "Clicked " + count.ToString())
    member this.CurrentPage
        with get() = target.CurrentPage

な感じで、Clicked イベントをつけてビルドしようとすると、

FSC: エラー FS2024: 静的リンクでは、別のプロファイルを対象にしたアセンブリは使用されない場合があります。
プロジェクト "SimpleEventTypeLocalLib.fsproj" のビルドが終了しました -- 失敗。

というエラーメッセージがでビルドができません。このエラーがどういう意味なのか分からなくて、1日ほど悩みました。TypeProvider のプロジェクトは、F# ライブラリで作ってあり、MainPage のプロジェクトも F# ライブラリで作ってあります。本当は PCL で作りたかったのですが、TypeProvider のクラス自体が PCL に対応していません。System.Reflection.Emit.TypeBuilder が potable library のほうにはないのです。

タイプライブラリを作るときは Windows 上で動くけど、生成されたクラスは Android 上で動くわけだから、参照されるライブラリ自体が異なるんですよね。動作環境も異なる。

■FS2024 のエラー

Static linking PCL assembly with mscorlib reference ・ Issue #224 ・ fsharp/fsharp
http://github.com/fsharp/fsharp/issues/224
Consider merging this packaging repository with the contribution repository ・ Issue #303 ・ fsharp/fsharp
http://github.com/fsharp/fsharp/issues/303#issuecomment-39557870

似たパターンがあって、どうやら直っているらしいんだけど、私の環境だといまだに静的リンクエラーが出てます。たぶん原因が違うのかなと思って、実験用のサンプルコードを書きました。

■SimpleEventTypeProvider

moonmile/SimpleEventTypeProvider
http://github.com/moonmile/SimpleEventTypeProvider

プロジェクトの構造がややこしいですが、こんな感じです。

  • SimplePclTypeProvider タイププロバイダ本体
  • XamlPcl プリミティブなクラスのみ使ったイベント処理
  • XamlPclXamarin Xamarin.Forms を使ったイベント処理
  • SimpleEventTypeLib PCL で作ったライブラリ
  • SimpleEventTypeLocalLib Library で作ったライブラリ

タイププロバイダ本体は、こんなコードになります。

namespace Moonmile.FSharp.Lib
open System
open Microsoft.FSharp.Core.CompilerServices
open ProviderImplementation.ProvidedTypes
open System.Reflection

[<assembly:TypeProviderAssembly>]
do ()

type MyButton() =

    let event1 = new Event<_>()
    [<CLIEvent>]
    member this.Click = event1.Publish
    member this.ClickEvent(arg) =
        event1.Trigger(this, arg)    

[<TypeProvider>]
type SimpleEventType(config:TypeProviderConfig) as this = 
    inherit TypeProviderForNamespaces()
    let namespaceName = "Moonmile.SimpleEventTypeProvider" 
    let thisAssembly = Assembly.GetExecutingAssembly()


    /// 型生成を残す場合
    /// [<Litelal>]
    /// let xaml = "<ContentPage>...</ContentPage>"
    /// type MainPage = SimpleEventTypeProvider.XAML< xaml >
    // 型の定義
    let t = ProvidedTypeDefinition(thisAssembly, namespaceName, "XAML", Some(typeof<obj>), IsErased = false )
    do t.DefineStaticParameters(
        [ProvidedStaticParameter("xaml", typeof<string>)],
        fun typeName parameterValues -> 

            let outerType = 
                ProvidedTypeDefinition (thisAssembly, namespaceName, 
                    typeName, Some(typeof<obj>), IsErased = false )
            // テンポラリアセンブリに出力
            let tempAssembly = ProvidedAssembly(System.IO.Path.ChangeExtension(System.IO.Path.GetTempFileName(), ".dll"))
            tempAssembly.AddTypes <| [ outerType ]

            // コンストラクタの生成
            let ctor = ProvidedConstructor([], 
                            InvokeCode = fun args -> <@@ () @@> )
            do outerType.AddMember( ctor )

            // プロパティを追加
            let prop = 
                ProvidedProperty( "Name", typeof<string>, 
                    GetterCode = fun args -> <@@ "masuda tomoaki" @@> )
            do outerType.AddMember( prop )

            // ボタンを追加
            let propButotn = 
                ProvidedProperty( "Button", typeof<MyButton>, 
                    GetterCode = fun args -> 
                        <@@  
                            // let me = %%(args.[0]):obj
                            new MyButton()
                        @@> )
            do outerType.AddMember( propButotn )
            (*
            // 直接参照しても駄目
            let propXButton =
                ProvidedProperty( "XButton", typeof<Xamarin.Forms.Button>, 
                    GetterCode = fun args -> 
                        <@@  
                            // let me = %%(args.[0]):obj
                            new Xamarin.Forms.Button()
                        @@> )
            do outerType.AddMember( propXButton )
            *)


            let propXamlPcl =
                ProvidedProperty( "XmlPCL", typeof<XamlPcl.XamlPage>, 
                    GetterCode = fun args -> 
                        <@@  
                            // let me = %%(args.[0]):obj
                            new XamlPcl.XamlPage()
                        @@> )
            do outerType.AddMember( propXamlPcl )
            /// Xamarin.Forms 関連を PCL 外出しにしても駄目    
            let propXamarinPcl =
                ProvidedProperty( "XmlXamarinPCL", typeof< XamlPclXamarin.XamarinButton >, 
                    GetterCode = fun args -> 
                        <@@  
                            // let me = %%(args.[0]):obj
                            new XamlPclXamarin.XamarinButton()
                        @@> )
            do outerType.AddMember( propXamarinPcl )

            outerType
    )
    // 名前空間に型を追加
    do this.AddNamespace( namespaceName, [t] )

    // Xamarin.Forms.Core 用に追加
    override this.ResolveAssembly(args) = 
        let name = System.Reflection.AssemblyName(args.Name)
        let existingAssembly = 
            System.AppDomain.CurrentDomain.GetAssemblies()
            |> Seq.tryFind(fun a -> System.Reflection.AssemblyName.ReferenceMatchesDefinition(name, a.GetName()))
        match existingAssembly with
        | Some a -> a
        | None -> 
            // Fallback to default behavior
            base.ResolveAssembly(args)

ResolveAssembly がオーバーライドされているのは、タイププロバイダを利用するプロジェクト(SimpleEventTypeLocalLib)がビルドをするときに、Xamarin.Forms.Core を要求するためにこうしています。他にも別なのを要求してるのですが、まあ、こうやっておくと同じフォルダにある DLL を読み込んでくれます。

呼び出し側のコードはこんな感じです。Xamarin.Forms.Color.Black のようなプロパティの設定はうまくいくのですが、target.XButton.Clicked を呼び出した途端に「静的リンクエラー」になります。逆に言えば、Clicked を使わない限りは、ビルドが正常に通ります。

namespace SimpleEventTypeLocalLib

type MainPage = Moonmile.SimpleEventTypeProvider.XAML<"MainPage.xaml">

type MainPageEx(target:MainPage) =
    let mutable Name = ""
    
    do
       Name <- target.Name
       target.Button.Click |> Event.add( fun e -> ())  
       // Xamarin.Forms.Core が動的ロードされているので、静的リンクエラーになる
       // target.XButton.Clicked |> Event.add( fun e -> ())  
       (*
       let col = target.XButton.BackgroundColor
       let a = col.A
       target.XButton.BackgroundColor <- Xamarin.Forms.Color.Black
       target.XButton.Clicked |> Event.add( fun e -> ())
       *)
       let pcl = target.XmlPCL.Xaml
       target.XmlPCL.Click |> Event.add( fun e -> ())

       let xpcl = target.XmlXamarinPCL
       xpcl.Text <- "test"
       // この時点で、Xamarin.Forms の型が参照されて静的リンクエラーになる
       xpcl.Clicked |> Event.add( fun e ->())
       ()

■アセンブリを動的生成すると Native から Portable ライブラリが参照できない???

Static linking PCL assembly with mscorlib reference ・ Issue #224 ・ fsharp/fsharp
http://github.com/fsharp/fsharp/issues/224

の最初のコメントにあるコードを見ていくと

http://github.com/TIHan/fsharp/blob/master/src/fsharp/fsc.fs#L1656

error を出しているところがあります。これはオープンソース版ですが、行数は違いますが同じチェックがあります。

              // Rewrite type and assembly references
              let ilxMainModule =
                  let isMscorlib = ilGlobals.primaryAssemblyName = PrimaryAssembly.Mscorlib.Name
                  let validateTargetPlatform (scopeRef : ILScopeRef) = 
                      let name = getNameOfScopeRef scopeRef
                      if (isMscorlib && name = PrimaryAssembly.DotNetCore.Name) || (not isMscorlib && name = PrimaryAssembly.Mscorlib.Name) then
                          error (Error(FSComp.SR.fscStaticLinkingNoProfileMismatches(), rangeCmdArgs))
                      scopeRef
                  let rewriteAssemblyRefsToMatchLibraries = NormalizeAssemblyRefs tcImports
                  Morphs.morphILTypeRefsInILModuleMemoized ilGlobals (Morphs.morphILScopeRefsInILTypeRef (validateTargetPlatform >> rewriteExternalRefsToLocalRefs >> rewriteAssemblyRefsToMatchLibraries)) ilxMainModule

アセンブリを書き込むとき(ビルドするとき?)に .NETCore や mscorlib の整合性をチェックしているので、これにひっかかっているのかもしれません。

図解したように、出来るだけ遠くに(苦笑)Xamarin.Forms の PCL を置いて、直接 Android のほうから呼び出してやればうまくいくかもしれません。少なくとも、F# で作成するコードビハイドのような SimpleEventTypeLocalLib からは Clicked のような PCL の型を呼び出そうとすると「静的リンクエラー」になります。

まあ、コードビハイドはやめて、MVVM にして ICommand オンリーにすれば通るのですが…ちょっと面白くない。せっかくだから、Clicked のままやりたいのです。そうすると、内部的にリフレクションを使って iPhone の TouchUpInside も取れるのですがね。ムズイ。

コンパイラを直してしまう方法も考えたのですが、タイププロバイダ部分のビルドならともかく、コードビハイド部分の生成にオレオレコンパイラを使うのもちょっと難点が多いのでパスです。先のバグが直ったのか直ってないのかわかりませんが、現状の最新を取ってきても同じ現象になります。

にしても、最初の TypeProvider の作成で1日以内にできたのは、F# meet up のおかげです。感謝。

カテゴリー: F#, Xamarin | Xamarin.Forms の TypeProvider を作ろうとしたが断念したの巻 はコメントを受け付けていません

既存のツールを Windows 8.1 のスタート画面に表示させる方法

デスクトップ PC で Windows 8.1 を使っているときスタート画面は、ほとんど使いません。というか、Win キーでスタート画面を開いた直後に、アプリの名前をキーボードで直接打って、目的のアプリを起動させています。まあ、Surface のようなタブレットの場合には、指でぽちぽちやるんでしょうけど、キーボードが付いている場合はこれが一番早いです。

こんな風に「vi」と打つと、アプリ名が補間されて「Visual Studio 2013」が起動できます。

image

この検索されるアプリ名ですが、前方一致なのか部分一致なのか、微妙なところでよくわかりません。たぶん、単語単位の前方一致になっているハズです。なので「i_view32」は、アンダーバーが単語の区切りとして認識されて、i_view32 としてマッチしているようです。

なので、うまく検索に出てこないときは、ショートカット名に適当なコマンド名を単語単位でつけておくとよいでしょう。たとえば、コマンドラインで C# をビルドするための「開発者コマンドプロンプト for VS2013」はよく動かす(私だけだと思うけど)のですが、「cmd」でマッチさせるために、ショートカットの名前を変更しています。まあ、自分好みのショートカット名にしてもよいわけですね。

image

■既存のアプリをスタート画面に出るようにする

さて、スタート画面からキーボードで直接アプリを起動できることは分かったのですが、そもそも既存のアプリをスタート画面に表示する場合にはどうするのか?ってのが疑問です。うまくインストールされているものは、スタート画面にアイコンが表示されるのですが、Vector などからダウンロードするインストーラー無しのツール(exe だけコピーするもの)は、うまいこと出ません。

仕方がないので、デスクトップにショートカットをつけて凌いでいたのですが、ふと「既にあるアイコンと同じ場所に置けばよかろう」と思い至りました。いや、探してみれば、フォルダ名はわかるはずなのですが、これが面倒臭いので。

image

こんな感じに、既にアイコンが出ているアプリを右クリックして、マウスで「ファイルの場所を開く」を選択します。ここでは、Firefox を選択していますが、Windows ストア アプリ以外ならば何でもいいです。

そうすると、「C:ProgramDataMicrosoftWindowsStart MenuPrograms」というフォルダが開かれます。この中身を検索しているっぽいので、ここに既存のアプリのショートカットを突っ込めば OK ですね。

image

常用している「QX」と「QTClick」を入れておきます。スタートアップに入れると、Windows 7 の頃のように PC の起動時に読み込まれます…が、あまりお勧めできません。Windows 8 の起動の速さの一因は「スタートアップに何もいれない」からってのもあるので。なんだかなー。

image

こうすると、スタート画面に「新しいアプリがインストールされました」ってのが出るので、スタート画面にピン留めしたり、スタート画面から直接検索したりできます。

image

適当なバッチファイルとかおくと、バッチを動かすのもスタート画面でできますね…って、それはデスクトップでやればいい話ですが。

カテゴリー: windows 8.1 | 既存のツールを Windows 8.1 のスタート画面に表示させる方法 はコメントを受け付けていません

F# で Windows.UI.Xaml のクラスをリフレクションを使ってラップして Windows ストア アプリ作る試み

今、手元で作っている WinXamlProvider は、Windows Store 8.1 と Windows Phone 8.1 で使っている XAML を F# で動かそうという試みのひとつです。

WinXamlProvider
http://github.com/moonmile/WinXamlProvider

Windows Store アプリを作る F# プロジェクトが Microsoft から提供されればいいのですが、そんな雰囲気もない。正当な方法としては、自前で F# プロジェクトのテンプレートを作ればよいのですが、どうやら *.target のほうを大きく変えないと駄目っぽくて、頓挫中です。おそらく MSBuild あたりを詳細に調べないとうまくいかなそうなのと、XAML ファイルから C#/VB/C++ コードに落としている箇所に、うまく F# を追加しないといけません。MVVM パターンのみでやるか、ここでやっているように実行時にコードビハイドをバインディングする方法をとれば、ビルド時に XAML からコードビハイドを出力する必要はありません。あるいは、F# の Type Provider を作って、ビルド時に静的に作る方法でもよいでしょう。WPF の XAML 用の Type Provider は FsXaml
https://github.com/fsprojects/FsXaml を使えばよいんですが、WinStore 用の XAML がありません。なぜ、WinStore の XAML に私がこだわるのかといえば、Surface RT のような WinRT タブレットでの使い手を想定しているためです。ええ、ユニバーサルアプリにして Windows Phone 8.1 でもうまく動くようになったのは「おまけ」ですから。

■プロジェクトを分ける

現状では、WinStore と WinPhone ではメインプロジェクトに F# を据えることが難しいので、

  • フロントエンドを C# プロジェクトで作る
  • バックエンドを F# プロジェクトで作る

ということにします。この方式は、Xamarin.Forms を使って iOS/Android を作るときと同じで、バックエンドのロジック部分をライブラリ化して、可搬性を高めるという方式です。ただし、バックエンド部分が PCL(Portable Class Library) になってしまうので、使えるライブラリに制限がでてきます。

20140729_01

こんな風に、C# プロジェクトでは簡単に参照できる Windows.Xaml.UI.* も、PCL の中ではできません。
直接 F# プロジェクトのファイルを開いて、ターゲットの書き換えと、

    <TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
    <TargetFrameworkProfile>Profile32</TargetFrameworkProfile>

ターゲットプラットフォームに Windows, Version=8.1 を無理やり追加すると Windows.UI.Xaml の namespace は参照できるようになるのですが、

  <ItemGroup>
    <TargetPlatform Include="Windows, Version=8.1" />
    <TargetPlatform Include="WindowsPhoneApp, Version=8.1" />
    <Compile Include="Class1.fs" />
  </ItemGroup>

実行時に

{"ファイルまたはアセンブリ 'Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null'、またはその依存関係の 1 つが読み込めませんでした。指定されたファイルが見つかりません。":"Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null"}

という謎なエラーを出力します。確かに、無理に追加しただけなので、ロード自体がうまくいっていないっぽいのです。ここは別途修正するとうまくいのかも。

実験したプロジェクトは https://github.com/moonmile/FsApp です。

■Windows.UI.Xaml をラッピングする

PCL から直接 Windows.UI.Xaml を扱うことはできませんが、リフレクションを使うとアクセスができます。

既にフロントのほうでは、Windwos.UI.Xaml に必要なアセンブリはロードされているハズですから、それめがけてちまちまとインターフェースを作っていく方法ですね。

この方法自体は、Xamarin.Forms の拡張レンダラーとか、MvvmCross のサービスの作り方だとかと似た感じで、Xamarin.Forms や MvvmCross の場合は、適当なインターフェースを使って呼び出すようにしていますが、面倒なので、直接リフレクション呼び出しをしています。インターフェースを使うと、F# の場合に型キャストでややこしいってのもあるので。

■プロジェクト構成

今、電卓アプリのサンプルを作っている途中ですが、構成はこんな感じです。

  1. WinStore/Phone のユニバーサルアプリを作成
  2. F# PCL を作成
  3. NuGet で WinXamlProvider をインストール(製作中)
  4. WinStore/Phone で XAML ファイルを動的にロードするように設定(調節中)
  5. Page クラスのコンストラクタで、BindInit を呼び出し。

のようにすると、特定の Xaml/Page が F# の Page クラスにバインドされます。4 の手順は、動的に XAML をロードする必要があって、こうなっています。元ネタの XAML はバイナリ形式の *.xbf ファイルになって手がだせないので、テキスト形式でコンテンツ埋め込みをさせています。

一応、動作させると、

な感じで動きます。

現在、Button, TextBlock, TextBox, Page クラスのプロパティしかバインドしていないので、かなり限定的ですが。

■リフレクションでバインド

詳細なコードは、WinXamlProvider にありますが、主要部分を抜き出すとこんな感じです。

type ParseXaml() =

    let findName( page, propName ) =
        let mi = page.GetType().GetRuntimeMethod("FindName", [|typeof<string>|])
        let res = mi.Invoke( page, [|propName|])
        res

    let bindMethod( page:obj, bind:obj, target:obj, eventName:string, methodName:string) =
        let ei = target.GetType().GetRuntimeEvent(eventName)
        let dt = ei.AddMethod.GetParameters().[0].ParameterType
        let mi = bind.GetType().GetRuntimeMethod(methodName,[|typeof<obj>; typeof<RoutedEventArgs>|])

        let handler =
            new Action<obj,obj>(
                fun sender eventArgs ->
                    let e = new RoutedEventArgs(eventArgs)
                    mi.Invoke( bind, [|sender; e|]) |> ignore )

        let handlerInvoke = handler.GetType().GetRuntimeMethod("Invoke",[|typeof<obj>; typeof<Type[]> |]);
        let dele = handlerInvoke.CreateDelegate(dt, handler)

        let add = new Func<Delegate, EventRegistrationToken> ( fun (t) ->
                        let ret = ei.AddMethod.Invoke(target, [|t|] )
                        ret :?> EventRegistrationToken
                    )
        let remove = new Action<EventRegistrationToken>( fun(t) ->
                ei.RemoveMethod.Invoke( target, [|t|]) |> ignore )
        WindowsRuntimeMarshal.AddEventHandler<Delegate>( add, remove, dele)

    let bindProperty( page:obj, bind:obj, propName:string, t:Type ) =
        let pprop = findName( page, propName )
        if pprop <> null then
            let bi = bind.GetType().GetRuntimeProperty(propName)

            match t.Name with
            | "TextBlock" -> bi.SetValue( bind, new TextBlock(target = pprop))
            | "TextBox" -> bi.SetValue( bind, new TextBox(target = pprop))
            | "Button"    -> bi.SetValue( bind, new Button(target=pprop))
            | _ -> bi.SetValue( bind, new UIElement( target=pprop ))

Page.FindName が必要なので、findName 関数でリフレクションを使います。
bindMethod 関数は、XAML で記述してある Click=”OnClickButton” のイベントを、F# のクラスメソッドに倍度する仕組みですね。イベントハンドラの登録は、WindowsRuntimeMarshal.AddEventHandler を使います。残念ながら、元ネタのイベントを削除していないので(頑張ればできる目途は立ちそうなのですが)、元のイベントと F# のイベントメソッドの両方を呼び出してしまいます。まあ C# のイベントハンドラは空なので、そのままでいいでしょう。

このあたりは、頭がちぎれそうになるぐらいややこしいのですが、まあなんとか。いくつか C# のコードも出てくるので、それを参照に組み立てます。こまめに小さなメソッドにしたほうがわかりやすいですね。

プロパティ呼び出しもいちいちリフレクションします。

namespace Moonmile.WinXamlProvider.UI
open System
open System.Reflection

[<AllowNullLiteral>]
type BaseElement() =
    member val target:obj  = null with get, set
    member this.getProp<'T>( propName:string ) =
        let pi = this.target.GetType().GetRuntimeProperty( propName )
        pi.GetValue(this.target) :?> 'T
    member this.setProp( propName:string, value:obj ) =
        let pi = this.target.GetType().GetRuntimeProperty( propName )
        pi.SetValue( this.target, value)

これを使って、Windows.UI.Xaml を再構築します。
Button クラスを、こんな風にリフレクションでバインドします。

type Button() =
    inherit ButtonBase()
    member this.Flyout
        with get() = base.getProp<FlyoutBase>("Flyout")
        and set(value:FlyoutBase) = base.setProp("Flyout", value )
    member this.CommandParameter
        with get() = base.getProp<Object>("CommandParameter")
        and set(value:Object) = base.setProp("CommandParameter", value )
    member this.Command
        with get() = base.getProp<Windows.Input.ICommand>("Command")
        and set(value:Windows.Input.ICommand) = base.setProp("Command", value )
    member this.ClickMode
        with get() = base.getProp<ClickMode>("ClickMode")
        and set(value:ClickMode) = base.setProp("ClickMode", value )
...

このあたりは手作業で書くと大変なので、適当なツールを作って、コード出力しています。
そのうち自動生成させてしまうつもり。

■F# で MainPage クラスを書いてみる

namespace WinXamlProvider.Lib
open System
open Moonmile.WinXamlProvider
open Moonmile.WinXamlProvider.UI

type MainPage() =
    member val textMessage:TextBlock = null with get, set
    member this.Button_Click(sender:obj, e:RoutedEventArgs) =
        this.textMessage.Text <- "New F# message."

Button をクリックしたときに Button_Click イベントが呼び出されて、textMessage の内容を書き換えるコードです。まだ MVVM タイプの SetBinding を実装していないので、WinForm っぽい書き方になりますが、これで Widows Store アプリと Windows Phone アプリを F# で書くことが可能になります。

■今後は

Button.Click イベントあたりをバインドしてしまえば、直接 this.btn.Click.Add( … ) が使えるようになるので、コードビハイドのイベントコードを書かなく手済むようになります。これを早急に実装。
あとは、自動生成コードを修正して、Windows.UI.Xaml 以下の全てのクラスをバインドさせてしまう。

カテゴリー: F#, WinRT, XAML | F# で Windows.UI.Xaml のクラスをリフレクションを使ってラップして Windows ストア アプリ作る試み はコメントを受け付けていません

後付けの拡張メソッドを使って既存の拡張メソッドをオーバーライドする

裏ワザなのか基本技なのかわかりませんが、拡張メソッドを使うと「うまくやれば」既存のメソッドの動きを上書きできるかもしれん、という技です。

■Xamarin.Forms の FindByName を入れ替える

XamlProvider では、x:Name の解決のために FindByName を提供しているのですが、本当は元ネタの Xamarin.Forms の FindByName を使いたかったのです。内部実装まで真似て、Xamarin.Forms が使っている FindByName をそのまま使いたかった。が、これは Xamarin.Forms.NameScopeExtensions.FindByName 拡張メソッドで、実際に中でどうやっているかは分からないんですよね。実際、Name をキープするところは、実装依存なところがあるので、そこに手を入れらるかどうかはわかりません。

namespace Xamarin.Forms
{
    // 概要:
    //     Extension methods for Xamarin.Forms.Element and Xamarin.Forms.INameScope
    //     that add strongly-typed FindByName methods.
    //
    // コメント:
    //     To be added.
    public static class NameScopeExtensions
    {
        // 概要:
        //     Returns the instance of type T that has name T in the scope that includes
        //     element.
        //
        // パラメーター:
        //   element:
        //     To be added.
        //
        //   name:
        //     To be added.
        //
        // 型パラメーター:
        //   T:
        //     To be added.
        //
        // 戻り値:
        //     To be added.
        //
        // コメント:
        //     To be added.
        public static T FindByName<T>(this Element element, string name);
    }
}

仕方がないので、この FindByName を入れ替えて自前の、FindByName を作ってすり替えています。

 [<Extension>]
type PageXaml() =
    static member LoadXaml(xaml:string) =
        Moonmile.XForms.ParseXaml.LoadXaml(xaml)

    static member LoadXaml<'T when 'T :> Page >(xaml:string) =
        Moonmile.XForms.ParseXaml.LoadXaml<'T>(xaml)

    static member FindByName(page:Page, name:string) =
        Moonmile.XForms.FindByName(name, page)

    /// <summary>
    /// Alias FindByName from Xamarin.Forms
    /// </summary>
    /// <param name="name"></param>
    [<Extension>]
    static member FindByName<'T when 'T :> Element >(this, name:string) =
        FindByName(name, this) :?> 'T

拡張メソッドの [<Extension>] なところは、以下なところを参考するとわかります。

C# から使いやすい F# コードの書き方 – ぐるぐる~
http://bleis-tift.hatenablog.com/entry/20121201/1354362376

で、うまくすり替えてしまったのはいいけれど、実際はどういう呼び出し方になっているのか?(どこが優先なのか)が気になっていました。

■テストコード

既定の BClass と、拡張メソッドを作った BClassExtentions, BClassOverrideExtentions を用意します。

class Program
{
    static void Main(string[] args)
    {
        new Program().main(args);
    }

    public void main(string[] args)
    {
        var b = new BClass("tomoaki");
        Console.WriteLine("{0}", b.GetPrint());
        Console.WriteLine("{0}", b.GetPrintEx());
        Console.WriteLine("{0}", (b as object).GetPrint());
        Console.WriteLine("{0}", (b as object).GetPrintEx());
    }
}

public class BClass
{
    private string _name;
    public BClass(string name)
    {
        _name = name;
    }
    public string Name { get { return _name; } }
    public string GetPrint()
    {
        return string.Format("BClass:{0}", _name);
    }
}

public static class BClassExtentions
{
    /// <summary>
    /// ExtentionLib による拡張 GetPrint
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    public static string GetPrint(this BClass obj)
    {
        return string.Format("BClassEx:{0}", obj.Name);
    }
    /// <summary>
    /// ExtentionLib による拡張 GetPrintEx
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    public static string GetPrintEx(this BClass obj)
    {
        return string.Format("BClassEx:{0}", obj.Name);
    }
}

public static class BClassOverrideExtentions
{
    /// <summary>
    /// OverrideLib による拡張 GetPrint
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    public static string GetPrint(this object obj)
    {
        return string.Format("BClassOver:{0}", (obj as BClass).Name);
    }
    /// <summary>
    /// OverrideLib による拡張 GetPrintEx
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    public static string GetPrintEx(this object obj)
    {
        return string.Format("BClassOverEx:{0}", (obj as BClass).Name);
    }
}

結果は、こんな感じで、うまく BClassOverrideExtentions のメソッドが呼び出されています。

BClass:tomoaki
BClassEx:tomoaki
BClassOver:tomoaki
BClassOverEx:tomoaki

まあ、よく見れば AClass#GetPrint で呼び出しているのか object#GetPrint で呼び出しているかの違いがあるので、呼び出すときのキャストで切り分けているのですが、これが、Xamarin.Forms のように BasicClass > Element > Page のような複数の階層になっている場合、Element にくっついている FindByName を Page にくっつけた FindByName で上書きができる、って話です。結構特殊ですが。

public class BClass
{
    private string _name;
    public BClass(string name)
    {
        _name = name;
    }
    public string Name { get { return _name; } }
    public string GetPrint()
    {
        return string.Format("BClass:{0}", _name);
    }
}
public class SubClass : BClass
{
    public SubClass(string name) : base(name) { }
}
public class SubSubClass : SubClass {
    public SubSubClass(string name) : base(name) { }
}

public static class SubClassExtentions
{
    /// <summary>
    /// SubClassExtentions による拡張 GetPrint
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    public static string GetPrintEx(this SubClass obj)
    {
        return string.Format("SubClassEx:{0}", obj.Name);
    }
}
public static class SubSubClassExtentions
{
    /// <summary>
    /// SubSubClassExtentions による拡張 GetPrint
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    public static string GetPrintEx(this SubSubClass obj)
    {
        return string.Format("SubSubClassEx:{0}", obj.Name);
    }
}

こんな風な階層構造にしておいて、最初は、SubSubClassExtentions 拡張をコメントアウトした状態で、次のコードを動かすと、当然 SubClassExtentions#GetPrintEx が動きます。

var bb = new SubSubClass("masuda");
Console.WriteLine("{0}", bb.GetPrint());
Console.WriteLine("{0}", bb.GetPrintEx());

結果
BClass:masuda
SubClassEx:masuda

その後に、SubSubClassExtentions を有効にして、同じコードを動かすと、結果は SubSubClassExtentions#GetPrintEx を呼び出すようになります。

BClass:masuda
SubSubClassEx:masuda

メソッドをサブクラスから順に探索するから、当たり前といえば当たり前なんだけど、なるほど、そういう動きをしていたのか、という記録ですね。

■同じ拡張メソッドがあるとビルド時にエラー

ちなみに、同じメソッド名、同じ引数で拡張メソッドを作るとビルド時にエラーがでます。

エラー	1	次のメソッドまたはプロパティ間で呼び出しが不適切です: 'OverrideLibFSharp.AClassFSharpExtentions.GetPrintEx(BaseLib.AClass)' と 'ExtentionLib.AClassExtentions.GetPrintEx(BaseLib.AClass)'	C:gitSamplesOverrideExtentionsMethodOverrideExtentionsMethodProgram.cs	25	38	OverrideExtentionsMethod

これは、F# で作った AClassFSharpExtentions.GetPrintEx メソッドと、AClassExtentions.GetPrintEx がぶつかっています、という意味です。F# のほうは PCL で作ったものを参照設定して使っています。

というわけで、結構限定的な使い方ですが、

  • クラスが階層構造になっている。
  • 階層構造の途中で拡張メソッドが使われている(Xamarin.Formsの場合は Element に FindByName がある)

このときは、さらにサブクラス/継承クラスに対して拡張メソッドをつけると、元の拡張メソッドを上書きできる、って話しでした。

カテゴリー: C#, F# | 後付けの拡張メソッドを使って既存の拡張メソッドをオーバーライドする はコメントを受け付けていません

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 | Xamarin.Forms+F# で Navigation.PushAsync を使うときの注意 はコメントを受け付けていません

小4の夏休みの宿題をチケット駆動で乗り切る方法を模索

先日の日曜日、日割りでスケジュールを立てた後、1日のタイムスケジュールを立てたのですが、初日からうまくいかないことが判明…いや、途中にプールが挟まったり、学校の短時間の補習がはさまったりして、うまくいきませんでした。初日から、午後6時過ぎまで夏休みの宿題をしている有様に。

まずは、前提から書きますので、参考にしてください。

■夏休みの宿題の全体を把握する

先週の日曜日にやったことは、まず、夏休みの宿題の量を全体として把握しておくことです。

  • 漢字ドリル
  • 計算ドリル
  • 公文算数
  • 一言日記
  • 自由研究(かたつむりの観察日記)
  • 読書感想文

この中で、一言日記は毎日やることなので別な計画にします。自由研究はかたつむりの観察日記だそうで、これも毎日やります。読書感想文は、適当な締切を決めて(祖母の家に遊びに行く前とか、海に遊びに行く前とか)自由にしておきました。

公文算数は週5ペースでやるのですが、週2回教室に行くことになります。持って帰ってきた宿題は週のうちにやればよいので、土日を含んでもよいし、含めなくても良い状態です。

漢字ドリルと計算ドリルは全体のページ数を把握した後、「何日で終わらせるのか?」の計画を立てます。夏休み自体は40日あるのですが、これには土日が含まれています。また、お盆に祖母に家に行ったり、海に行ったりする日も含めてしまっています。これを取り除くと、だいたい20日になります。祖母の家や土日はプロジェクトバッファとして扱います。

■日割り計算する

計算ドリルと漢字ドリルの全体像を把握して、宿題をする日数が20日間と決まったら、これを日割りします。小数点になってしまうので、小4には難しい(分数や小数点は小5からですよね)。あまりを出して、それを上回れば計画通りに終わる、ということを子供に印象付けます。

ここでは、20日間で、2ページずつやれば終わることがわかりました。

そのままでもいいのですが、20日間ずっと宿題が続いているという状態がいいのか、「早めに宿題を終わらせたいのか?」という判断を子供にさせます。これは子供の性質にもよるので、どちらともいえないのですが「自発」を重んじます。

その後、再計画を立てて、計算ドリルと漢字ドリルを10日間で終わらせることにしました。

  • 計算ドリルを毎日4ページずつ
  • 漢字ドリルを毎日3ページずつ
  • 公文算数を平日にやる

という計画が立てられました。これに従って進めばいいのですが、さて、いろいろ「誘惑」なりがあり現実はままならないものです。

■ひとまず、計画を実行してみる

計画がスムースにいくのかどうかは、実際やってみないとわかりません。そのためにプロトタイプとして宿題をやるスピードを確かめるのが第一です。で、初日(火曜日)にやってみたところ、結構、散々な結果でした。

  • 午前中に学校のプールがあった。
  • プールから帰ってきて、宿題をやったかどうかは不明(ワタクシは家で仕事中)
  • お昼を食べた
  • 午後に、漢字と計算ドリルの宿題をやった…模様
  • 午後4時頃のテレビを見ていた。
  • 夕方に弟を迎えに行った。
  • 保育園から帰ってくると、公文が終わっていなかった。
  • 夕飯前(7時半)に公文を無理やり終わらせる。
  • 8時半ごろにカタツムリの観察日記を書いている。
  • 寝る直前に一言日記を書いている。

というわけで、ダメダメですね。これらは一見、学習習慣の問題のように見えますが、違います。まあ「習慣」ができると、早めにやる「習慣」ができるのですが、小4の段階ではまだ「習慣」以前なので、もうちょっと違う問題です。

基本、私は家で仕事なので、娘を監視できる立場にはいるのですが、それじゃ面倒ですよね。面倒というか、監視されないと宿題ができないのでは駄目ですし、これからが大変です。なので、面倒な監視作業はしません。が、「習慣」づけは必要なので、この夏はやる予定です。

■夏休みの宿題にも割り込みがたくさんある

毎日の課題(漢字ドリル、計算ドリル)をやるための障害として、「割り込み」があります。日常ルーチンをこなそうとしても、電話が入ったり、メールに答えないと駄目だったり、そういう問題ですね。私の場合、宅配便が一番集中力がそがれるのですが…まあ、これは仕方がない愚痴です。

プールなり学校の補習なりがあれば、一日の中で夏休みの宿題の時間が削られます。計画通り、むりやり夏休みの宿題をする時間をとればいいのですが(実際、毎日のルーチンワークとしての宿題は1時間以内でおさまるでしょう)、ルーチンワークができない小4の娘には、計画がずれても、計画通りに進むスタイルが必要になります。

■チケットを切る

宿題の日割りはできたものの、それを「毎日やる」ことにすると負担が出てしまうので、毎日やる必要がない状態にします。夏休みの宿題の場合は、

  • 全体の量が決まっている(途中でボリュームが増えたりしない)
  • 手戻りがほとんどない(やった宿題が無駄にならない)
  • 締切が決まっている(エンドが決まっている)
  • 分割が可能

という特徴があります。プロジェクトとしての組み立て方も WBS に落とすよりも、日割りで計算する簡単な方法がとれます。これは、読書感想文のような場合には、WBS な落とし込みをするか、別なスタイルが必要です。

というわけで、「毎日、計画通りに漢字ドリルと計算ドリルを数ページずつやる」という計画が破たんしてしまったので、「夏休みの宿題チケットを消費する」というスタイルに変更します。漢字ドリル3ページ、計算ドリル4ページのチケットをそれぞれ作って、これを10枚の付箋にします。全部で20枚になるわけですね。

■チケットを消費する

チケット駆動にすると、あまりに遅いペースでチケットを消費すると、エンドがずれるという現象がおこります。プロジェクトの場合は、締切を動かすことも可能ですが、夏休みの場合はエンドが絶対なので、締切を動かすことができません。このため、チケットが消費されているスピードは適宜「監視」が必要です。

本来ならば、バーンダウンチャートを使ってグラフ化するとわかりやすいのですが、ちょっとそのあたりは実際に娘にとってチケット駆動がうまく動くかどうか確かめてからやってみましょう。

カテゴリー: 雑談 | 小4の夏休みの宿題をチケット駆動で乗り切る方法を模索 はコメントを受け付けていません

Xamarin.Forms の XAML を F# から使うために XamlTypeProvider を使う

XFormsPreviewer では、動的にXAMLファイルをロードしてレンダリングする(内部的にはXamarin.Formsのコントロールを生成する)してプレビューを実現していますが、このコアライブラリに XamlTypeProvider があります。最初は Xamarin.Forms のプレビューだけを作るつもりだったけど、作っているうちに F# の TypeProvider と似ていることがわかり、じゃあ TypeProvider 的にしようとしたのが発端です。

■Xamrin.Forms と XAML の関係

要は、WPF や Windows Store の XAML と同じように、フロントエンドを記述する XAML と、ビルド時に自動生成される partial class とで成り立っています。この partial class のほうで、コントロールの名前による参照や、ボタンコントロールのクリックイベントなどを記述するわけで、事実上のコードビハイドですね。Silverlight の XAML の場合には、XAML 自体に C# のコードを埋め込めるので(今でもそうだと思う)XAML 単体で動かすことも可能なわけですが…Windows Store/Windows Phone 8.1 の XAML はできたかどうかわかりません。たぶん、Xamarin.Forms の XAML もできません。

で、コードビハイドを使うと、XAML で記述した View に内部動作のコードが埋め込まれるわけで、(それがコードビハイドだとしても)View と Logic が分離していないのではないか。という話なのですが、まあ、実装的には MVVM の INotifyPropertyChanged/ICommand にしてしまうか、別な方法をにしてしまうか、という具合です。ワタクシ的にはコードビハイドでもいいと思うんですけどね。うまく分離されていれば。

「うまく分離する」という点で、MVVM パターンは、View と Modelが分離できていますが、複数の View を扱うときにちょっと困ります。これは Xamarin.Forms の問題かもしれないけど、とあるビューから別のビューに移るときに、PushAsync を使いますが、この Navigation プロパティは Page にくっついて引数もページなんですよね。

page.Navigation.PushAsync(nextPage)

当たり前といえば当たり前だけど、画面遷移自体は、ページからページに移る。ViewModel の場合は Page に依存したくないので、View から本格的に ViewModel を分離させたときに Command パターン内では他のページに遷移させることができない、ってことになります。まあ、現実的な解としては ViewModel に対応する View を持たせてしまえばいいのですが。

そんな依存関係から、Xamarin.Forms も Windows Store の XAML も View=XAML に対応するコードを出力して、各種イベントやデータを保持する方法をとっています。
実は、F# の TypeProvider も同じことが言えて、実行時にビルドして Type を決めるので FsXaml のように WPF の XAML をコンバートできるけど、これは動作時にはできないのです。というのも、未知の XAML に対して、内部プロパティやイベントの判定を、先にコードビハイドに記述することはできませんよね。。まあ、方法としては、

  • XAML ダウンロード時に TypeProvider でビルドする。
  • Page に付属するプロパティは、dynamic で受けて参照させる。

ということで、実行にデータの整合性のチェックをすればよいのですが、完全にダイナミックに、というわけにはいきません。

また、Xamrin.Forms の XAML から出力するコードは C# のために、F# から使おうとするビルドエラーになります。これは Windows Store の XAML の場合も同じで、ここのコードビハイドのコードが言語依存になってしまって邪魔です。XAML 自体はコード依存していないのに。
逆に言えば、コードビハイドに全く依存しない形で、書けばバックエンドが C# であろうと F# であろうと、場合によっては Visual Basic であろうと関係なく書ける可能性があります。

■View と ViewModel を動的ににリンクさせる

通常の MVVM モデルの場合には、先の画面遷移の関係もあって、View と ViewModel は1対1の関係になっています。View(XAML)に対して、ひとつひとつ ViewModel を作ることが多いのですが、ViewModel から View への依存を完全に切り離してしまえば、ひとつの ViewModel を複数の View に対応させることができます。

コンパイル時にこれを実現するのは非常に簡単で、同じ ViewModel を使っているけど状況に応じて複数の View を切り替えることができます。ただし、「状況に応じて」切り替えられるのは、ViewModel の中ではなく、先の Page のようなコードビハイドの中になります。

ここで、View と ViewModel を完全に分離させようとすると矛盾がでます。

– XAML は、コードビハイドを出力する
– コードビハイドでのみ、画面遷移/Viewの切り替えが可能
– View は ViewModel と分離させたい
– ViewModel から画面遷移くを直接アクセスできない。

この矛盾は、XAML の仕組みがコードビハイドに依存している(partial class)ためであって、XAML 自体が徹底して View に徹していれば、うまく矛盾が解消できるような気がします。もちろん、ビヘイビアのような View 内部に存在するコード(アニメーションのようなコード)は、XAML にあるほうがよいのですが、ViewModel でもまかなえるものは、切り離してしまったほうがよいでしょう。

…という発想のもとに、XAML コード自体を完全に動的にロードさせます。動的にロードさせるわけですから、未知の XAML ファイルでも大丈夫です。いままで、リソースとして持つか、文字列としてプロジェクト内に埋め込むかしかなかったものが、インターネット上に XAML ファイルを置いて、それをダウンロードして使うことができます。まあ、作り始めて分かったのですが、ちょうど HTML がブラウザ上で動くのと同じ仕組みですね。

HTML の場合が

  • HTML でデザインコードを書く。
  • Javascript で動きのあるコードを書く。
  • Javascript はブラウザのインタープリタで動く

というように、コード部分がインタープリタにしかならないのがスピードではネックだったりします。

Xamarin.Forms 用の XAML ファイルを直接ダウンロードできるようにすることで、

  • XAML でデザインコードを書く。
  • 動作は、C# で ViewModel 内に書く。あるいは、動的リンクされるコードビハイドもどきに記述する。
  • ViewModel内 のコードは事前コンパイルで、Xamarin によりネイティブコードで動く。

ViewModel のアセンブリを動的ダウンロードできるかどうかは分からないのですが(ActiveXみたいな感じ?)、ひとまず、この方式をとることで Javascript などによるインタープリタではないネイティブの動作が可能になります(もちろん、Javascript等であっても、実行時ビルドされていればスピードは同じなんですけど)。

Xaml のパース自体が車輪の再発明っぽいので、あまりやりたくないのですが、ざっと F# で書いてみたところ 400 行程度なのでなんとか許容範囲でしょう。

■実際どう使うのか?

moonmile/XamarinXamlPreview
https://github.com/moonmile/XamarinXamlPreview

Xamarin.Forms for F# のコンテスト用に作ったので F# になっていますが、当然ながら C# でも使えます。page.Navigation.PushAsync あたりが、変なことになっているのは F# だからであって、C# だと結構素直に書けます。

type MainPage(path:string) =
    inherit ContentPage()
    let mutable page:Page = null
    let mutable buttonNextPage:Button = null
    let mutable buttonStopWatch:Button = null
    let mutable buttonStopWatchDownload:Button = null
    let mutable label:Label = null
    
    let pushPageVM(rn:string, vm:ViewModelBase) =
        let xaml = ResourceLoader.GetString(rn)
        let next = ParseXaml.LoadXaml(xaml) 
        next.BindingContext <- vm
        let t = new Task( fun() -> 
            let task = page.Navigation.PushAsync(next)
            task.Start()
        )
        t.RunSynchronously()
    
    let pushPage(rn:string) =
        let xaml = ResourceLoader.GetString(rn)
        let next = ParseXaml.LoadXaml(xaml) 
        let t = new Task( fun() -> 
            let task = page.Navigation.PushAsync(next)
            task.Start()
        )
        t.RunSynchronously()

    let pushPageDL(url:string) =
        
        try 
            let hc = new HttpClient()
            let xaml = hc.GetStringAsync(url).Result
            let next = ParseXaml.LoadXaml(xaml) 
            let t = new Task( fun() -> 
                let task = page.Navigation.PushAsync(next)
                task.Start()
            )
            t.RunSynchronously()
        with
        | _ -> 
            label.Text <- String.Format("Error: cannot open {0}", url )

    do 
        let xaml = ResourceLoader.GetString(path)

        page <- ParseXaml.LoadXaml(xaml)

        buttonNextPage <- page.FindByName<Button>("buttonNextPage")
        buttonStopWatch <- page.FindByName<Button>("buttonStopWatch")
        buttonStopWatchDownload <- page.FindByName<Button>("buttonStopWatchDownload")
        label <- page.FindByName<Label>("label")
        
        buttonNextPage.Clicked.Add( fun(e) -> 
                pushPageVM("NextPage.xml", new ViewModelNextPage())
            )
        buttonStopWatch.Clicked.Add( 
            fun(e) -> pushPage("StopWatchPage.xml"))

        buttonStopWatchDownload.Clicked.Add( 
            fun(e) -> pushPageDL("http://moonmile.net/up/DownLoadPage.xml"))

    member this.Page 
        with get() = page

Xaml ファイルをそのままプロジェクトに埋め込むと、C# のコードビハイドを吐き出すので、拡張子を xml にしています。ViewModel 自体は、XAML を動的に生成して結びつけます。内部的にはリフレクションの嵐になっていますが気にしてはいけません。外側から使う時、ViewModel から使う時はリフレクションなどは意識しない作りになっています。普通の ViewModel を使うときと同じ要領ですね。

F# なので Command パターンのところがややこしくなっていますが、C# でも同じように書けます。データバインディングを使うので、View に依存しないように書けます。ここは MVVM と同じです。

type ViewModelStopWatch() =
    inherit ViewModelBase()

    let mutable startTime = DateTime.Now
    let mutable endTime  = DateTime.Now
    let mutable time:TimeSpan = new TimeSpan()
    let mutable task:Task = null
    let mutable taskflag = false


    /// リフレクションで iOS/Android の System.Timers.Timer を持ってくる
    /// 現在調節中
    let makeTimer (interval:int, hdr:ElapsedEventHandler) =
        let asm = Assembly.Load(AssemblyName("System"))
        let t = asm.GetType( "System.Timers.Timer" )
        let item = System.Activator.CreateInstance(t)

        t.GetRuntimeProperty("Enabled").SetValue( item, true )
        t.GetRuntimeProperty("AutoReset").SetValue( item, true )
        t.GetRuntimeProperty("Interval").SetValue( item, interval )
        t.GetRuntimeEvent("Elapsed").AddEventHandler( item, hdr )
        t.GetRuntimeMethod("Start", null).Invoke(item, null) |> ignore
        item

    member this.Time 
        with get() =  
            time <- endTime - startTime
            String.Format("{0:00}min {1:00}sec {2:000}", time.Minutes, time.Seconds, time.Milliseconds )
    member this.StartTime
        with get() = startTime.ToString()

    member this.StartCommand 
        with get() =
            let cmd = 
                new DelegateCommand(
                    (fun (o) -> true ),
                    (fun (o) -> 
                        startTime <- DateTime.Now 
                        endTime <- startTime
                        this.OnPropertyChanged("StartTime")
                        this.OnPropertyChanged("Time")
                        // makeTimer(2000, new ElapsedEventHandler(fun(s,e)-> 
                        //     System.Diagnostics.Debug.WriteLine("time {0}", endTime ))) |> ignore
                        // )))
                        ))
            cmd

    member this.StopCommand 
        with get() =
            let cmd = 
                new DelegateCommand(
                    (fun (o) -> true ),
                    (fun (o) -> 
                        taskflag <- false
                        endTime <- DateTime.Now 
                        this.OnPropertyChanged("Time")
                    ))    
            cmd

    member this.ResetCommand 
        with get() =
            let cmd = 
                new DelegateCommand(
                    (fun (o) -> true ),
                    (fun (o) -> 
                        taskflag <- false
                        startTime <- DateTime.Now
                        endTime <- DateTime.Now 
                        this.OnPropertyChanged("StartTime")
                        this.OnPropertyChanged("Time")
                    ))    
            cmd

ここでストップウォッチを作ろうと思ったのですが、Xamarin.Android の場合、Task.Delay の扱いが Windows Forms と異なるので苦戦中です。実は WinRT も同じ現象になるので、定期的なメソッド起動はタイマーを使わないと駄目なんですよね。このあたりは、別なサンプルアプリとして実装します。

Xamarin.Forms や MvvmCross 的には Service を使うのでしょうが、今回はプラットフォーム依存のアセンブリを動的ロードして、タイマークラスをリフレクションを使って操作する、ということを試しています。結構乱暴ですが、こうすると PCL からプラットフォームへのインターフェースがいらなくなって(タイマーを起動するインターフェースが必要なくなる)依存関係が「疎」になります。実は、タイマーの扱いも、Android/iOS/WinStore とそれぞれ異なるので、工夫が必要なのですが、Device を使って回避する予定です。

ライブラリとしての XamlTypeProvider は NuGet を使って取得できます。
https://www.nuget.org/packages/Xamarin.Forms.XamlProvider/

■実行する

「Download View from Internet」ボタンをクリックすると、インターネット上から XAML コードをダウンロードしています。こうすることで、View だけを定期的に更新することが可能です。

XFormsPerviwer もそうなのですが、Xamarin.Forms の XAML を外だしにしていつでも更新できるようにしておくと、ちょこちょこと XAML を手作業で修正して、エミュレータ上で動作を確認できます。たぶん、開発効率が上がるかな、ってのと、ちょっとしたパズルゲームとか設定画面をダイナミックにロードさせると、画面の雰囲気ががらりと変わってよいかと。アプリ自体はそのままなので、手軽に変えらえるのがミソです。そう、ややこしい「審査」がいらなくなりますからね。

この仕組み自体が「審査」が通るからどうかは、近々試してみる予定です。しばらくは、プレビュー的に使うのと、XamlTypeProvider の整備ってところです。

カテゴリー: 開発 | Xamarin.Forms の XAML を F# から使うために XamlTypeProvider を使う はコメントを受け付けていません

NuGetでXamarin.Forms.XamlProvilderを公開しました

動的にXamarin.FormsのXAMLファイルをロードするときのライブラリとして使っている、Xamarin.Forms.XamlProvilder を NuGet で公開しました。

NuGet Gallery | Xamrin.Forms Xaml Provider 0.1.1
https://www.nuget.org/packages/Xamarin.Forms.XamlProvider/0.1.1

XAMLの文字列から ContentPage 等を作るので、こんな風にXAML形式のデータをC#のコード内に埋め込んで記述ができます。

public void LoadSample()
{
	string xml = @"<?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='XFormsPreview.NewPage'>
		<StackLayout>
			<Label Text='New Page'  />
			<Button Text='Click me'  />
		</StackLayout>
		</ContentPage>";

	var page = PageXaml.LoadXaml<ContentPage>(xml);
	Assert.IsNotNull(page);
	var layout = page.Content as StackLayout;
	Assert.IsNotNull(layout);
	Assert.AreEqual(2, layout.Children.Count);

文字列から動的に変換するので、ファイルリソースをプロジェクトに埋め込ませて読み込むこともできます。

[TestMethod]
public void GridDemoPage()
{
    var fs = File.OpenText(@"XamlGridDemoPage.xaml");
    var xaml = fs.ReadToEnd();
    var page = PageXaml.LoadXaml<ContentPage>(xaml);

    Assert.IsNotNull(page);
    Assert.AreEqual("Grid Demo Page", page.Title );
    Assert.AreEqual(new Thickness(0, 20, 0, 0), page.Padding);

これを HTTP 経由で拾ってくるようにしたのが XFormsPreviewer です。

async void OnClickSample0(object sender, EventArgs e)
{
    var hc = new HttpClient();
    try
    {
        var res = await hc.GetStringAsync("http://localhost:10150/get/0");
        var page = PageXaml.LoadXaml(res);
        await this.Navigation.PushAsync(page);
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine(ex.Message);
    }
}

当然、HTTP 経由で拾ってくるので、インターネット上に XAML ファイルを配置しておけば、アプリケーションの更新なしに、動的に Xamarin製 XAML ファイルをダウンロードして表示を変えることが可能です…が、いまのところ、プレビューの機能しかないので静的ページしか表示できません。この部分はちょうど Javascript 無しの WebView みたいなものです。

■名前を取得する

ver.0.1.1 では、x:Name で Element を参照できるようにしました。

public void TestName()
{
    string xml = @"<?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='XFormsPreview.NewPage'>
	<StackLayout>
		<Label x:Name='label1' Text='New Page'  />
		<Button  x:Name='button1' Text='Click me'  />
	</StackLayout>
</ContentPage>
";
         
    var page = PageXaml.LoadXaml<ContentPage>(xml);
    Assert.IsNotNull(page);
    var layout = page.Content as StackLayout;

    var label1 = page.FindByName<Label>("label1");
    var button1 = page.FindByName<Button>("button1");
    Assert.IsNotNull( label1 );
    Assert.AreEqual("New Page", label1.Text);
    Assert.AreEqual("Click me", button1.Text);
}

XAML内に x:Name で名前を付けておくと、Xamarin.Forms や WPFなどでは、それぞれのプロパティにしてくれますが、動的にXAMLファイルをロードする場合は直接プロパティを参照しづらいので、FindByName<Label>(“name”) のように参照します。FindByName 自体は Xamarin.Forms にもあるし、Silverlight でよく使うパターンです。
こうすると、固定で Xamarin.Forms を作ったときと同じように、各種のプロパティが設定できます。

var page = PageXaml.LoadXaml<ContentPage>(xml);
var label1 = page.FindByName<Label>("label1");
label1.Text = "change display";

■BindingContent を実装中

ただし、x:Name でコントロールのオブジェクトを取ってくると、動的にロードされた XAML との接点が多くなってしまうので、もっと沿結合にするため BindingContext の部分を実装中です。MVVM パターンにしたがって、
INotifyPropertyChanged インターフェースと ICommand インターフェースが実装されていれば、ViewModel がそのまま利用できる感じになる予定です。

カテゴリー: C#, Xamarin | NuGetでXamarin.Forms.XamlProvilderを公開しました はコメントを受け付けていません

Xamarin.Forms 用のプレビューアをアルファ版で公開

1週間ほど掛けて、おおまかなところが動いてきたのでアルファ版で公開します。
何ができるかというと、Xamarin.Forms の XAML ファイルを動的にロードして、エミュレータ上に表示させます。現在のところ、Xamarin Studio にも Visual Studio にも GUI ベースのデザイナがないので、XAML コードを手打ちしています。まあ、手打ち自体は構わないのですが、画面の表示を確認するのに、いちいちビルドしてエミュレータなどにデプロイしないと画面が分かりません。勢い、StatckLayout などの汎用的な画面を作ることが多いのですが、それにしても色とか微妙な文字の配置とかが面倒です。そこで、XAML 記述を HTTP 経由で持ってきて、エミュレータ内で再構築するプレビューツールを作りました。エミュレータ上で画面をリロード(HTTP 経由で XAML ファイルを再びロード)することで、画面の状態が更新できます。これにより、ひとまず、XAMLのコードを書く→デプロイ→デザインの確認→XAMLのコード直し、というサイクルがなくなり、XAMLのコードを書く→画面をリロードする→XAMLの手直し、の手順だけで良くなります。ちょっと、使ってみた感じでは、表示している文字変更とか色変更が手軽にできるので、Xamarin Studio で本格的なデザイナが作られるまでのツナギになるとは思います。

■ダウンロード

moonmile/XFormsPreviewer
https://github.com/moonmile/XFormsPreviewer

まだ、NuGet を作っていないので、github から直接取ってきてください

■構成

image

  • Host/XFormsPreviewerHost ? XAML データを提供する簡易ホスト
  • Sample/XFormsSampleApp — サンプルプロジェクト(XAMLファイルが含まれる)
  • Viewer/XFormsPreviewer — 簡易プレビューア
  • XFormsProvider ? Xamarin製XAMLのパーサ
  • XFormsProvider.Test ? テストプロジェクト

プレビューアが、簡易ホストを経由してサンプルプロジェクト内の XAML ファイルを表示させます。プレビューア自体は、iOS/Android/Windws Phoneエミュレータなので、適当にカスタマイズしてリロードしてください。今後はもうちょっとプレビューア/簡易ホストをもう少しリッチにしていきましょう。

■使い方

応答する XAML ファイルを指定する。
簡易ホストを書き換えて、応答する XAML ファイルを指定します。/get/item のような形式で、プレビューアから呼び出されます。

 
public PreviewHost()
{
    this.Port = 10150;
    this.LoadPaths = new Dictionary<string, string>();
    // TODO: change load xaml file
    // ここで、レスポンスを返す XAML ファイルを指定。
    // 作成中のプロジェクトフォルダを指定すればok.
    // 後で WPF あたりで作成してドラッグ&ドロップ.
    // mac のためにコマンドラインで使えるようにしておく。
    var basefolder = @"..\..\..\Sample\XFormsSampleApp\XFormsSampleApp\";
    this.LoadPaths.Add("0", basefolder + "MainPage.xaml");
}

簡易クライアントを管理者モードで立ち上げる。
Windows 8.1 の場合は、ローカルサーバーを立ち上げるときに指定ポートを明示的に開ける必要があります。面倒な場合は、コマンドプロンプトを管理者モードで立ち上げてください。

image

プレビューアで要求ボタンを書き換える。
新しくボタンを作ってもよいし、既存のボタンを書き換えてもよいです。

/// <summary>
/// HTTP クライアント経由で XAML ファイルをロードする
/// http://hostname:10150/get/[num] 形式で取得する
/// [num] 部分は XFormsPreviewHost と合わせる
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
async void OnClickSample0(object sender, EventArgs e)
{
    var hc = new HttpClient();
    try
    {
        var res = await hc.GetStringAsync(this.Uri + "0");
        var page = PageXaml.LoadXaml(res);
        await this.Navigation.PushAsync(page);
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine(ex.Message);
    }
}

呼び出しは /get/item 形式です。XAML データのローディングは非常に簡単ですPageXaml.LoadXaml(res) のように LoadXaml メソッドを呼び出すことで Page オブジェクト(内部で設定している ConentPage オブジェクトなど)を受け取ることができます。ここでは Navigation を使って PushAsync メソッドで画面遷移をさせます。これにより、ボタンを押すごとに XAML データのリロードができます。

ホスト名を書き換える。通常、エミュレータからは名前解決ができないと思うので、IP アドレスを直書きしています。このあたりは、別途設定ファイルから読み込ませる予定です。

public MainPage()
{
    InitializeComponent();

    this.Port = 10150;
    // TODO: Change IP address of XFormsPreviewHost.exe on machine.
    // XAML ファイルをホスティングしている PC の IP アドレスを書く。
    // エミュレータから名前解決ができれば、マシン名でもOK.
    this.Hostname = "172.16.0.9";
}

imageimage

一度作ったプレビューアは何度も再利用ができます。/get/item コマンドだけ変更しなければ、簡易ホストのほうで、自由に応答 XAML ファイルを変更できます。

Visual Studio のほうでは、XAML のインテリセンスが効かないので、Xamarin Studio で XAML ファイルを編集します。属性などをコード補間してくれるので、多少は間違いがすくないかも。

image

■Type Provider 風に提供

XFormsPreviewer/XFormsProvider/PageXaml.fs at master ・ moonmile/XFormsPreviewer
https://github.com/moonmile/XFormsPreviewer/blob/master/XFormsProvider/PageXaml.fs

パーサ自体は F# で書いています。PCL の場合 FSharp.Core の競合があって懸念点が多いのですが、各種のワーニングは無視です。警告がいやだったので、最初は C# で書いていたのですが、今後のメンテを考えると F# のほうが良いかなと(単なる趣味…かもしれません)。

すごい大ざっぱに書いているので、タグ名とプロパティ名の対応を取ってない(DTDを考慮していない…というか、そもそも Xamarin.Forms の XAML に DTD が用意されていないのが問題では?…とも思ったけど、MS製XAML http://schemas.microsoft.com/winfx/2006/xaml/presentation のほうの記述もないので、そういうものかもしれない)のですが、まあツナギなので動けばよいという考えて作っています。が、どうせならば、F# の Type Provider と互換をとりたいですよね。FsXaml な感じで、WPF 製 XAML と互換を取るぐらいまでは作る予定です。最終的には、Windows Store 製 XAML や Android の axml, Xcode の storyboard の互換までいきたい。

■できないこと

本格的にデザイナ/プレビューアとして使うには、足りない機能が満載なのですが、

  • static resource の参照ができない。
  • 外部 UI モジュールを呼び出せない。
  • x:Name を参照できない。
  • ICommand が動かない。
  • MVVM の Binding が動かない。
  • デザイン時の Data Binding がないので、ListView が表示できない。

の項目ができません。このあたりは、おいおい実装していく予定です。これを実装すると、動的に XAML をロードするだけで、本体に View がいらないという状態になって、本格的に MVVM での View と ViewModel の疎結合が実現できそうです。

カテゴリー: F#, Xamarin | Xamarin.Forms 用のプレビューアをアルファ版で公開 はコメントを受け付けていません

Xamarin.Formsへ動的にXAMLデータをロードする…の手始め

第1回 Xamarin User Group は無事終了。プレゼン資料については後ほど解説を入れるとして、なにかと気になったので、ちょっと手を付けてみました。

Xamarin.Forms を使うとき、Phone の場合は画面が狭いので手打ちで XAML を書いてもなんとかなるのですが、タブレットのように広い画面になった時や、Xcode の autolayout みたいな感じでレイアウトしようとすると手打ち XAML だけだと結構大変です。実行しないとうまくいかないし、懇親会でもレイアウトに苦労している場面もあったりして、

  • エミュレータで実行してみないと、タグの typo が分からない。
  • エミュレータで実行してみないと、レイアウト崩れがわからない。

のは、いかんともしがたいところ。Xamarin.Forms 用のデザイナが出ればいいんですが、暫くは出る様子もないので、なんとかならんかなと。そこに、「プレビューでもあれば便利なのに」とのツイートを見つけました。

なるほど、デザイン自体は大変そうだけど、プレビューだけならなんとかなりそうです。

  1. Xamarin.Froms の XAML を手打ちする。
  2. 更新されたファイルを読み取って、デザイナに描画する。

程度の繰り返しができればよいわけで、そのサイクルが早く回れば try & error も進みそう。で、なにか方法はないかと思って、ひとつ思いついたのが、

  1. デザイン用の XAML を手打ちする。
  2. なんらかの形で iOS/Android のエミュレータにアップ。あるいは、エミュレータからプル。
  3. なんらかの形で、エミュレータ上で XAML を解析して、レイアウトを作成する。

というパターンです。プレビュー自体をエミュレータ内で動かしてしまえばよいのです。

image

幸いにして、Xamarin.Forms の場合は、プラットフォーム共通のレイアウトページとして Xamarin.Forms の ContentPage が用意されています。大抵は PCL で作るんですが、この new ContentPage したものを、iOS/Android のプラットフォーム毎に再構成しています。ならば、この new ContentPage のところをすり替えてしまえば、良いわけで実際、XAML のほうではなくて、コードでちまちまと ContentPage を作っているところを、XAML から作り込めるようにすれば良いわけです。

namespace XFormsPreview
{
    public partial class MainPage
    {
        public MainPage()
        {
            InitializeComponent();
        }

        void OnClickPreview(object sender, EventArgs e)
        {
            // var page = new NewPage();
            string xml = @"<?xml version='1.0' encoding='utf-8' ?>
<Contentpage x:class="XFormsPreview.NewPage" 
xmlns:x=http://schemas.microsoft.com/winfx/2009/xaml
xmlns="http://xamarin.com/schemas/2014/forms"> <Stacklayout> <Label Text="New Page"> <Button Text="click me"> </Stacklayout> </Contentpage> "; var page = ContentPageXaml.LoadXaml(xml); this.Navigation.PushAsync(page); } } }

こんな風に文字列で XAML を設定して LoadXml すると ContentPage ができるようにします。ここは F# の Type Provider と似た感じですね。

まだテスト中なので、XAML の文字を埋め込んでいますが、これをエミュレータの動いている PC から XAML ファイルをダウンロードできるようにすれば、XAML の修正とプレビューのサイクルが早くなります。大まかの動きの目途は立ったので、あとはちまちまと Xmarin.Forms のコントロールを移し替えていきます。車輪の再発明ような感じもするし、既にあるような気もするし、本格的なデザイナが出るまでの短い間という訳でもあるけど、ひとまず、一通り動くところまで作ってみる予定。

カテゴリー: C#, Xamarin | Xamarin.Formsへ動的にXAMLデータをロードする…の手始め はコメントを受け付けていません