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# プロジェクトのファイルを開いて、ターゲットの書き換えと、

[code lang=”xml”]
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
<TargetFrameworkProfile>Profile32</TargetFrameworkProfile>
[/code]

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

[code lang=”csharp”]
<ItemGroup>
<TargetPlatform Include="Windows, Version=8.1" />
<TargetPlatform Include="WindowsPhoneApp, Version=8.1" />
<Compile Include="Class1.fs" />
</ItemGroup>
[/code]

実行時に

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

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

実験したプロジェクトは 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 にありますが、主要部分を抜き出すとこんな感じです。

[code lang=”vb”]
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 ))
[/code]

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

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

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

[code lang=”vb”]
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)
[/code]

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

[code lang=”vb”]
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 )
…
[/code]

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

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

[code lang=”vb”]
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."
[/code]

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

■今後は

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

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