先日の 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 のおかげです。感謝。