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

Показаны сообщения с ярлыком ActiveX. Показать все сообщения
Показаны сообщения с ярлыком ActiveX. Показать все сообщения

MFC fails again

MFC, I love you. Really. Your source code is a fantastic example of how not to do it. Don't do it ever.

The problem

OK, so here's the problem - given an MFC dialog (or dialog control), sometimes we have no focus cues when tabbing through UI elements. You press Tab, use cursor keys and yet focus rectangle is not there. Forever. What might the problem be? Let's find out how this works internally.

Focus rect internals

First of all open any system dialog (for example taskbar Properties) using mouse only and ensure there is no focus rectangle on currently focused UI element. Press Tab and watch for it to immediately appear. Whatever you do from now on (excluding cases where the whole window hierarchy is hidden or recreated), focus rect is always on the screen. Now try to open this dialog using keyboard operation (you can open taskbar context menu and select Properties menu item using keyboard keys). Focus rect is shown immediately. What's happening here?

The answer begins it's path in dlgbegin.c, function InternalCreateDialog:

HWND InternalCreateDialog(
    HANDLE hmod,
    LPDLGTEMPLATE lpdt,
    DWORD cb,
    HWND hwndOwner,
    DLGPROC lpfnDialog,
    LPARAM lParam,
    UINT fSCDLGFlags)
{
...
}

The most interesting part is the following:

 /*
  * UISTATE: if keyboard indicators are on and this is a topmost dialog
  * set the internal bit.
  */
 if (TEST_KbdCuesPUSIF) {
     /*
      * If property page, UISTATE bits were copied from parent when I was created
      * Top level dialogs act as containers and initialize their state based on
      * the type of the last input event, after sending UIS_INITIALIZE
      */
     if (!TestwndChild(pwnd)) {
         SendMessageWorker(pwnd, WM_CHANGEUISTATE, MAKEWPARAM(UIS_INITIALIZE, 0), 0, FALSE);
     }
 }

TEST_KbdCuesPUSIF tests current execution environment for keyboard cues enablement (field check in some global structure), TestwndChild ensures top level dialog. Then the window gets WM_CHANGEUISTATE message with two arguments - UIS_INITIALIZE and zero. Documentation says Top level dialogs act as containers and initialize their state based on the type of the last input event. What does this mean? The answer is in the default window procedure (dwp.c):

/***************************************************************************\
* xxxDefWindowProc (API)
*
* History:
* 10-23-90 MikeHar Ported from WaWaWaWindows.
* 12-07-90 IanJa   CTLCOLOR handling round right way
\***************************************************************************/

LRESULT xxxDefWindowProc(
    PWND pwnd,
    UINT message,
    WPARAM wParam,
    LPARAM lParam)
{
...
}

Take a look at WM_CHANGEUISTATE handler:

case WM_CHANGEUISTATE:
    {
        WORD wAction = LOWORD(wParam);
        WORD wFlags = HIWORD(wParam);
        BOOL bRealChange = FALSE;

        if (wFlags & ~UISF_VALID || wAction > UIS_LASTVALID ||
            lParam || !TEST_KbdCuesPUSIF) {
                return 0;
        }

        if (wAction == UIS_INITIALIZE) {
            if (gpsi->bLastRITWasKeyboard) {
                wAction = UIS_CLEAR;
            } else {
                wAction = UIS_SET;
            }
            wFlags = UISF_HIDEFOCUS | UISF_HIDEACCEL;
            wParam = MAKEWPARAM(wAction, wFlags);
        }

        UserAssert(wAction == UIS_SET || wAction == UIS_CLEAR);
        /*
         * If the state is not going to change, there's nothing to do here
         */
        if (wFlags & UISF_HIDEFOCUS) {
            bRealChange = (!!TestWF(pwnd, WEFPUIFOCUSHIDDEN)) ^ (wAction == UIS_SET);
        }
        if (wFlags & UISF_HIDEACCEL) {
            bRealChange |= (!!TestWF(pwnd, WEFPUIACCELHIDDEN)) ^ (wAction == UIS_SET);
        }

        if (!bRealChange) {
            break;
        }

        /*
         * Children pass this message up
         * Top level windows update their children's state and
         * send down to their imediate children WM_UPDATEUISTATE.
         */
        if (TestwndChild(pwnd)) {
            ThreadLockAlways(pwnd->spwndParent, &tlpwndParent);
            lt = xxxSendMessage(pwnd->spwndParent, WM_CHANGEUISTATE, wParam, lParam);
            ThreadUnlock(&tlpwndParent);
            return lt;
        } else {
            return xxxSendMessage(pwnd, WM_UPDATEUISTATE, wParam, lParam);
        }

        }
    break;

case WM_QUERYUISTATE:
    return (TestWF(pwnd, WEFPUIFOCUSHIDDEN) ? UISF_HIDEFOCUS : 0) |
           (TestWF(pwnd, WEFPUIACCELHIDDEN) ? UISF_HIDEACCEL : 0);
    break;

case WM_UPDATEUISTATE:
    {
        WORD wAction = LOWORD(wParam);
        WORD wFlags = HIWORD(wParam);

        if (wFlags & ~UISF_VALID || wAction > UIS_LASTVALID ||
            lParam || !TEST_KbdCuesPUSIF) {
                return 0;
        }

        switch (wAction) {
            case UIS_INITIALIZE:
                /*
                 * UISTATE: UIS_INITIALIZE sets the UIState bits
                 * based on the last input type
                 */
                if (!gpsi->bLastRITWasKeyboard) {
                    SetWF(pwnd, WEFPUIFOCUSHIDDEN);
                    SetWF(pwnd, WEFPUIACCELHIDDEN);
                    wParam = MAKEWPARAM(UIS_SET, UISF_HIDEACCEL | UISF_HIDEFOCUS);
                } else {
                    ClrWF(pwnd, WEFPUIFOCUSHIDDEN);
                    ClrWF(pwnd, WEFPUIACCELHIDDEN);
                    wParam = MAKEWPARAM(UIS_CLEAR, UISF_HIDEACCEL | UISF_HIDEFOCUS);
                }
                break;

            case UIS_SET:
                if (wFlags & UISF_HIDEACCEL) {
                    SetWF(pwnd, WEFPUIACCELHIDDEN);
                }
                if (wFlags & UISF_HIDEFOCUS) {
                    SetWF(pwnd, WEFPUIFOCUSHIDDEN);
                }
                break;

            case UIS_CLEAR:
                if (wFlags & UISF_HIDEACCEL) {
                    ClrWF(pwnd, WEFPUIACCELHIDDEN);
                }
                if (wFlags & UISF_HIDEFOCUS) {
                    ClrWF(pwnd, WEFPUIFOCUSHIDDEN);
                }
                break;

            default:
                break;
         }

        /*
         * Send it down to its immediate children if any
         */
         if (pwnd->spwndChild) {

            PBWL pbwl;
            HWND *phwnd;
            TL tlpwnd;

            pbwl = BuildHwndList(pwnd->spwndChild, BWL_ENUMLIST, NULL);
            if (pbwl == NULL)
                return 0;

            for (phwnd = pbwl->rghwnd; *phwnd != (HWND)1; phwnd++) {
                /*
                 * Make sure this hwnd is still around.
                 */
                if ((pwnd = RevalidateHwnd(*phwnd)) == NULL)
                    continue;

                ThreadLockAlways(pwnd, &tlpwnd);
                xxxSendMessage(pwnd, message, wParam, lParam);
                ThreadUnlock(&tlpwnd);
            }
            FreeHwndList(pbwl);
        }
    }
    break;

We are interested in the following code:

if (wAction == UIS_INITIALIZE) {
    if (gpsi->bLastRITWasKeyboard) {
        wAction = UIS_CLEAR;
    } else {
        wAction = UIS_SET;
    }
    wFlags = UISF_HIDEFOCUS | UISF_HIDEACCEL;
    wParam = MAKEWPARAM(wAction, wFlags);
}

It checks whether last input event was the one from keyboard (bLastRITWasKeyboard, RIT - raw input thread) and prepares appropriate action - set UISF_HIDEFOCUS and UISF_HIDEACCEL flags or clear them. The system tries to understand user experience type (mouse or keyboard) and initializes window state for better interaction. That's why you don't see focus or accelerator cues having opened a dialog using mouse whereas they are both on if you used your keyboard.

There is a little trick to have the system on: move the mouse right after you triggered a dialog using keyboard, or press any key after you triggered a dialog using mouse. The effect speaks for itself.

The rest of the handler simply decides where to send WM_UPDATEUISTATE message. WM_UPDATEUISTATE's handler just saves the state to internal OS structure for particular window. As you have already guessed, WM_QUERYUISTATE reads the state back.

What does this all mean? There is a window state for focus and accelerator cues and a set of messages for it's manipulation. This state is kept in sync by all windows of the same hierarchy. OS initializes the state for best user experience.

Who uses the state

At least common controls, of course. The pattern is simple (listview.c for example):

case WM_UPDATEUISTATE:
{
    DWORD dwUIStateMask = MAKEWPARAM(0xFFFF, UISF_HIDEFOCUS);

    // we care only about focus not accel, and redraw only if changed
    if (CCOnUIState(&(plv->ci), WM_UPDATEUISTATE, wParam & dwUIStateMask, lParam))
    {
        if(plv->iFocus >= 0)
        {
            // an item has the focus, invalidate it
            ListView_InvalidateItem(plv, plv->iFocus, FALSE, RDW_INVALIDATE | RDW_ERASE);
        }
    }

    goto DoDefault;
}

It just listens to UI state updates and does appropriate actions (saves the state to local structure, invalidates the window etc.).

Who updates the state

As I've already mentioned, focus rect is shown immediately as you pressed focus navigation key (Tab for example). This is done by ::IsDialogMessage OS function:

case WM_SYSKEYDOWN:
    /*
     * If Alt is down, deal with keyboard cues
     */
    if ((HIWORD(lpMsg->lParam) & SYS_ALTERNATE) && TEST_KbdCuesPUSIF) {
        if (TestWF(pwnd, WEFPUIFOCUSHIDDEN) || (TestWF(pwnd, WEFPUIACCELHIDDEN))) {
                SendMessageWorker(pwndDlg, WM_CHANGEUISTATE,
                                  MAKEWPARAM(UIS_CLEAR, UISF_HIDEACCEL | UISF_HIDEFOCUS), 0, FALSE);
            }
    }
    break;
case WM_KEYDOWN:
    code = (UINT)SendMessage(lpMsg->hwnd, WM_GETDLGCODE, lpMsg->wParam,
            (LPARAM)lpMsg);
    if (code & (DLGC_WANTALLKEYS | DLGC_WANTMESSAGE))
        break;

    switch (lpMsg->wParam) {
    case VK_TAB:
        if (code & DLGC_WANTTAB)
            break;
        pwnd2 = _GetNextDlgTabItem(pwndDlg, pwnd,
                (GetKeyState(VK_SHIFT) & 0x8000));

        if (TEST_KbdCuesPUSIF) {
            if (TestWF(pwnd, WEFPUIFOCUSHIDDEN)) {
                SendMessageWorker(pwndDlg, WM_CHANGEUISTATE,
                                      MAKEWPARAM(UIS_CLEAR, UISF_HIDEFOCUS), 0, FALSE);
            }
        }

WM_SYSKEYDOWN and WM_KEYDOWN analyze keyboard input and trigger UI state updates by sending WM_CHANGEUISTATE. All child windows then receive state change update, focused window shows up a shiny focus rectangle, labels begin to draw accelerator cues. Simple, huh?

So what the fuck is wrong might be in MFC

As you already know (I hope) MFC focus navigation is implemented via CWnd::PreTranslateMessage virtual function which eventually calls CWnd::IsDialogMessage method:

BOOL CWnd::IsDialogMessage(LPMSG lpMsg)
{
 ASSERT(::IsWindow(m_hWnd));

 if (m_nFlags & WF_OLECTLCONTAINER)
  return afxOccManager->IsDialogMessage(this, lpMsg);
 else
  return ::IsDialogMessage(m_hWnd, lpMsg);
}

In case of ActiveX controls in your dialog template (or any other shit involving OCC state initialization), WF_OLECTLCONTAINER flag is set and the control passes to COccManager::IsDialogMessage instead of API ::IsDialogMessage. Believe me or not, COccManager::IsDialogMessage analyzes keyboard input and moves the focus. The biggest problem with this method - it doesn't consider UI layout the way ::IsDialogMessage does. ZOrder enumeration is done using CWnd::GetNextDlgTabItem (see correct overload with COleControlSiteOrWnd * parameter), which moves through controls registered in COleControlContainer - m_pCtrlCont->m_listSitesOrWnds. As you might have already guessed, this container is populated by MFC during dialog creation (from template of course) and is not updated afterwards. So if you add your controls dynamically (WTL/MFC/whatever) - be ready for focus navigation problems. The solution is to update container with necessary items for each control you create/recreate etc. Plus you have to keep items order to reflect proper ZOrder. What a crap!

Finally, the answer

COccManager::IsDialogMessage doesn't fucking send WM_CHANGEUISTATE as ::IsDialogMessage does. It doesn't fucking do it. Fucking MFC, fuck you. FUCK YOU.

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
Copyright 2007-2011 Chabster