C#でお手軽ハイブリッドアプリケーション (2)

前書き

前回で、最新IE(事実上、Vista以外ならIE11)を使ったハイブリッドアプリケーションの基本形ができました。

mokake.hatenablog.com

今回は、C#側とJavaScript側で、データのやり取りをしたいと思います。しかも(やや不便ながら)配列やオブジェクトも含めて。

JavaScriptC#でやり取りする

基本的なやり取りは、とても簡単です。

C#からJavaScriptへの値の引き渡し

webBrowser.Document.InvokeScript(string, object[]); を呼び出すだけ。

function test1() {
    alert("test1 called");
}

こういう関数を呼び出すなら、C#側は、

webBrowser.Document.InvokeScript("test1");

ですし、

function test2(arg1, arg2) {
    alert("test2 called : arg1=" + arg1 + " / arg2=" + arg2);
}

のようにJavaScript側関数に引数があれば、C#側も第2引数に入れればOKです。

webBrowser.Document.InvokeScript("test2", new string[] { "str1", "str2" });

JavaScriptからC#への値の引き渡し

こちらは、C#側で呼び出し対象のクラスを決めておき、それをWebBrowser.ObjectForScriptingに登録すればOKです。

ここでは、仮に呼び出し対象クラスを独立に作ってみます。別にFormとかでも構わないようですが。

[ComVisible(true)]
public class Callee
{
    public void CalleeTest1()
    {
        MessageBox.Show("CalleeTest1 called.");
    }

    public void CalleeTest2(string arg1, string arg2)
    {
        MessageBox.Show("CalleeTest2 called. arg1=" + arg1 + " /// arg2=" + arg2);
    }
}

引数なしと引数あり、2種類の関数だけを入れてみました。クラスのComVisible属性は必須です。

これをWebBrowser側に登録します。

private void Form1_Load(object sender, EventArgs e)
{
    // 略
    webBrowser.ObjectForScripting = new Callee();
}

簡易的ですが、これでJavaScript側からの呼び出しが可能になります。

window.external.CalleeTest1();
window.external.CalleeTest2("string1","string2");

こんな感じで呼び出します。引数の数は整合しないとうまくいきません。

配列やオブジェクトのやり取り

上記でC#JavaScriptのやり取りが可能ですが、あくまで、引数や戻り値としては、数値や文字列など、単純なものしか使えません。

qiita.com

unarist.hatenablog.com

unarist.hatenablog.com

という風に各種の方法が提案されています。

でも、個人的には、もっと簡単でもいいんじゃないかなーとか思ったりします。

JavaScriptでオブジェクトを表現といえば、JSONJSONにすれば文字列でやり取りできるので、もう何もこわくありません。

JavaScript側で配列だの連想配列だのオブジェクトだのをJSONにするならwindow.JSON.stringifyですね。

// Object -> JSON
var obj = {
    i: 42,
    f: 3.14,
    b: true,
    s: "[Hello, World!]"
};
window.external.CalleeTest3(window.JSON.stringify(obj));

そして、JSONをオブジェクトなどにするにはwindow.JSON.parseですね。

var obj = window.JSON.parse(arg);

一方、C#JSONというと、Json.NETDynamicJsonなどもありますが、標準でも.NET3.5からはDataContractJsonSerializerがあります。今回は標準でいってみます。

なお、C#JSONXMLを使う場合の注意点については、DynamicJson作者によるまとめが便利です。

neue cc - .NETの標準シリアライザ(XML/JSON)の使い分けまとめ

上記によるDataContractJsonSerializerの弱点は次の通りです。

  • JSONが整形できない(常に1行形式での出力となる)
  • 若干遅い
  • JSONに対応した形式の型を用意しておく必要がある

まあ、C#がWebBrowserをホストする場合、JSONはあくまでC#JavaScriptのやり取り用なので、これらはあまり問題とはならないでしょう。

まずは、参照設定に.NET項目を追加します。追加するのはSystem.Runtime.SerializationSystem.ServiceModel.Web。後者が曲者で、DataContractJsonSerializerに必要なんですよね。なんか名前全然違いますけど。

その上で、ソース側に名前空間を追加しておきましょうか。

using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;

ここでusing行に波線が出る場合は、参照設定ができていないものと思われます。

ここまで問題がなければ、次はJavaScriptとやり取りするデータ用の型を作ります。

    [DataContract]
    public class Data
    {
        [DataMember]
        public int i { get; set; }

        [DataMember]
        public float f { get; set; }

        [DataMember]
        public bool b { get; set; }

        [DataMember]
        public string s { get; set; }
    }

とりあえず感に満ちたクラスですが、大事なのはDataContractDataMember属性。これをつけないと処理できません。

ここまで来れば、いよいよJSON処理です。……といっても、案外面倒なんですよね。そこで任意型に対応するメソッドを書いてみました。

まずはC#のオブジェクトをJSON文字列に変換。

public static string GetJsonString<T>(T src) where T : class
{
    if (src == null) return null;

    var jsoner = new DataContractJsonSerializer(typeof(T));
    var mem = new MemoryStream();
    jsoner.WriteObject(mem, src);

    string json = null;
    try
    {
        json = Encoding.UTF8.GetString(mem.ToArray());
    }
    catch (DecoderFallbackException) { }

    return json;
}

次はJSON文字列をC#オブジェクトに変換。

public static T GetObjectFromJson<T>(string json) where T : class
{
    var jsonee = new DataContractJsonSerializer(typeof(T));
    byte[] bytes = null;
    try
    {
        bytes = Encoding.UTF8.GetBytes(json);
    }
    catch (EncoderFallbackException) { }
    if (bytes == null) return null;
    var mem = new MemoryStream(bytes);
    var obj = (T)jsonee.ReadObject(mem);
    return obj;
}

これで、無事C#でもJSONが扱えるようになりました。

Data d = new Data { i = 1234, f = 2.8F, b = false, s = "From C#" };
string json = null;
try
{
    json = JsonUtility.GetJsonString<Data>(d);
}
catch (Exception) { }
if (json == null)
{
    MessageBox.Show("JSONize failed.");
    return;
}
webBrowser1.Document.InvokeScript("test3", new string[] { json });
Data obj = null;
try
{
    obj = JsonUtility.GetObjectFromJson<Data>(json);
}
catch (Exception) { }
if (obj == null)
{
    MessageBox.Show("CalleeTest3 : failed");
}
else
{
    MessageBox.Show(
        string.Format("CalleeTest3 : int={0} float={1} bool={2} str={3}",
        obj.i, obj.f, obj.b, obj.s));
}

例外処理がちょっとかっこ悪いですね。

でも、これでC#JavaScript間のやり取りは一通りできるようになりました。めでたしめでたし。