C# ActiveX Development, Deploying and Web Execution

最近在用 C# 開發 ActiveX 來擺在網頁上面執行,查了一些資料之後終於做出來了,來整理一下放在下面。

這次主要是要做兩個 ActiveX control,雖然介面類似但是功能不同,一個叫 FullControl,另一個叫做 TestControl。

建立User Control

跟過去用 MFC 或者用 VB 寫 ActiveX 有點不太一樣,在 .net 裡頭是用 User Control 來做出 ActiveX control,所以先在 VS 2005 裡頭開一個新的 User Control 的專案,然後把兩個 Control 加進去。

所以現在會有兩個檔案: FullControl.cs 跟 TestControl.cs。大概會長得類似下面這樣...

namespace ActiveXTest
{
    public partial class TestControl : UserControl
    {
        public TestControl()
        {
            InitializeComponent();
        }
        //其他的程式碼
    }
}
namespace ActiveXTest
{
    public partial class FullControl : UserControl
    {
        public TestControl()
        {
            InitializeComponent();
        }
        //其他的程式碼
    }
}

接下來就是製作需要的功能了,做好之後可以另外開一個 windows form project 來測試做好的 User Control 是否可以正常運作。

讓這個 User Control 變成 COM 物件

這是最重要的步驟!!

雖然 .net 做出來的 User Control 也是一個 .dll 檔,但是這跟過去用 VB 寫出來的一個 .ocx 或者 .dll,用 MFC 寫出來的一個 .dll 是截然不同的。

因為 ActiveX control 基本上是一個 com 物件,但是一般情況下 .net 做出來的 User control dll 並不是,所以我們要對 User Control 進行改造,使得這個 User Control 變成一個 COM 物件。

要變成 COM 物件首先我們需要建立這個 dll 的 COM Interface,所以要先幫 User Control 加上 IObjectSafty interface。

    [ComImport, GuidAttribute("GUID")]
    [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IObjectSafety
    {

        [PreserveSig]
        int GetInterfaceSafetyOptions(ref Guid riid,
                      [MarshalAs(UnmanagedType.U4)] ref int pdwSupportedOptions,
                      [MarshalAs(UnmanagedType.U4)] ref int pdwEnabledOptions);

        [PreserveSig()]
        int SetInterfaceSafetyOptions(ref Guid riid,
                      [MarshalAs(UnmanagedType.U4)] int dwOptionSetMask,
                      [MarshalAs(UnmanagedType.U4)] int dwEnabledOptions);
    }

要注意的是,因為現在 FullControl 跟 TestControl 都在同一個 namespace (ActiveXTest) 底下,所以這個 IObjectSafety interface 只要寫一個就好,看是寫在 FullControl.cs 或者 TestControl.cs 都可以。

而那個 GUID 的話可以用 GUID 工具產生,它放在 VS 2005 的 "工具" --> "建立GUID"。如果沒有這工具的話可以用 "外部工具" 來新增這個工具,預設路徑是在 C:\Program Files\Microsoft Visual Studio 8\Common7\Tools\guidgen.exe 。產生的 Interface 可能像下面這樣:

namespace ActiveXTest
{    [ComImport, GuidAttribute("CB5BDC81-93C1-11CF-8F20-00805F2CD064")]
    [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IObjectSafety
    {
    ...
    }
}

寫好 Interface 之後接著就是需要 implement 這個 interface 中的 methods,所以兩個 User Control 都需要繼承這個 interface...

namespace ActiveXTest
{
    public partial class TestControl : UserControl, IObjectSafety
    {
    ...
    }

    [ComImport, GuidAttribute("CB5BDC81-93C1-11CF-8F20-00805F2CD064")]
    [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IObjectSafety
    {
    ...
    }
}
namespace ActiveXTest
{
    public partial class FullControl : UserControl, IObjectSafety
    {
    ...
    }

    [ComImport, GuidAttribute("CB5BDC81-93C1-11CF-8F20-00805F2CD064")]
    [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IObjectSafety
    {
    ...
    }
}

而怎麼 implement int GetInterfaceSafetyOptions() 跟 int SetInterfaceSafetyOptions() 呢?直接去 pinvoke.net 這邊去抄 :p

接下來也要給 User Control 一個 GUID,要不然也空有 Interface 但不知道 ActiveX control 實際的 GUID 的話也是不行的。

namespace ActiveXTest
{
    [Guid ("9551B223-6188-4387-B293-C7D9D8173E3A")]
    [ProgId("ActiveXUpload.TestControl")]
    [ComVisible(true)]
    public partial class TestControl : UserControl, IObjectSafety
    {
    ...
    }

    [ComImport, GuidAttribute("CB5BDC81-93C1-11CF-8F20-00805F2CD064")]
    [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IObjectSafety
    {
    ...
    }
}
namespace ActiveXTest
{
    [Guid ("9551B223-6188-4387-B293-C7D9D8173E3A")]
    [ProgId("ActiveXUpload.FullControl")]
    [ComVisible(true)]
    public partial class FullControl : UserControl, IObjectSafety
    {
    ...
    }

    [ComImport, GuidAttribute("CB5BDC81-93C1-11CF-8F20-00805F2CD064")]
    [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IObjectSafety
    {
    ...
    }
}

這樣子就已經差不多了,但是在建置之前必須要設定一下 project 的屬性,把「註冊為 Com Interop」選項勾選起來。另外還要把這個專案作簽署,就是把專案屬性的簽署組件勾選起來。這樣子,建置之後應該會有 .dll 以及一個 .tlb (typed library),其實到這邊就可以把 ActiveX 的 DLL 拿來用了,可是這時候如果要在網頁上面使用這個 ActiveX 的話,要做一大堆的設定,所以,還是乖乖地包裝成 CAB 吧。

建立 ActiveX 自動安裝專案

要建立 ActiveX 的 CAB 時,需要 Cabarc.exe,這個tool在 VS 2005 裡頭就會有了,預設的話會在 C:\Program Files\Microsoft Visual Studio 8\Common7\Tools\Bin 裡頭。要不然也可以在 Microsoft 下載中心去下載 Windows XP Service Pack 2 支援工具,雖然在 Windows 的 server 版本中無法安裝,但是可以用 winrar 來解壓縮把 cabarc.exe 拿出來用。

如果不想要去下載 XP 支援工具的話,還是有方法可以解決的。首先還是先在 VS 2005 裡頭作一個安裝的專案,然後就可以產生出一個 .msi 跟一個 setup.exe 檔案。但接下來的我們只需要 .msi,這也是為什麼我們不直接建立一個 CAB 封裝的專案,而是先建立一個自動安裝的專案。

建立 ActiveX 的 CAB 封裝

接下來在 VS 2005 裡頭建立一個 CAB 封裝專案,(請直接看最下方的紅色文字部分補充說明) 然後加入前一個步驟的 .msi 檔案,以及一個 install.inf 檔案,比方說下面這樣:

[version] signature="$CHICAGO$"
AdvancedINF=2.0

[Setup Hooks] hook1=hook1

[hook1] run=msiexec.exe /i "%EXTRACT_DIR%\ActiveXTest.msi" /qn

Signature 的值可以是 $Windows NT$, $Windows 95$, $Chicago$。其中 $Windows NT$ 為NT系列專用,$Windows 95$ 為 windows 9x系列專用,而 $Chicago$ 是除了WIN3X以下系統其他WIN系統都可以用,所以當然是用 $Chicago$ 囉。產生出 cab 之後就可以在網頁上使用了。

如果要把 install.inf 寫得更好一點的話可以寫成類似下面這樣:

[Setup Hooks] hook.mycontrol=hook.mycontrol

[Add.Code] mycontrol.dll=mycontrol.dll

[mycontrol.dll] CLSID={50894390-9cb3-4a6d-bb40-b786b7e9548d}
FileVersion=1,1,0,3
hook=hook.mycontrol

[hook.mycontrol] run=msiexec.exe /i %EXTRACT_DIR%\mycontrol.msi /qn

[Version] ; This section is required for compatibility on both Windows 95 and Windows NT.
Signature="$CHICAGO$"
AdvancedInf=2.0

如果想要讓這個 CAB 做得更完美一點的話,還可以對這個 CAB 作簽章。這時候要用到 signtool.exe ,也是放在 C:\Program Files\Microsoft Visual Studio 8\Common7\Tools\Bin 裡面。使用 signtool.exe wizard 之後就會彈出視窗來做簽署動作。

Jun. 1, 2010: 經過反覆測試發現,還是用 Cabarc.exe 自己來包 .cab 比較好一點。只要用 cabarc.exe N ActiveXTest.cab ActiveXTest.msi install.inf 這樣的指令就可以了。如果用 VS 2005 的 CAB 專案,則會在產生出來的 .cab 檔案中多一個 xxx.OSD 這樣的一個 OSD (open source description) 檔案,而導致在使用這個 activex cab 包的時候發生錯誤無法正確安裝。