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」メソッドをサポートしてくれればもっと簡単にできたんですけどね!!