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

Я использую обширный COM API (может быть Outlook, но это не так) в .NET (C #). Я сделал это, добавив «COM Reference» в 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 для одного и того же базового "родного" 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). Но теперь проблема в том, что Dictionary <> будет поддерживать оба объекта «живыми», поэтому они никогда не будут собирать мусор; это, вероятно, плохо для объекта COM. И сейчас все становится грязно ...

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

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

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

Ответы [ 4 ]

11 голосов
/ 14 февраля 2010

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

РЕДАКТИРОВАТЬ: Одной из проблем, с которыми вы можете столкнуться, является недетерминированное уничтожение COM-объектов. Подсчет ссылок, который происходит за кулисами, основан на сборке мусора, поэтому вы не можете быть уверены, когда ваши объекты будут уничтожены. В зависимости от вашего приложения вам может потребоваться более детерминированное уничтожение ваших COM-объектов. Для этого вы должны использовать Marshal.ReleaseComObject . Если это так, то вы должны знать, это Гоча.

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

6 голосов
/ 19 февраля 2010

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

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

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

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

2 голосов
/ 13 февраля 2010

Вы делаете это излишне трудным.Это не «дескриптор», нет необходимости в подсчете ссылок.__ComObject - это обычный класс .NET, на который распространяются обычные правила сборки мусора.

0 голосов
/ 14 февраля 2010

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

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

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

...