C# における COM のレイトバインディング（動的参照）

# COM のレイトバインディング
COM を参照設定して利用するのは静的参照（アーリーバインディング）で、参照無しに動的に利用できるようにすることを動的参照（レイトバインディング）と言います。

# 記事にするきっかけ
C# 環境で COM クライアント側のレイトバインディングのサンプルを探しましたが、わかりやすいものが見当たりませんでした。
そのため、自分のサンプル実装をふまえてメモとして残しておくことにしました。
Windows Script で ActiveX を制御するようなコードを書いたことがあれば、それを C# で記述したものと考えてください。


# 環境

- Windows 10 Professional
- Visual Studio 2019 Professional
- COM Automation サーバー（C++, MFC で作成したダイアログベースアプリケーション）
- COM クライアントアプリケーション（ここで実装した C# WPF アプリケーション）

# C# でのサンプルコード

## COM インスタンスの生成

COM インスタンスを dynamic 型で定義し、ProgID を指定して生成するだけです。

```csharp
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Runtime.InteropServices;
using System.Threading;
using System.Runtime.CompilerServices;          // for MethodImpl
using System.Runtime.InteropServices.ComTypes;  // for IConnectionPointContainer, IConnectionPoint
using System.Windows.Threading;

namespace WpfApp1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        protected dynamic m_comInstance = null;   // COM インスタンス

        private void CreateButton_Click(object sender, RoutedEventArgs e)
        {
            if (null == m_comInstance)      
            {
                ///////////////////////////////////
                // COM のインスタンスを作成する
                const string progID = "OutOfProcessSample.Application";　// ProgID を指定する

                Type comType = Type.GetTypeFromProgID(progID);
                m_comInstance = Activator.CreateInstance(comType);
                Debug.WriteLine("Instance created.");

```

## COM インスタンスの開放

```csharp
        private void QuitButton_Click(object sender, RoutedEventArgs e)
        {
            if (null != m_comInstance)
            {

                // Quit でアプリケーションを閉じる
                // のような処理メソッドがある場合は、以下。
                // そのようなメソッドがない場合、「（一般的な）COMオブジェクトの開放」のコードを利用する
                m_comInstance.Quit();
                m_comInstance = null;

                // （一般的な）COMオブジェクトの開放
                //Marshal.ReleaseComObject(m_comInstance);
                //m_comInstance = null;
            }
            return;
        }

```

## 属性の取得、設定、メソッドの呼び出し

普通に呼び出すだけです。
属性やメソッドにどのようなものがあるかは、Windows Kit に付属の 「OLE/COM Object Viewer」アプリで確認するとよいでしょう。

```csharp
                ///////////////////////////////////
                // 属性の取得

                object name = m_comInstance.Name;
                Debug.WriteLine("Name = {0}", name);

                object id = m_comInstance.Id;
                Debug.WriteLine("Id = {0}", id);

                object status = m_comInstance.Status;
                Debug.WriteLine("Status = {0}", status);

                ///////////////////////////////////
                // 属性の設定

                m_comInstance.Visible = -1;
                Thread.Sleep(1000);

                ///////////////////////////////////
                // メソッドの呼び出し

                m_comInstance.SetRect(0, 0, 200, 200);

                Thread.Sleep(1000);
                m_comInstance.SetRect(100, 100, 400, 400);
```

## イベントの受信

COM サーバーのイベントインターフェースに対応する場合のみ実装が必要です。
イベントインターフェースを受信する手順とコードは以下のようになります。

### メンバ の用意
```csharp
        protected int m_sinkCookie = -1;          // イベントインターフェース Cookie
        protected IConnectionPoint m_connectionPoint = null;      // イベント接続ポイント
        protected OutOfProcessEvents_SinkHelper m_sink = null;    // イベントシンク
```

### イベント受信設定：イベント接続部

```csharp
                ///////////////////////////////////
                // COM からのイベント受信

                // COM インスタンスから、IConnectionPointContainer を取得する
                IConnectionPointContainer connectionPointContainer = m_comInstance as IConnectionPointContainer;

                // 指定 GUID から IConnectionPoint を取得する
                Guid guid = new Guid("00000000-0000-0000-0000-000000000000");   // ← イベントインターフェースIDを指定
                connectionPointContainer.FindConnectionPoint(ref guid, out m_connectionPoint);

                m_sink = new OutOfProcessEvents_SinkHelper(); // イベント受信クラスの生成
                m_sink.win = this;    // イベント受信の結果を画面に反映するためにセットする。（独自実装：無くてもよい）

                m_connectionPoint.Advise(m_sink, out m_sinkCookie); // イベント受信クラスを接続
                Debug.WriteLine("IConnectionPoint::Advise ok.");
```

### イベント受信設定：イベント受信クラス

```csharp
        // COM のイベントに関連する enum も記載しておく。（後述の「イベント受信インターフェース記述の手順」を参照）
        public enum LibStatus
        {
            LibStatus_Fatal = -1, // 0xFFFFFFFF
            LibStatus_Unknown = 0,
            LibStatus_Ready = 1,
            LibStatus_Busy = 2,
            LibStatus_Done = 3,
            LibStatus_Error = 1000
        }

        // イベント受信インターフェース（後述の「イベント受信インターフェース記述の手順」を参照）
        [TypeLibType(4096)]
        [Guid("00000000-0000-0000-0000-000000000000")] // ← 具体的なイベントIF の ID を記載します
        [InterfaceType(2)]
        [ComImport]
        public interface _IOutOfProcessSampleEvents
        {
            [DispId(1)]
            [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            void ClickIn([ComAliasName("stdole.OLE_XPOS_PIXELS")] int xCoord, [ComAliasName("stdole.OLE_YPOS_PIXELS")] int yCoord);

            [DispId(2)]
            [MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            void StatusChange(LibStatus Status);
        }

　　　　// イベント受信クラス（イベント受信インターフェースのラッパークラスを自前で実装します）
       // 基本的にイベントインターフェース名やメソッドはインターフェースからそのまま持ってくればよいです。
        [ClassInterface(ClassInterfaceType.None)]
        public sealed class OutOfProcessEvents_SinkHelper : _IOutOfProcessSampleEvents
        {
            public MainWindow win = null;   // GUI にイベントを表示するための独自実装（無くてもよい）

            public void ClickIn(int xCoord, int yCoord)
            {
                Debug.WriteLine("ClickIn {0}, {1}.", xCoord, yCoord);

                // GUI にイベントを表示するための独自実装（無くてもよい） ===>>>
                Application.Current.Dispatcher.BeginInvoke(
                      DispatcherPriority.Background,
                      new Action(() => {
                          win.ClickLabel.Content = String.Format("{0}, {1}", xCoord, yCoord);
                      }));
                // GUI にイベントを表示するための独自実装（無くてもよい） ===<<<<

            }

            public void StatusChange(LibStatus Status)
            {
                Debug.WriteLine("StatusChange : {0}", Status);

                // GUI にイベントを表示するための独自実装（無くてもよい） ===>>>
                Application.Current.Dispatcher.BeginInvoke(
                      DispatcherPriority.Background,
                      new Action(() => {
                          win.StatusLabel.Content = Status;
                      }));
                // GUI にイベントを表示するための独自実装（無くてもよい） ===<<<<
            }
        }
```

### イベント受信の切断

```csharp
        private void QuitButton_Click(object sender, RoutedEventArgs e)
        {
            if (null != m_connectionPoint)
            {
                // イベントシンクとの切断
                m_connectionPoint.Unadvise(m_sinkCookie);
                m_sink = null;

                // COMオブジェクトの開放
                Marshal.ReleaseComObject(m_connectionPoint);
                m_connectionPoint = null;
            }
            return;
        }
```

### イベント受信インターフェース記述の手順

Visual Studio で COM のオブジェクト参照を設定して、いったんビルドします。
すると相互運用性のために DLL が生成されるので DLL パスを確認します。（下図の赤枠参照）
※一連の作業後に COM のオブジェクト参照は削除してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30578/64c74e6d-3ac1-8e16-c2cd-e58dec4eec33.png)

[dotPeek](https://www.jetbrains.com/decompiler/) をインストールして、起動します。
※アセンブリをデコンパイルできるツールであれば、他のツールでも構いません。

[dotPeek](https://www.jetbrains.com/decompiler/) を起動したら、相互運用性のために生成された DLL のパスを読み込ませます。	
そして、イベントI/F （赤枠）を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30578/3e64bc85-4454-4e79-3575-33f7839c55e4.png)

右側に表示されているイベントインターフェースを namespace の中身だけ、C# のソース（.cs）に貼り付けます。
利用している列挙型などあれば、あわせて namespace の中身だけを C# にコピーします。

前述の記述例を参考にイベントインターフェース を継承するラッパークラスを定義します。


# 気づき

- イベントを除けば、ProgID さえわかってしまえば、レイトバインディングで実装することができます。
- 利用する COM は automation （IDispatch 継承）である必要があります。
- レイトバインディングとはいっても、イベントインターフェースを利用する場合は、イベントインターフェースの ID がソースに埋め込まれる形になる（この例では「_IOutOfProcessSampleEvents」の定義部分）ため、その制約が残ります。


# 参考リンク

- [Receiving Events from late-bound COM servers - CodeProject](https://www.codeproject.com/Articles/10262/Receiving-Events-from-late-bound-COM-servers)