今、手元で作っている 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) になってしまうので、使えるライブラリに制限がでてきます。
こんな風に、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# の場合に型キャストでややこしいってのもあるので。
■プロジェクト構成
今、電卓アプリのサンプルを作っている途中ですが、構成はこんな感じです。
- WinStore/Phone のユニバーサルアプリを作成
- F# PCL を作成
- NuGet で WinXamlProvider をインストール(製作中)
- WinStore/Phone で XAML ファイルを動的にロードするように設定(調節中)
- 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 以下の全てのクラスをバインドさせてしまう。