ScriptObjectからJsonValueへの変換

昨日のエントリで、

個人的にはScriptObjectからJsonValueにいきなりキャストできたら最高だった。

なんてことをつぶやいていたけど、せっかくなんで作ってみた。

JsonValueオブジェクトはJSON文字列からしか生成できないけど、Silverlightは生の(文字列ではない)JSONオブジェクトを扱えるので(ScriptObjectの事ね)、そこからJsonValueオブジェクトを生成したいという話。

これをやるにはScriptObjectをJSON文字列にしてやればいいだけなんだけど、「toSource」メソッドをサポートするブラウザ*1なら話は簡単で、以下のようにすればいい。

// JSONオブジェクトを返すgetJsonというJavaScript関数があるとする。
var obj = (ScriptObject)HtmlPage.Window.Invoke("getJson");

var json = JsonValue.Parse(obj.Invoke("toSource").toString());

でも、「toSource」メソッドをサポートしないブラウザ*2ではこれができないので、自力でJSON文字列に変換する必要がある。結構面倒くさそうだったけど、やってみたら案外簡単にできた。

まずSilverlightコンテンツをホストするHTMLページに以下のJavaScript関数が定義されているとする。

Default.aspx
function getJson() {
    return [
        { name:"John", age:20 },
        { name:"Bob", age:26 }
    ];
}

「name」と「age」という属性を持つオブジェクトを配列として返す関数。

この関数をSilverlight側から呼び出すには以下のようにすればいい。

var objs = (ScriptObject)HtmlPage.Window.Invoke("getJson");

Invokeメソッドの返り値として生のJSONオブジェクトが返ってくるので、これをJSON文字列に変換してJsonValueオブジェクトを生成するクラスを作ればいい。それが以下。

ScriptObjectExtension.cs

using System;
using System.Text;
using System.Linq;
using System.Json;
using System.Windows.Browser;
using System.Collections.Generic;
using System.Diagnostics;

namespace System.Json.Extensions {
    /// <summary>
    /// ScriptObjectの拡張クラス
    /// </summary>
    public static class ScriptObjectExtension {
        /// <summary>
        /// 指定したScriptObjectをJsonValueオブジェクトに変換します。
        /// </summary>
        /// <typeparam name="T">JsonValueかその派生型</typeparam>
        /// <param name="obj">オブジェクト</param>
        /// <returns>Jsonオブジェクト</returns>
        public static T ToJson<T>(this ScriptObject obj) where T : JsonValue {
            string jsonString = string.Empty;

            if(obj.GetProperty("toSource") != null) {
                // Firefox
                jsonString = obj.Invoke("toSource").ToString();

            } else {
                // IE
                jsonString = ToJsonString(obj);
            }
            return (T)JsonValue.Parse(jsonString);
        }

        static string ToJsonString(object obj) {
            var buf = new StringBuilder();

            if(obj is ScriptObject) {
                var sobj = (ScriptObject)obj;

                if(sobj.Is("Array")) {
                    buf.Append('[');
                    buf.Append(
                        string.Join(",", sobj.ForEachArray(o => ToJsonString(o)).ToArray())
                    );
                    buf.Append(']');

                } else if(sobj.Is("String")) {
                    buf.AppendFormat("\"{0}\"", sobj.ConvertTo<string>());

                } else if(sobj.Is("Boolean")) {
                    buf.Append(sobj.ConvertTo<bool>());

                } else if(sobj.Is("Number")) {
                    buf.Append(sobj.ConvertTo<double>());

                } else if(sobj.Is("Function") || sobj.Is("RegExp")) {
                    // こいつらはどうしようもないから、文字列にでも変換しとく。
                    buf.Append(sobj.Invoke("toString"));

                } else {
                    // object
                    buf.Append('{');
                    buf.Append(
                        string.Join(",", sobj.ForEachMembers(
                            name => string.Format("{0}:{1}", name, ToJsonString(sobj.GetProperty(name)))

                        ).ToArray())
                    );
                    buf.Append('}');
                }

            } else {
                var type = obj.GetType();

                if(type == typeof(string)) {
                    // string
                    buf.AppendFormat("\"{0}\"", obj.ToString());

                } else {
                    // boolean or number or etc...
                    buf.Append(obj);
                }
            }
            return buf.ToString();
        }

        [DebuggerStepThrough]
        static bool Is(this ScriptObject obj, string typeName) {
            const string VAR_NAME = "__ctor__";

            HtmlPage.Window.SetProperty(VAR_NAME, obj);
            try {
                return (bool)HtmlPage.Window.Eval(
                    string.Format("{0}.constructor == {1}", VAR_NAME, typeName)
                );

            } finally {
                // 一応
                HtmlPage.Window.SetProperty(VAR_NAME, null);
            }
        }

        static IEnumerable<string> ForEachArray(this ScriptObject obj, Func<object, string> func) {
            var length = (double)obj.GetProperty("length");

            for(var i = 0; i < length; i++) {
                yield return func(obj.GetProperty(i.ToString()));
            }
        }

        static IEnumerable<string> ForEachMembers(this ScriptObject obj, Func<string, string> func) {
            const string OBJ_NAME = "__obj__";
            const string VAR_NAME = "__names__";

            var names = HtmlPage.Window.CreateInstance("Array");

            HtmlPage.Window.SetProperty(OBJ_NAME, obj);
            HtmlPage.Window.SetProperty(VAR_NAME, names);
            try {
                HtmlPage.Window.Eval(
                    string.Format("for(var n in {0}) {1}.push(n);", OBJ_NAME, VAR_NAME)
                );
                return names.ForEachArray(name => func(name.ToString()));

            } finally {
                HtmlPage.Window.SetProperty(OBJ_NAME, null);
                HtmlPage.Window.SetProperty(VAR_NAME, null);
            }
        }
    }
}

JavaScript演算子とかを呼び出すためにEvalを使っているので、パフォーマンスが悪いかもしれないけど他にやりようがなかった。

このクラスを使ってScripObjectからJsonValueへの変換を行うコードが以下。

Page.xaml.cs

using System;
using System.Text;
using System.Linq;
using System.Json;
using System.Json.Extensions;
using System.Windows.Browser;
using System.Windows.Controls;
using System.Collections.Generic;
using System.Diagnostics;

namespace SilverlightJson {
    public partial class Page : UserControl {
        public Page() {
            InitializeComponent();

            var objs = (ScriptObject)HtmlPage.Window.Invoke("getJson");

            var result = from o in objs.ToJson<JsonArray>()
                         select new {
                             Name = (string)o["name"],
                             Age = (int)o["age"]
                         };

            foreach(var o in result) Debug.WriteLine(o);
        }
    }
}

デバッグ出力

{ Name = John, Age = 20 }
{ Name = Bob, Age = 26 }

あまりこういうことをする機会は無いかもしれないけど、JavaScriptとの相互運用という点では興味深い。

というか、IEが「toSource」メソッドをサポートしてくれればもっと簡単にできたんですけどね!!

ソース

*1:Firefoxとか

*2:IEのこと