Managed コードから JavaScript のファンクションを呼ぶ

作ってみた。

using System;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Browser.Serialization;

[Scriptable]
public class JavaScript
{
  private static JavaScript instance = new JavaScript();

  public static void Initialize()
  {
  }

  public static void Invoke(string methodName, params object[] args)
  {
    instance.CallScript(methodName, args);
  }

  public static void Alert(string message)
  {
    instance.CallScript("alert", message);
  }

  private JavaScript()
  {
    WebApplication.Current.RegisterScriptableObject("Managed", this);
  }

  private void CallScript(string methodName, params object[] args)
  {
    if (Call != null) 
    {
      Call(this, new JavaScriptArgs(methodName, (new JavaScriptSerializer()).Serialize(args)));
    }
  }

  [Scriptable]
  public event EventHandler<JavaScriptArgs> Call;
}

public class JavaScriptArgs : EventArgs
{
  public JavaScriptArgs(string methodName, string jsonArguments)
  {
    this.MethodName = methodName;
    this.JsonArguments = jsonArguments;
  }

  [Scriptable]
  public string MethodName { get; private set; }

  [Scriptable]
  public string JsonArguments { get; private set; }
}

細かい説明は省く。RegisterScriptableObject と Scriptable で検索をかけると引っかかる。

しかし、いくつか引っかかったところがあるためメモ。

公開するイベントの型はEventHandlerでないとだめ。

動かしたらエラーが発生した。
でも、そもそも EventHandler 以外でイベントを定義できるのかは知らなかったりするので、やはりそもそもダメなのかもしれないため、当然のごとくこのケースでもダメなだけなのかもしれない。
っていうか、でもコンパイルは通っているからそもそもダメってこともなさそうだ。

JavaScript側で配列とか受け取れない。

きっちり試してないのだが JavaScriptEventArgs の JsonArguments はもともと object の配列だったのだが、JavaScript で見たときに null と言われた。string の配列でも駄目だった。
これもきっちり試してないから確証は持てないのだが、単純な値(数値とか文字列とか)じゃないとだめなのかも知れない。なので、いったん JSON に Serialize して文字列として渡すようにしている。

使い方

まず、最初に JavaScript.Initialize を呼んでおく。一番最初によばれるクラス(サンプルとかだと Page) のコンストラクタに書いとくのがベター。
とくにこのメソッドで何かしてるってわけじゃない、っていうか中身の実装がないのでなにもしてないのだけど、これを呼ぶことでスタティックなインスタンスが初期化されるから、メンバー変数にある自分のインスタンスも初期化されてコンストラクターが走って、JavaScriptに登録される。
JavaScript側でイベントを取得するコードが走るより先に登録されてないと例外が発生する。

あとは Invoke メソッドの第1引数に実行する JavaScript のファンクション名で、それ以降にそのファンクションに渡す引数を指定する。

JavaScript.Invoke("Foo.Greeting","Ya!");

Alert は JavaScript の alert を呼び出す。

JavaScript.Alert("Hello.");


続いて JavaScript 側のコード。

sender.Content.Managed.Call = function(sender,args){
  var execExemptionMethod = function(methodName){
    switch(methodName){
      case "alert" : 
        alert(eval(args.JsonArguments));
        return true;
    }
  }
  if(execExemptionMethod(args.MethodName)) return;
  var names =  args.MethodName.include(".") ? 
    args.MethodName.split(".") : 
    [args.MethodName];
  var obj = window;
  for(var i = 0; i < names.length; i++){
    if(typeof obj[names[i]] != "undefined") obj = obj[names[i]];
    else return;
  }
  try{
    obj.apply(null,eval(args.JsonArguments));
  }catch(e){;}
}

onLoad の後だったらどこで動いてもかまわない。簡単に説明すると、、

  1. 特別扱いようのファンクションを実行するファンクションを定義 ← あまり美しくないけどとりあえず。
  2. 最初に特別扱いのファンクションを実行してしまう。実行されたら終わる。
  3. "."で分けて配列にする。"."がない場合は今の値だけで配列にする。←ちなみ include ってのは Prototype.js が拡張してくれるファンクション。
  4. 分けた名前をループしてオブジェクト階層をたどる。
  5. 見つけたメソッドを実行。

ところどころに手抜きが入ってるけど、多くの場合はこれで動くので今のところOKっていうことで。