Edited at

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

More than 3 years have passed since last update.


はじめに

C# で UI を HTML/CSS/JavaScript で実装するなら、フォームに System.Windows.Forms.WebBrowser をドッキングして InvokeScript や ObjectForScripting で連携すると思います。

C# から JavaScript の関数を実行

// http://msdn.microsoft.com/ja-jp/library/4b1a88bz(v=vs.80).aspx

webBrowser.Document.InvokeScript(string, object[]);

JavaScript から C# のオブジェクトにアクセス


TestClass.cs

[System.Runtime.InteropServices.ComVisibleAttribute(true)]

public class TestClass
{
public string TestFunc()
{
return "foo";
}
}

webBrowser.ObjectForScripting = new TestClass();



Test.js

var foo = window.external.TestFunc();


これらだけでもちょっとした連携を楽しめますが、もっと色々しようと思うといくつか嫌なことに気が付きます。


嫌なとこ


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


  • 変数の設定や取得ができない。

  • 戻り値が配列や連想配列の場合は ComObject が返ってくる。

  • 引数として配列や連想配列を渡せない。

  • クラス内関数を実行できない。


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


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


じゃあどうするか

以下のクラスで嫌なことは大体解決しています。Eval が大活躍です。

.net3.5 で動作させるため、dynamic 型は使用していません。でも多分使ったほうが楽だと思います。


JavaScript.cs

namespace MMFrame

{
/// <summary>
/// <see cref="System.Windows.Forms.WebBrowser"/> を利用した JavaScript との連携に関するクラス
/// </summary>
public class JavaScript
{
/// <summary>
/// JavaScript を実行する <see cref="System.Windows.Forms.WebBrowser"/> を取得、設定します。
/// </summary>
private System.Windows.Forms.WebBrowser WebBrowser
{
get;
set;
}

/// <summary>
/// <see cref="MMFrame.JavaScript"/> オブジェクトを生成します。
/// </summary>
/// <param name="dstWebBrowser">Javascript を実行する WebBrowser</param>
public JavaScript(System.Windows.Forms.WebBrowser dstWebBrowser)
{
this.WebBrowser = dstWebBrowser;
}

/// <summary>
/// JavaScript からアクセスできるオブジェクトを設定します。
/// object は [System.Runtime.InteropServices.ComVisibleAttribute(true)] が設定されている必要があります。
/// </summary>
public object ObjectForScripting
{
get
{
return this.WebBrowser.ObjectForScripting;
}
set
{
this.WebBrowser.ObjectForScripting = value;
}
}

/// <summary>
/// 現在読み込んでいる HTML に JavaScript を追加します。
/// </summary>
/// <param name="scriptSrc">追加する Javascript のソースコード</param>
public void AddScript(string scriptSrc)
{
if (this.WebBrowser.Document == null || this.WebBrowser.Document.Body == null)
{
return;
}

System.Windows.Forms.HtmlElement el = this.WebBrowser.Document.CreateElement("script");
el.InnerHtml = scriptSrc;
this.WebBrowser.Document.Body.InsertAdjacentElement(System.Windows.Forms.HtmlElementInsertionOrientation.BeforeEnd, el);
}

/// <summary>
/// 変数を取得します。
/// </summary>
/// <param name="varName">変数名</param>
/// <returns>変数の値</returns>
public object GetVariable(string varName)
{
return GetVariable(varName, null);
}

/// <summary>
/// 変数を取得します。
/// </summary>
/// <param name="varName">変数名</param>
/// <param name="varKeys">戻り値が連想配列の場合は key の配列</param>
/// <returns>変数の値</returns>
public object GetVariable(string varName, string[] varKeys)
{
object scriptResult = this.WebBrowser.Document.InvokeScript("eval", new object[] { varName });
return ConvertObject(scriptResult, varKeys);
}

/// <summary>
/// 変数を設定します。
/// </summary>
/// <param name="varName">変数名</param>
/// <param name="varValue">変数の値</param>
public void SetVariable(string varName, object varValue)
{
this.WebBrowser.Document.InvokeScript("eval", new object[] { varName + "=" + ConvertArg(varValue) + ";" });
}

/// <summary>
/// 関数を実行します。
/// </summary>
/// <param name="funcName">関数名</param>
/// <returns>関数の戻り値</returns>
public object Invoke(string funcName)
{
return Invoke(funcName, null, null);
}

/// <summary>
/// 関数を実行します。
/// </summary>
/// <param name="funcName">関数名</param>
/// <param name="funcArg"><paramref name="funcName"/> の引数。object[0] が第1引数。</param>
/// <returns>関数の戻り値</returns>
public object Invoke(string funcName, object[] funcArg)
{
return Invoke(funcName, funcArg, null);
}

/// <summary>
/// 関数を実行します。
/// </summary>
/// <param name="funcName">関数名</param>
/// <param name="funcArg"><paramref name="funcName"/> の引数。object[0] が第1引数。</param>
/// <param name="varKeys">戻り値が連想配列の場合は key の配列</param>
/// <returns>関数の戻り値</returns>
public object Invoke(string funcName, object[] funcArg, string[] varKeys)
{
if (System.String.IsNullOrEmpty(funcName))
{
return null;
}

object scriptResult = null;

if (funcArg == null)
{
// 引数なし
if (funcName.Contains("."))
{
// クラス内関数
scriptResult = this.WebBrowser.Document.InvokeScript("eval", new object[] { funcName + "();" });
}
else
{
// 通常の関数 or 匿名関数
scriptResult = this.WebBrowser.Document.InvokeScript(funcName);
}
}
else
{
// 引数あり
bool inArr = false;

foreach (object arg in funcArg)
{
if (arg.GetType().IsArray)
{
inArr = true;
break;
}
}

if (!funcName.Contains(".") && !inArr)
{
// クラス内関数ではない && 引数に配列なし
scriptResult = this.WebBrowser.Document.InvokeScript(funcName, funcArg);
}
else
{
// それ以外
string arg = ConvertArg(funcArg);
arg = arg.Substring(1, arg.Length - 2);
scriptResult = this.WebBrowser.Document.InvokeScript("eval", new object[] { funcName + "(" + arg + ");" });
}
}

return ConvertObject(scriptResult, varKeys);
}

/// <summary>
/// JavaScript に値を返します。JavaScript から C# の関数を実行して、戻り値が配列だった場合に使用します。
/// </summary>
/// <param name="varValue">変数</param>
/// <returns>変換された変数</returns>
public object Return(object varValue)
{
return (varValue == null || !varValue.GetType().IsArray) ? varValue : this.WebBrowser.Document.InvokeScript("eval", new object[] { ConvertArg(varValue) + ";" });
}

/// <summary>
/// 引数を適切な文字列に変換します。
/// </summary>
/// <param name="arg">引数</param>
/// <returns>文字列に変換された引数</returns>
public static string ConvertArg(object arg)
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();

if (arg == null)
{
sb.Append("undefined");
}
else if (arg is System.Collections.Generic.Dictionary<string, object>)
{
// 連想配列
int count = 0;
System.Collections.Generic.Dictionary<string, object> hashTable = arg as System.Collections.Generic.Dictionary<string, object>;

sb.Append("{");

foreach (string key in hashTable.Keys)
{
if (count != 0)
{
sb.Append(",");
}

sb.AppendFormat("{0}:{1}", key, ConvertArg(hashTable[key]));

count++;
}

sb.Append("}");
}
else if (arg.GetType().IsArray)
{
// 配列
int count = 0;
sb.Append("[");

foreach (object val in (arg as System.Array))
{
if (count != 0)
{
sb.Append(",");
}

sb.Append(ConvertArg(val));
count++;
}

sb.Append("]");
}
else
{
// string や int
if (arg is System.String)
{
sb.AppendFormat("'{0}'", arg.ToString().Replace("\r", "").Replace("\n", "").Replace("'", "\\'"));
}
else
{
sb.Append(arg.ToString());
}
}

return sb.ToString();
}

/// <summary>
/// JavaScript の Object を変換します。
/// </summary>
/// <param name="jsObject">JavaScript の object</param>
/// <param name="jsObjectKeys">連想配列の場合は key の配列</param>
/// <returns>変換された object</returns>
public static object ConvertObject(object jsObject, string[] jsObjectKeys)
{
object result = null;

if (IsComObject(jsObject))
{
// ComObjectだった場合
if (IsJsArray(jsObject))
{
// 配列だった場合
object[] objArray = new object[(int)GetProperty(jsObject, "length", null)];

for (int i = 0; i < objArray.Length; i++)
{
objArray[i] = GetProperty(jsObject, i.ToString(), null);
objArray[i] = ConvertObject(objArray[i], jsObjectKeys);
}

result = objArray;
}
else if (jsObjectKeys != null)
{
// 連想配列だった場合
System.Collections.Generic.Dictionary<string, object> objDic = new System.Collections.Generic.Dictionary<string, object>();

for (int i = 0; i < jsObjectKeys.Length; i++)
{
objDic[jsObjectKeys[i]] = GetProperty(jsObject, jsObjectKeys[i], null);
objDic[jsObjectKeys[i]] = ConvertObject(objDic[jsObjectKeys[i]], jsObjectKeys);
}

result = objDic;
}
else
{
// こ…これ…これは………… ComObject だあああああ┗(^o^)┛wwwww┏(^o^)┓ドコドコドコドコwwwww
result = jsObject;
}
}
else
{
// string や int などだった場合
result = jsObject;
}

return result;
}

/// <summary>
/// ComObject かどうか評価します。
/// </summary>
/// <param name="jsObject">評価する object</param>
/// <returns>ComObject の場合は true</returns>
private static bool IsComObject(object jsObject)
{
return jsObject.GetType().IsCOMObject;
}

/// <summary>
/// JavaScript の配列かどうか評価します。
/// </summary>
/// <param name="jsObject">評価する object</param>
/// <returns>配列の場合は true</returns>
private static bool IsJsArray(object jsObject)
{
int length = 0;

if (jsObject != null && IsComObject(jsObject))
{
// これはひどい
try
{
length = (int)GetProperty(jsObject, "length", null);
}
catch
{
length = 0;
}
}

return (length > 0);
}

/// <summary>
/// 指定したプロパティを取得します。
/// </summary>
/// <param name="jsObject">プロパティを取得する object</param>
/// <param name="propertyName">プロパティ名</param>
/// <param name="args">呼び出すメンバに渡される引数を格納する配列</param>
/// <returns>取得したプロパティの値</returns>
private static object GetProperty(object jsObject, string propertyName, object[] args)
{
return jsObject.GetType().InvokeMember(propertyName, System.Reflection.BindingFlags.GetProperty, null, jsObject, args);
}
}
}


不規則な連想配列には勝てなかったよ…。


おわりに

不規則な連想配列やその他オブジェクトには未対応ですが、これで UI はHTML/CSS/Javascript 、中身は C# ってことが不自由なく可能になりました。

IronPython などと連携すると更に面白くなりそうな感じですね。