Как я могу обернуть COM-объект в собственный класс .NET?

Я использую обширный существующий COM-API (может быть, Outlook, но это не так) в .NET(С#). Я сделал это, добавив "COM-ссылку" в Visual Studio, поэтому все "волшебство" выполняется за кулисами (т.е. Мне не нужно вручную запускать tlbimp).

В то время как COM API теперь можно "легко" использовать из .NET, он не очень дружелюбен к .NET. Например, нет никаких дженериков, странных событий, таких как IPicture и т.д. Итак, я бы хотел создать собственный .NET API, который был реализован используя существующий COM API.

Простым первым проходом может быть

namespace Company.Product {
   class ComObject {
       public readonly global::Product.ComObject Handle; // the "native" COM object
       public ComObject(global::Product.ComObject handle) {
          if (handle == null) throw new ArgumentNullException("handle");
          Handle = handle;
       }

       // EDIT: suggestions from nobugz
       public override int GetHashCode() {
          return Handle.GetHashCode();
       }
       public override bool Equals(object obj) {
          return Handle.Equals(obj);
       }
   }
}

Одной из ближайших проблем с этим подходом является то, что вы можете легко получить несколько экземпляров ComObject для одного и того же основного объекта "native COM". Например, при перечислении:

IEnumerable<Company.Product.Item> Items {
   get {
      foreach (global::Item item in Handle.Items)
         yield return new Company.Product.Item(item);
   }
}

Вероятно, это было бы неожиданно в большинстве ситуаций. Исправление этой проблемы может выглядеть как

namespace Company.Product {
   class ComObject {
       public readonly global::Product.ComObject Handle; // the "native" COM object
       static Dictionary<global::Product.ComObject, ComObject> m_handleMap = new Dictionary<global::Product.ComObject, ComObject>();
       private ComObject(global::Product.ComObject handle) {
          Handle = handle;
          handleMap[Handle] = this;
       }
       public ComObject Create(global::Product.ComObject handle) {
          if (handle == null) throw new ArgumentNullException("handle");

          ComObject retval;
          if (!handleMap.TryGetValue(handle, out retval))
              retval = new ComObject(handle);
          return retval;             
       }
   }
}

Это выглядит лучше. Перечислитель меняет вызов Company.Product.Item.Create(item). Но теперь проблема заключается в том, что Словарь < > сохранит оба объекта "живыми", чтобы они никогда не собирались собирать мусор; это, вероятно, плохо для COM-объекта. И теперь все начинает запутываться...

Похоже, что часть решения использует WeakReference в некотором роде. Есть также рекомендации об использовании IDisposable, но это не выглядит очень дружественным .NET, чтобы иметь дело с Dispose() на каждом один объект. И затем там различные обсуждения, когда/если ReleaseComObject должен быть вызван. Существует также code над http://codeproject.com, который использует последнее связывание, но я доволен API-интерфейсом, зависящим от версии.

Итак, на данный момент я не совсем уверен, что это лучший способ для продолжения. Я бы хотел, чтобы мой собственный .NET API был как можно более ".NET" (возможно, даже встраивал сборку Interop с .NET 4.0) и не имел необходимости использовать эвристику, как правило "двух точек".

Одна вещь, которую я думал о попытке создания проекта ATL, скомпилировать с флагом /clr и использовать поддержку COM компилятора С++ (Product:: ComObjectPtr, созданная с помощью #import), а не .NET. RCWS. Конечно, я обычно скорее код в С#, чем С++/CLI...

Ответы

Ответ 1

Самая большая проблема, с которой я столкнулся с приведением COM-объектов в .NET, заключается в том, что сборщик мусора работает в другом потоке, и окончательная версия COM-объекта будет часто (всегда?) вызываться из этого потока.

Microsoft намеренно нарушила правила модели потоковой передачи COM, в которых утверждается, что с объектами, связанными с квартирами, все методы должны вызываться из одного потока.

Для некоторых библиотек COM это не большое дело, но для других это огромная проблема - особенно для библиотек, которые должны выделять ресурсы в своих деструкторах.

Что-то, о чем нужно знать...

Ответ 2

Вы сами не имеете дело с COM-объектами. Вы уже имеете дело с фасадом, который был создан в тот момент, когда вы добавили ссылку на двоичный код COM в свой проект. (.NET) создаст для вас фасад, что упростит задачу использования COM-объектов для простого использования обычных классов .NET. Если вам не нравится интерфейс, созданный для вас, вы должны, вероятно, создать фасад для существующего фасада. Вам не нужно беспокоиться о сложных тонкостях COM, потому что это уже сделано для вас (возможно, некоторые вещи вам нужно беспокоиться, но я думаю, что их немного и далеко). Просто используйте класс как обычный .net-класс, потому что это именно то, что он есть, и справляйтесь с любыми проблемами по мере их возникновения.

EDIT: Одна из проблем, которые могут возникнуть, - это недетерминированное уничтожение COM-объектов. Сопоставление ссылок, которое происходит за кулисами, зависит от сбора мусора, поэтому вы не можете быть уверены, когда ваши объекты будут уничтожены. В зависимости от вашего приложения вам может потребоваться более детерминированное уничтожение ваших COM-объектов. Для этого вы должны использовать Marshal.ReleaseComObject. Если это, тогда вы должны знать this getcha.

Извините, я бы разместил больше ссылок, но, видимо, я не могу опубликовать более 1, не получив 10 репутации.

Ответ 3

Вы делаете это излишне сложно. Это не "ручка", нет необходимости в подсчете ссылок. A __ComObject является обычным классом .NET и подчиняется нормальным правилам сбора мусора.

Ответ 4

Похоже, вы хотите создать более простой .NET API для вашего сложного COM-интерфейса. Как сказал nobugz, ваши классы ComObject являются реальными, родными объектами .NET, которые внутренне содержат неуправляемые ссылки на ваши фактические объекты com. Вам не нужно делать что-то смешное, чтобы управлять ими... просто используйте их, как обычные объекты .NET.

Теперь, в отношении представления "более красивого лица" для ваших пользователей .NET. Для этого существует существующий шаблон проектирования, и его называют Facade. Я собираюсь предположить, что вам действительно нужна часть функциональности, предоставляемой этими COM-объектами. Если это так, создайте слой фасада вокруг ваших объектов inter inter. Этот уровень должен содержать необходимые классы, методы и типы поддержки, которые обеспечивают необходимую функциональность для всех клиентов .NET с более дружественным API, чем сами COM-объекты. Фасад будет также отвечать за упрощение странностей, таких как преобразование любых COM-объектов для событий с обычными событиями .NET, маршалинг данных, упрощение создания COM-объектов, настройка и срыв и т.д.

Хотя в целом абстракции следует избегать, поскольку они, как правило, добавляют работу и сложность. Однако иногда они необходимы, а в некоторых случаях могут значительно упростить ситуацию. Если более простой API может повысить производительность других членов команды, которые должны потреблять некоторые функции, предоставляемые очень сложной системой com-объектов, то абстракция обеспечивает ощутимую ценность.