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 パーマリンク