都 2021 年了,還需要把 .NET 元件包成 COM+ 給老 ASP/VBScript/IE ActiveX/VB6/Delphi 用的場合如鳳毛麟角,但我有不少文章本來就是寫給有緣人看的 (收到有緣人留言還會一陣激動),這篇文件適合從事古蹟維修的同學,沒聽過 COM+ 的朋友請跳過,把時間花在更有意義的地方。

將 .NET 元件包成 COM+ 元件的技術,學名叫做 COM Callable Wrapper,簡稱 CCW。網路有不少教學,但通常都很簡單,多是傳回現在時間、數字相加之類為賦新辭強說愁之類的應用,但實務上若有用到第三方程式會有一些眉角比較少人討論,這篇將用一個實例示範。

我選擇 XML/JSON 互相轉換當題材,這要在 VBScript 實現有點難度,但偉大的 Json.NET 有現成方法,我們只需寫個元件參照 Json.NET 當白手套,接收 JSON 呼叫 JsonConvert.DeserializeXNode(json)、接收 XML 呼叫 JsonConvert.SerializeXNode(XDocument.Parse(xml).Root) 就能輕鬆搞定,只需處理一下製作 CCW 所需的細節。

範例 CCW 專案的結構像這樣:

我習慣將 gacutil.exe、gacutil.exe.config、gacutlrc.dll 包進專案(參考),並提供 Reg.bat 與 Unreg.bat 以快速註冊及反註冊,另外需要一支 test.vbs 做驗證測試。如此可串接成:改程式 -> 編譯 -> Reg.bat 註冊 -> C:\Windows\SysWow64\cscript test.vbs 測試 -> 發現 Bug -> Unreg.bat 取消註冊 -> 改程式 -> 編譯 -> Reg.bat 註冊... 的開發流程,測試與開發會比較流暢有效率。

由於 CCW 元件一般會註冊到 GAC,需要設定數位簽章:(專案中的 DummyKey.snk 是加密金鑰,每個專案隨便產生一把能簽署即可)

週邊檔案講完了,再來看核心類別 JsonXmlConverter 怎麼寫。先定義一個 IJsonXmlConverter 介面,再寫 JsonXmlConverter 類別實作它。JsonXmlConverter、IJsonXmlConverter 要加上唯一的 [Guid()],並附加 [ComVisible(true)]、[ClassInterface(ClassInterfaceType.None)]、ComDefaultInterface(typeof(IJsonXmlConverter))]、 [ProgId("JsonToolkitCom.JsonXmlConverter")]... 等必要 Attribute。

完整程式範例如下:

using Newtonsoft.Json;
using System;
using System.Runtime.InteropServices;
using System.Xml.Linq;

namespace JsonToolkitCom
{

    [ComVisible(true)]
    [Guid("7FD0B31A-8DAD-4F87-B325-16789EBB985E")]
    [InterfaceType(ComInterfaceType.InterfaceIsDual)]
    public interface IJsonXmlConverter
    {
        string XmlToJson(string xml);
        string JsonToXml(string json);
    }

    [ClassInterface(ClassInterfaceType.None)]
    [ComVisible(true)]
    [Guid("1012B3FF-333B-43CA-A931-A9EAB4A4643E")]
    [ProgId("JsonToolkitCom.JsonXmlConverter")]
    [ComDefaultInterface(typeof(IJsonXmlConverter))]
    public class JsonXmlConverter : IJsonXmlConverter
    {
        public string XmlToJson(string xml)
        {
            return JsonConvert.SerializeXNode(XDocument.Parse(xml).Root);
        }

        public string JsonToXml(string json)
        {
            return JsonConvert.DeserializeXNode(json).ToString();
        }
    }
}

一般簡單的傳回現在時間、數字相加範例這樣就可以跑了,但我們因為用到第三方程式庫,直接執行時會出現找不到 Netwonsoft.Json.dll 錯誤:

最直覺的解法是將 Newtonsoft.Json.dll 也註冊到 GAC,但註冊 GAC 可能會影響系統上其他應用程式的組件解析 (延伸閱讀:ASP.NET /bin 組件載入跟你想的不一樣),故我想到另一種做法是用 ILMerge 將參照組件併進來,但實測發現 MSBuild.ILMerge 跟 CCW 專案不相容,之前 WPF 也遇過類似問題,這裡也比照辦理。在 csproj 加入 <Target Name="AfterResolveReferences"> 併入第三方組件,JsonXmlConverter 則加入靜態建置式掛載 AppDomain.CurrentDomain.AssemblyResolve 以支援從內嵌資源取得組件:

using Newtonsoft.Json;
using System;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Xml.Linq;

namespace JsonToolkitCom
{

    [ComVisible(true)]
    [Guid("7FD0B31A-8DAD-4F87-B325-16789EBB985E")]
    [InterfaceType(ComInterfaceType.InterfaceIsDual)]
    public interface IJsonXmlConverter
    {
        string XmlToJson(string xml);
        string JsonToXml(string json);
    }

    [ClassInterface(ClassInterfaceType.None)]
    [ComVisible(true)]
    [Guid("1012B3FF-333B-43CA-A931-A9EAB4A4643E")]
    [ProgId("JsonToolkitCom.JsonXmlConverter")]
    [ComDefaultInterface(typeof(IJsonXmlConverter))]
    public class JsonXmlConverter : IJsonXmlConverter
    {
        public string XmlToJson(string xml)
        {
            return JsonConvert.SerializeXNode(XDocument.Parse(xml).Root);
        }

        public string JsonToXml(string json)
        {
            return JsonConvert.DeserializeXNode(json).ToString();
        }

        static JsonXmlConverter()
        {
            AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
        }

        private static Assembly OnResolveAssembly(object sender, ResolveEventArgs args)
        {
            Assembly executingAssembly = Assembly.GetExecutingAssembly();
            AssemblyName assemblyName = new AssemblyName(args.Name);

            string path = assemblyName.Name + ".dll";
            if (assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture) == false)
            {
                path = String.Format(@"{0}\{1}", assemblyName.CultureInfo, path);
            }

            using (Stream stream = executingAssembly.GetManifestResourceStream(path))
            {
                if (stream == null)
                    return null;

                byte[] assemblyRawBytes = new byte[stream.Length];
                stream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length);
                return Assembly.Load(assemblyRawBytes);
            }
        }

    }
}

修改後,重跑 test.vbs:

Set conv = CreateObject("JsonToolkitCom.JsonXmlConverter")
WScript.Echo conv.XmlToJson("<User><Id>A001</Id><Name>Jeffrey</Name></User>")
WScript.Echo conv.JsonToXml("{ ""User"": { ""Id"": ""D001"", ""Name"": ""darkthread"" } }")

測試成功!

範例專案已放上 Github 給未來的自己參考(希望是用不到啦),一併分享有需要的朋友(也希望沒人需要啦)。

Example of create a CCW project with 3rd party assembly dependency.


Comments

# by Rain

有緣人在此 ^_^ , 謝謝分享

# by thomas

有緣人感謝分享

# by Ray

感謝分享!知識增加!!

# by Huang

也是以下這樣,直接專案加引用,會直接顯示bin或GAC(可能在web.config出現該元件) 在 csproj 加入 <Target Name="AfterResolveReferences"> 併入第三方組件

# by 洪有德

我在想這個做法的用途。我記得曾經有骨董程式需要用。因為當時沒經驗就放棄了。現在,記起來,下次預到類似的問題可以試試。 謝謝黑大。

# by Clark

這篇文件適合從事古蹟維修的同學,沒聽過 COM+ 的朋友請跳過,把時間花在更有意義的地方 =>推這段說明文字放第一段落,棒棒

Post a comment