読者です 読者をやめる 読者になる 読者になる

うならぼ

どうも。

WebBrowserを通してC#とJSでやり取りする

C#

C#からはHtmlDocument.InvokeScriptでグローバル関数を呼び出せる。evalも呼べる。

でもってWebBrowser.ObjectForScriptingComVisible(true)なオブジェクトを設定しておくと、publicなメンバーがJSからアクセス可能になる。

How to: Implement Two-Way Communication Between DHTML Code and Client Application Code

C#でもUIをHTML/CSS/JavaScriptで実装したい! - Qiita

C# から JavaScript を利用する場合の嫌なとこ

  • 変数の設定や取得ができない。
  • 戻り値が配列や連想配列の場合は ComObject が返ってくる。
  • 引数として配列や連想配列を渡せない。
  • クラス内関数を実行できない。

JavaScript から C# を利用する場合の嫌なとこ

  • 戻り値が配列だと値を受け取れない。

InvokeScriptの正体

WebBrowserコントロールは多分IWebBrowser2。てことはIWebBrowser2を使って同じようにJSを呼び出せるはず。

まずIHTMLWindow2::execScriptなるものがあります。けれどこれは eval 相当のものであって、あたかも目的の関数にオブジェクトを直接渡すかのようなInvokeScriptの動きとは異なります。

ところでIHTMLWindow2のメソッド一覧では「execScriptは古いからeval使いな」って書いてあります。実はexecScriptというのは以前window.execScriptとしてJSから呼び出すことができた。というか、IHTMLWindow2こそがJavascriptwindowオブジェクトだったわけです。

そしてJavascriptのwindowオブジェクトは全てのグローバル変数を抱えています。

ということはIHTMLWindow2のプロパティを参照すれば、グローバル変数や関数にホスト側から・・・どうやらこれがHtmlDocument.InvokeScriptの正体のようです。

JavaScript call from C++ - CodeProject

InvokeScript改の夢を見た

さて、そうとわかれば以下の問題も解決できるはずです。

  • 変数の設定や取得ができない。
  • クラス内関数を実行できない。

C#でやるためにはWebBrowserからIHTMLWindow2を手に入れる必要があります。COM上の構造と同じように、documentから辿って・・・HtmlWindow.DomWindowですね。

それで昨日の時点では

これをdynamicなりInvokeMemberなりで呼び出してやれば、と書こうとして2時間ほど経ちました。残念ながらこれらの機能ではIDispatchを素直に(ぶっつけで)呼ぶことができないようで、

  • alertやlocationといったIHTMLWindow2に定義されているメンバは当然呼べる
  • evalはエラーになるが、先にIHTMLWindow2::execScript()すれば呼べるようになる
  • 自分で定義したグローバル変数・関数はそれでもだめ

とかいう謎挙動。

じゃあInvokeScriptは結局なにやってんのさってReferenceSourceを見たら、IDispatchを叩いてたという。

って書いてたんですが、ドキュメントを読み込んだ後に Application.DoEvents() すればよかっただけでした。execScript() は内部的に似たようなことやってそうです。

InvokeScriptdynamicType.InvokeMember
eval
location.host 取得
location 代入
hoge 呼び出し
hoge.prop 取得・代入
hoge.method 呼び出し××
hoge(匿名関数)呼び出し××
Date 呼び出し
new Date()×××

dynamicで匿名関数を呼ぼうとすると、関数呼び出しの形を取っているにも関わらず、関数オブジェクトが返ってくるという。Type.GetMethod()でも取得できないが、BindingFlags.InvokeMethod を指定してType.InvokeMember()すればOK。

InvokeScript: コンストラクタ

Javascriptの場合newを付けて関数を呼び出すとコンストラクタになるという仕様ですが、これはCOMの世界から見ても特殊なケースのようです。なんせIDispatch2でこのためのInvokeExメソッドが追加されたんですから。

Wherefore IDispatchEx? - Fabulous Adventures In Coding - Site Home - MSDN Blogs

Creating JavaScript arrays and other objects from C++ - CodeProject

匿名関数を呼び出した時のようにBindingFlags.CreateInstanceが使えるかと思ったものの、COM Interopした型には使えないようで・・・。

ObjectForScripting: インデクサ

気を取り直して、逆方向。

JavascriptのArrayは結局ExpandoObjectみたいな拡張可能なオブジェクトで実装されているので、行きも帰りも厄介です。順番にappendしてやれば作ることは可能でしょうが、以下略。

とはいえC#から公開するだけなら、自前でComVisibleなコレクションを作ることはできます。インデクサを書くだけ。引数は複数にできますし、セッターも動きます。

ただしVBAJScriptと同様、アクセスにはobj(1)のように丸括弧を使います。

ObjectForScripting: 動的なメソッド・可変長引数

可変長引数とかできないのかなーってparamsをつけただけでは流石に無理でした、はい。

普通にCOMに公開するだけでIDispatchにも対応させてはくれるわけですが、IReflectというインターフェイスを使うことで、IDispatchへの応答を動的に決めることができます。dynamicに使うDynamicMetaObjectを思い出します。

IReflect インターフェイス (System.Reflection)

.net - C# COM object with a dynamic interface - Stack Overflow

上のQ&Aでは動的に追加削除する例が載っていますが、これを活用して可変長引数にも生かせます。

MethodInfoでは引数の情報も提供するのですが、これより多くても少なくてもエラーにはならないようです。また実際に呼び出されるときにはMethodInfo.InvokeではなくIReflect.InvokeMemberが使われるようなので、引数を配列にまとめてparamsに渡すこともできますし、引数の数に応じて別のメソッドを呼ぶことだって可能です。

感想

dynamicで自由自在に呼べるかと思ったんだけどなあ・・・。

おかげで朝日が眩しい。