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

  • 41
    Like
  • 0
    Comment
More than 1 year has 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 などと連携すると更に面白くなりそうな感じですね。