Hungry Mind , Blog about everything in IT - C#, Java, C++, .NET, Windows, WinAPI, ...

Using .NET Controls from WinAPI/MFC/WTL applications (Part 1)

До появления .NET графические приложения разрабатывались с использованием узкого круга инструментов и библиотек. Самыми популярными были Borland Delphi/C++ Builder (VCL), Microsoft Visual Studio (WinAPI, MFC, WTL) и QT. Инфраструктура Borland позволяла сторонним разработчикам создавать библиотеки графических елементов управления, которые хорошо интегрировались в IDE и позволяли быстро "рисовать" красивые, удобные интерфейсы. Microsoft Visual Studio на тот момент была по всем параметрам хуже - слабенький компилятор, не поддерживающий актуальные стандарты, отсутствие полноценного дизайнера форм, плохо спроектированная обьектная модель базовых библиотек (а их расширения можно перечислить на пальцах одной страусиной ноги). Выход Visual Studio 2002/2003 (официальное рождение .NET) сильно повлиял на чашу весов - все больше проектов пишут под .NET. Borland решил не отставать и включил поддержку управляемого кода в свою IDE, правда это не поменяет текущее аутсайдерское положение.

Единственный механизм, который позволяет подружить функционал различных платформ (речь о платформах разработки, а не ОС), - COM/ActiveX. Ни у кого не вызывает сомнений факт возможности интеграции компонента VCL-ActiveX в приложение MFC или наоборот. Оказывается, .NET тоже позволяет пользоваться этим механизмом на полную. Я рассмотрю лишь случай использования .NET-обьектов из неуправляемого кода (C++/CLI является управляемым!), так как обратный вариант взаимодействия прост и не нуждается в разжевывании. Существует 2 способа достижения поставленной цели:

  1. Классический - использование .NET через COM/ActiveX.
  2. Анальный - непосредственный хостинг CLR и использование COM-подобных механизмов взаимодействия.

Каждый рассмотрим детально. Для понимания того, что написано далее, необходимы твердые знания технологии COM! Мы напишем элемент управления .NET WinForms (ComplexControl), который попытаемся использовать из неуправляемого приложения следующим образом:

  1. Создать екземпляра обьекта
  2. Добавить элемент управления в форму или диалог
  3. Вызывать методы обьекта, читать и записывать его свойства
  4. Подписываться на уведомления и получать их через callback-интерфейс

Диаграмма классов следующая:

Часть первая. COM-Interop в .NET.


Любой тип MySuperDotNetType (класс MySuperDotNetClass, структура MySuperDotNetStruct, перечисление MySuperDotNetEnum, инртерфейс IMySuperDotNetInterface или делегат MySuperDotNetDelegate) при определенных условиях может быть доступен посредством COM. Эти условия также влияют на видимость полей структур, методов и свойств классов. Минимальный набор таков:
  1. Область видимости элемента - public.
  2. Наличие атрибута [ComVisible(true)] в обьявлении самого элемента либо в его родительской области (сборка в целом или тип, в котором находится обьявление).
    Также его можно использовать для сокрытия ([ComVisible(false)]) полей структуры, методов и свойств класса, а также внутренних типов.
Далее любой элемент CLR, удовлетворяющий этим требованиям, будем называть ComVisible.

Дополнительно можно использовать следующие атрибуты для более тонкой настройки взаимодействия:
  • [Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")] - CLSID CO-класса, IID интерфейса, ID библиотеки типов (в зависимости от контекста атрибута - класс, интерфейс, сборка)
  • [ProgId("MyHumanReadableTypeName")] позволяет задавать ProgID для типа MySuperDotNetType
  • [ClassInterface(ClassInterfaceType.XXX)] определяет вид экспортируемого интерфейса класса (далее - класс-интерфейс) MySuperDotNetClass. Перечисление ClassInterfaceType содержит 3 значения:
    1. AutoDispatch
      Означает, что класс будет явно поддерживать исключительно позднее связывание через свой dispinterface. Библиотека типов, которую создает утилита Tlbexp.exe (о ней - чуть позже), не содержит информацию о "начинке" этого интерфейса - свойствах и методах. Это сделано для предотвращения кеширования (когда они подставляются непосредственно в вызовы метода Invoke компилятором) клиентами значений DISPID. Является значением по умолчанию.
      Вот, как в этом случае выглядит IDL для CO-класса ComplexControl:
      [
        uuid(2D7FDCB4-6C3F-4529-A93D-10DFC72927B1),
        version(1.0),
        custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, NetControlsLibrary.ComplexControl)
      ]
      coclass ComplexControl {
          [default] interface _ComplexControl;
          interface _Object;
          interface IComponent;
          interface IDisposable;
          interface IWin32Window;
          interface IComplexView;
          [default, source] dispinterface IComplexView_Events;
      };
      
      
      Обратите внимание, что CO-класс ComplexControl явно поддерживает итерфейс _Object, который на самом деле - дуальный dispinterface:
      [
        uuid(65074F7F-63C0-304E-AF0A-D51741CB4A8D),
        hidden,
        dual,
        nonextensible,
        custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, System.Object)
      
      ]
      dispinterface _Object {
          properties:
          methods:
              [id(00000000), propget, custom(54FC8F55-38DE-4703-9C4E-250351302B1C, 1)]
              BSTR ToString();
              [id(0x60020001)]
              VARIANT_BOOL Equals([in] VARIANT obj);
              [id(0x60020002)]
              long GetHashCode();
              [id(0x60020003)]
              _Type* GetType();
      };
      
      
      Обратите внимание, что интерфейс _ComplexControl - это dispinterface, а в CO-классе обьявлен, как обычный:
      [
        uuid(6148B23F-BFFE-344F-809A-FF0915CC4C5B),
        hidden,
        dual,
        custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, NetControlsLibrary.ComplexControl)
      
      ]
      dispinterface _ComplexControl {
          properties:
          methods:
      };
      
      
      Важно:
      • Такой диспинтерфейс позволяет использовать исключительно ComVisible методы и свойства класса такие, что C#-выражение myClassInstance.SomeMemberIWantToUseFromIDispatch не вызывает ошибку компиляции. К примеру, методы интерфейсов с явной реализацией сюда не попадают. Другими словами, он еквивалентен общедоступному интерфейсу (здесь речь идет концепции, а не о ключевом слове interface) класса.
      • Возможности явно задать IID для него нет, поэтому во время разработки можно смело ожидать сюрпризов в виде E_NOINTERFACE.
    2. AutoDual
      Означает, что интерфейс класса будет дуальным дисп-интерфейсом (dual dispinterface). Анологично предыдущему варианту + возможность использовать интерфейс класса через стандартный vtbl-механизм + в библиотеку типов попадает информация об экспортируемых свойствах и методах (попадающих под вышеизложенное правило). Использовать этот механизм не рекомендуют из-за проблем с версионностью (изменение таблицы виртуальных функций после добавления или удаления методов класса).
      IDL для CO-класса ComplexControl анологичен, а вот интерфейс _ComplexControl выглядит монстрообразно:
      [
        uuid(6148B23F-BFFE-344F-809A-FF0915CC4C5B),
        hidden,
        dual,
        custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, NetControlsLibrary.ComplexControl)
      
      ]
      dispinterface _ComplexControl {
          properties:
          methods:
              [id(0x60020000), propget,
                custom(54FC8F55-38DE-4703-9C4E-250351302B1C, 1)]
              BSTR ToString();
              [id(0x60020001)]
              VARIANT_BOOL Equals([in] VARIANT obj);
              [id(0x60020002)]
              long GetHashCode();
              [id(0x60020003)]
              _Type* GetType();
              [id(0x60020004)]
              VARIANT GetLifetimeService();
              [id(0x60020005)]
              VARIANT InitializeLifetimeService();
              [id(0x60020006)]
              _ObjRef* CreateObjRef([in] _Type* requestedType);
              [id(0x60020007), propget]
              ISite* Site();
              [id(0x60020007), propputref]
              void Site([in] ISite* rhs);
              [id(0x60020009)]
              void add_Disposed([in] _EventHandler* value);
              // Last 1000000 lines omited for brevity... :)
      };
      
    3. None
      Означает, что интерфейс для класса не генерируется. Рекомендуемое значение.
  • [InterfaceType(ComInterfaceType.XXX)] - по аналогии с предыдущим, только касательно .NET-интерфейсов.
  • [ComDefaultInterface(typeof(IMySuperDotNetInterface))] указывает на интерфейс по-умолчанию CO-класса. В IDL - default.
  • [ComSourceInterfaces(typeof(IMySuperDotNetInterface_Events))] присоединяет Sink-интерфейс (добавляет в класс Connection Point). В IDL - source. О связи этих интерфейсов с классом - далее.
  • [DispId(1234)] - явно заданный DISPID для элемента дисп-интерфейса (свойства или метода).
  • Прочие атрибуты из пространства имен System.Runtime.InteropServices.

С помощью интроспекции CLR способен спроектировать обьектную модель на COM и создать на лету объект (так называемый CCW - COM Callable Wrapper), совместимый с конвенциями вызова __stdcall и по структуре памяти совпадающий с vtbl-интерфейсами. CCW выглядит, как обычный COM-объект, но транслирует все вызовы в упраыляемую среду. А утилита Tlbexp.exe (Type Library Exporter) способна создать для сборки библиотеку типов (Type Library) в виде tlb-файла, который можно просмотреть с помощью программы OleView (идет в комлекте со студией по адресу C:\Program Files\Microsoft Visual Studio 8\Common7\Tools\Bin\OleView.Exe).
Алгоритм отображения сборки на IDL сложен и часто просто непонятен. Более полную модель можно увидеть с помощью OleView, заглянув в категорию ".NET Category" (насколько я понимаю, эту информацию .NET-обьекты отдают непосредственно через IDispatch::GetTypeInfo). Вот несколько интересных и важных моментов, которые стоит знать:

  • Информация tlb-файла является порядочно урезанной версией реальной обьектной модели, которую строит CLR.
  • Каждый .NET COM-обьект поддерживает интерфейсы IUnknown, IDispatch, _Object, IConnectionPointContainer, IProvideClassInfo, ISupportErrorInfo, IManagedObject.
  • ComplexControl дополнительно поддерживает интерфейсы _Component, _ContainerControl, _Control, _MarshalByRefObject, _ScrollableControl, _UserControl, IComponent, IOleControl, IOleInPlaceActiveObject, IOleInPlaceObject, IOleObject, IOleWindow, IPersist, IPersistPropertyBag, IPersistStorage, IPersistStreamInit, IQuickActivate, IViewObject, IViewObject, IWin32Window. Судя по этому списку можно сделать следующие выводы.
  • Каждый .NET COM-обьект поддерживает все ComVisible интерфейсы своих предков.
  • Каждый .NET COM-обьект поддерживает все ComVisible класс-интерфейсы своих предков (с учетом атрибута ClassInterfaceType, конечно-же).
  • Каждый .NET WinForms Control COM-обьект поддерживает множество интерфейсов (если не все) OLE/ActiveX. Соответственно, может быть использован, как OLE/ActiveX.
Для того, чтобы использовать библиотеки посредством классического COM, их нужно зарегистрировать.
  1. Статической регистрацией занимается утилита RegAsm.exe (C:\Windows\Microsoft.NET\Framework\v2.0.50727\RegAsm.exe). Она выполняет инструментирование сборки (аналогичным Tlbexp.exe образом) и вносит записи в системный реестр. Запуск RegAsm.exe можно возложить на Visual Studio - в настройках проекта в разделе Build поставить галочку [Register for COM interop]. Запись в реестре для CO-класса ComplexControl выглядит следующим образом:
    [HKEY_CLASSES_ROOT\CLSID\{2D7FDCB4-6C3F-4529-A93D-10DFC72927B1}]
    @="NetControlsLibrary.ComplexControl"
    [HKEY_CLASSES_ROOT\CLSID\{2D7FDCB4-6C3F-4529-A93D-10DFC72927B1}\Implemented Categories]
    [HKEY_CLASSES_ROOT\CLSID\{2D7FDCB4-6C3F-4529-A93D-10DFC72927B1}\Implemented Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}]
    [HKEY_CLASSES_ROOT\CLSID\{2D7FDCB4-6C3F-4529-A93D-10DFC72927B1}\InprocServer32]
    @="mscoree.dll"
    "Assembly"="NetControlsLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
    "Class"="NetControlsLibrary.ComplexControl"
    "CodeBase"="file:///D:/Development/Projects/NETComInterop/Debug/NetControlsLibrary.dll"
    "RuntimeVersion"="v2.0.50727"
    "ThreadingModel"="Both"
    [HKEY_CLASSES_ROOT\CLSID\{2D7FDCB4-6C3F-4529-A93D-10DFC72927B1}\InprocServer32\1.0.0.0]
    "Assembly"="NetControlsLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
    "Class"="NetControlsLibrary.ComplexControl"
    "CodeBase"="file:///D:/Development/Projects/NETComInterop/Debug/NetControlsLibrary.dll"
    "RuntimeVersion"="v2.0.50727"
    [HKEY_CLASSES_ROOT\CLSID\{2D7FDCB4-6C3F-4529-A93D-10DFC72927B1}\ProgId]
    @="NetControlsLibrary.ComplexControl"

    Здесь {2D7FDCB4-6C3F-4529-A93D-10DFC72927B1} - CLSID моего класса ComplexControl, {62C8FE65-4EBB-45e7-B440-6E39B2CDBF29} в ключе "Implemented Categories" - GUID категории компонента (".NET Category" в нашем случае), 1.0.0.0 в ключе "InprocServer32" - версия класса (поддержка нескольких версий компонента). Стоит отметить, что InprocServer32 указывает на mscoree.dll! Именно там находится точка входа в фабрику классов - функция DllGetClassObject. Ну, а остальные записи ниже InprocServer32 служат для нахождения нужной сборки с типом (читает их, конечно-же, не подсистема COM, а CLR).
  2. Динамически зарегистрировать поможет класс TypeLibConverter.
  3. Утилита regsvcs.exe тоже годится, но это другой конек, связанный с COM+.
Для добавдения в CO-класс точки подключения (эта часть была самая сложная в инвестигации!) необходимо выполнить следующие действия:
  1. Создать интерфейс IMySuperDotNetClass_Events
  2. Настроить COM-аспекты - [ComVisible(true)], [Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")] и [InterfaceType(ComInterfaceType.XXX)] (рекомендуется использовать InterfaceIsIDispatch, обьяснение дальше).
  3. Наполнить интерфейс обьявлениями методов, совместимыми с типами-делегатами поддерживаемых событий.
  4. Добавить в класс MySuperDotNetClass определения событий с именами, которые соответствуют именам функций интерфейса IMySuperDotNetClass_Events.
Лучше это показать на примере:
Интерфейс (открытый) и делегат (область видимости не имеет значения).
internal delegate void PropertyChangedEventHandler(Object src, String propertyName);

[ComVisible(true)]
[Guid("FD9AEC7A-3688-4394-B4D0-636E4A7FE3B9")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] // if InterfaceIsDual then SinkObj must also be dual!
public interface IComplexView_Events
{
   [DispId(0x01)]
   void PropertyChanged(Object src, String propertyName);
}
Тело класса ComplexControl.
#region IComplexView_Events Mappings

private PropertyChangedEventHandler _propertyChanged;
internal event PropertyChangedEventHandler PropertyChanged {
   add {
      _propertyChanged += value;
      Trace.WriteLine("PropertyChanged.add(...)");
   }
   remove {
      _propertyChanged -= value;
      Trace.WriteLine("PropertyChanged.remove(...)");
   }
}

#endregion

3 коммент.:

Анонимный комментирует...

В статью закралась ошибочка:
для класса ComplexControl нужно еще задать атрибут [ComSourceInterfaces(typeof(IComplexView_Events))], чтобы заработали connection points.

В целом, очень полезная статья!

Анонимный комментирует...

Hey there would you mind letting me know which hosting company you're using? I've loaded
your blog in 3 different browsers and I must say this blog loads a lot faster then most.
Can you suggest a good web hosting provider at a reasonable
price? Thank you, I appreciate it!

My blog verizon iphone 5 features

Анонимный комментирует...

The other day, while I was at work, my cousin stole my apple ipad and tested to see if it can
survive a thirty foot drop, just so she can be a youtube sensation.
My iPad is now broken and she has 83 views. I know this is entirely off topic but I had to share
it with someone!

Feel free to surf to my website: wasserdichter mp3 player test

Отправить комментарий

Copyright 2007-2011 Chabster