
How to create a COM component in Visual C# and use it in native MFC applications.
Richiamare da un'applicazione nativa (MFC) delle windows forms C#, interne ad un assembly .Net, che espone i suoi metodi a COM.

Copyright (C) Giuseppe Pischedda 2012
Introduzione
Quando è necessario creare codice riusabile, in ambiente windows, è naturale usare la tecnologia COM (Component Object Model).
COM consente a diverse applicazioni di comunicare tra loro esponendo funzioni e classi (detti appunto oggetti COM). Questi oggetti possono essere creati in vari linguaggi, naturalmente quelli object oriented , in particolare il C++, sono i più indicati per l’implementazione di oggetti COM.
Il .Net framework mette a disposizione nel namespace System.Runtime.InteropServices una grande quantità di metodi e attributi per interagire con le librerie COM. In particolare l’attributo DllImportAttribute consente di definire i metodi per le chiamate alle API native di windows e più in generale al codice non gestito in maniera trasparente allo sviluppatore e, l’attributo MarshalAsAttribute che consente di gestire in memoria i dati managed e unmanaged (in pratica esegue il cast dei tipi in maniera sicura).
Ma se si volessero esportare le proprie classi o i propri metodi in codice gestito (es. C#) o direttamente tutta una windows form per essere poi usati da applicazioni native come quelle scritte in MFC (Microsoft Foundation Classes) ?
Ebbene si. Si può fare.
Ci sono due metodi fondamentalmente:
- Creare codice mixed, nativo e managed in Visual C++
- Utilizzare COM in .Net
- - Il primo metodo è quello che viene usato più frequentemente, che consiste nello sviluppare classi e metodi in C++/CLI e quindi in un progetto MFC importare le relative classi e funzioni; abilitare quindi lo switch /clr che dice al compilatore Visual C++ di produrre codice gestito. Il risultato sarà obbligatoriamente un assembly mixed (native e managed).
- - Il secondo metodo (quello che esegurò in questo post) consiste nell’esporre le classi ed i metodi a COM da librerie .Net. Questo consente alle applicazioni scritte in qualunque linguaggio nativo o managed di ” consumare” i metodi esposti dall’assembly via COM.
Gli assembly .Net che vogliamo siano disponibili alle altre applicazioni nel sistema operativo devono essere registrati nella GAC (Global Assembly Cache). Per poter essere registrati nella GAC questi devono utilizzare un nome sicuro (strong name). (Questo ovviamente è necessario per avere assembly univoci nel sistema) Vale a dire che devono essere firmati con una coppia di chiavi pubblica e privata. Anche per questo aspetto il .Net Framework mette a disposizione lo strumento a riga di comando sn.exe.
Fortunatamente Visual Studio integra tutto all’interno dell’IDE , anche gli strumenti a riga di comando di .Net Framework, semplificando di molto tutti i passaggi.
Ora che abbiamo gli ingredienti sarà bene riepilogare sia il nostro obiettivo, sia i passi dettagliati per conseguirlo.
- Creazione di una libreria di classi C# firmata (strong-name) che esporti i suoi metodi a COM Interop
- Aggiunta di due windows form alla libreria di classi (e i relativi metodi per richiamare le winforms). Drag&drop dalla toolbox di un ActiveX in una delle due winforms e drag&drop nell'altra winform di un componente .Net.
- Registrazione dell'assembly nella GAC (con gacutil.exe) e delle interfacce COM del file tlb (con regasm.exe).
- Creazione di una applicazione MFC nativa che usi i metodi esposti a COM Interop per aprire le winforms in modalità : modal , child e modeless.
- Visual Studio 2010 Express per la libreria di classi e una versione Professional o superiore (anche di prova), che includa MFC, per il programma di test . Infine il .Net Framework 4 SDK. (Se avete Visual Studio 2010 avrete già il .Net 4 SDK).
using System.Runtime.InteropServices;di seguito alle altre direttive using.
Ora c’è da segnalare che dovendo rendere visibili a COM i metodi della nostra classe è necessario che questi vengano dichiarati nel modo corretto, cioè all’interno di COM interface pubbliche, di tipo dual,dispatch o IUnknown ( si aggiungono allo scopo gli attributi GUID e InterfaceType alla COM Interface). La nostra classe (nel mio caso CDialogCOM) dovrà discendere dalla COM Interface e deve implementare i metodi dichiarati nella interface di base. In altre parole la nostra classe deve avere come classe base la COM interface.
Pertanto nel file sorgente della classe dobbiamo creare:
- una COM interface pubblica con gli attributi GUID (globally unique identifier) e ComInterfaceType.InterfaceIsIDispatch (il tipo di interfaccia per il relativo binding). Nel caso specifico è necessario che il binding avvenga solo in maniera ritardata, quindi sceglieremo il tipo ComInterfaceType.InterfaceIsIDispatch. Nelle COM interface vengono solamente dichiarati i metodie le proprietà, come si farebbe nelle classi astratte. I metodi dichiarati nelle COM interface possono avere dei dispatch identifier (DispId). C’è da dire che gli DispId sarebbero comunque assegnati dal framework anche se non esplicitamente dichiarati. Tuttavia dichiarandoli esplicitamente COM Interop mantiene lo stesso DispId per ogni applicazione che invoca il metodo (sia essa nativa che managed).
- una classe ereditata che implementi i metodi dichiarati nella classe base (la COM Interface). Quando abbiamo creato il progetto libreria di classi avevamo già una classe (CDialogCOM), sarà sufficiente far discendere questa dalla classe base interface.
- - Creiaimo la interface (deve essere public). Io chiamerò la interface IDlgCom.
- - Dichiariamo i metodi da esporre a COM.
- - Assegniamo ad ognuno il proprio DispId (dispatch identifier). I DispId dei metodi dichiarati verranno numerati in sequenza (1,2,3 etc..)
- - Aggiungiamo gli attributi Guid e ComInterfaceType.InterfaceIsIDispatch.
//Interface declaration - Copyright (C) Giuseppe Pischedda 2012
[Guid("F0702FE9-FA42-455F-A01B-7C6A164AAB3C")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IDlgCOM
{
[DispId(1)]
void MainFormShow();
[DispId(2)]
IntPtr GetMainHandle();
[DispId(3)]
void MainFormShowModal();
//Child
[DispId(4)]
void ChildFormShow();
[DispId(5)]
void ChildFormShowModal();
[DispId(6)]
IntPtr GetChildHandle();
}// fine interface
- - Facciamo discendere la classe creata in precedenza dalla interface IDlgCom
- - Aggiungiamo gli attributi Guid,ClassInterface,ProgId.
- - Implementiamo i metodi dichiarati nella interface.
//Class library implementation - Copyright (C) Giuseppe Pischedda 2012 [Guid("461D07C8-FB60-43FF-82E2-AE251F971CB0")] [ClassInterface(ClassInterfaceType.None)] [ProgId("DialogFromCom.CDialogCom")] public class CDialogCOM : IDlgCOM { private frmMain MainFrm; private frmChild ChildFrm; //Costruttore predefinito public CDialogCOM() { MainFrm = new frmMain(); ChildFrm = new frmChild(); } //Main public void MainFormShow() { frmMain main = new frmMain(); main.Show(); } public void MainFormShowModal() { frmMain main = new frmMain(); main.ShowDialog(); } public IntPtr GetMainHandle() { return MainFrm.Handle; } //Child public void ChildFormShow() { frmChild dl = new frmChild(); dl.Show(); } public void ChildFormShowModal() { frmChild child = new frmChild(); child.ShowDialog(); } public IntPtr GetChildHandle() { return ChildFrm.Handle; } }//fine classe
Se tutto è andato bene avremo realizzato la nostra libreria COM in C#.
Ora, prima di registrare la libreria che abbiamo creato, se riguardiamo l’obiettivo che ci siamo preposti, dobbiamo ancora aggiungere un ActiveX nella windows form e ricompilare il progetto. Metteremo un controllo ActiveX molto noto : Microsoft Windows Media Player.
Se non l’avete ancora fatto aggiungete il componente windows media player nella casella degli strumenti di VStudio. Quindi fate drag&drop sul form e tentate di trascinare il controllo ActiveX. Ho detto tentate perché Visual Studio NON consentirà questa operazione.

Questo avviene per motivi legati all’allocazione di oggetti activex che non approfondisco qui. Per ovviare al problema, eliminare i due file aggiunti in reference da VStudio al momento del drag&drop (AxWMPLib e WMPLib).

Quindi dal menu Progetto -> Proprietà scegliere Firma e quindi togliere la spunta al flag Firma assembly. Ricompilare il progetto. Rieseguire il drag&drop del controllo ActiveX Windows Media Player. Finalmente l’operazione riesce. Abbiamo inserito l'ActiveX.

Dal menu Progetto-> Proprietà rimettere il segno di spunta nel flag Firma assembly.
Tentare di ricompilare il progetto. Già… dico tentare perché il progetto NON verrà ricompilato da VStudio che darà un errore di KeyContainer.

A quanto ne so dovrebbe trattarsi di un bug di VStudio. Comunque, per correggere il problema basta selezionare elenco errori nella tabsheet in basso, quindi prendere nota dell’errore (basta un semplice copia/incolla in blocco note). Quindi nella cartella dove avete salvato il progetto, creare un file vuoto senza estensione con il nome che viene descritto nell’errore dopo “il percorso”. Nel mio caso : VS_KEY_38CC07E012A26B3F.
Ricompilare il progetto. Finalmente la nostra libreria COM ha il suo controllo ActiveX ospitato correttamente. A questo punto, giusto per far fare qualcosa alla windows form a runtime, mettiamo qualche button, uno per scegliere il file multimediale per il controllo media player, uno per mostrare un messaggio a runtime un altro per chiudere la windows form stessa etc.
Aggiungere una seconda windows form al progetto. Nella seconda form mettere un componente .Net. Nel mio caso ho usato SlideText Component (creato da me peraltro).
Creare una cartella in C: e chiamarla per esempio prove_com. Copiare la dll del progetto nella cartella appena creata (e anche gli altri assembly a corredo), dal menu Strumenti di VStudio scegliere la voce Prompt de comandi di Visual Studio.
Nel prompt scrivere il comando:
gacutil /i dialogfromcom.dll (e premere invio).e di seguito il comando:
regasm /tlb dialogfromcom.dll dialogfromcom.tlb (e premere invio).
Per quanto mi riguarda ho scelto una GUI in stile Windows 7. Quindi con ribbon. Aggiungere dei buttons al ribbon. Aprire il file di implementazione MainFrm.cpp e aggiungere la direttiva #import seguita dal percorso in cui si trova il file tlb dell'assembly (nel mio caso C:\\prove_com):
#import "C:\\prove_com\\dialogfromcom.tlb"
Aggiungere i gestori degli eventi ai vari buttons nel ribbon. Implementare nei rispettivi eventi onClick dei buttons il codice per avviare le dialogs in modalità: modal ,child o modeless.
//Modal dialog from C# COM - Copyright (C) Giuseppe Pischedda 2012 //Visual C++ code void CMainFrame::OnButton2() { // TODO: aggiungere qui il codice per la gestione dei comandi. // Interface pointer DialogFromCom::IDlgCOM *com_ptr; // Smart pointer via the GUID of the object DialogFromCom::IDlgCOMPtr p(__uuidof(DialogFromCom::CDialogCOM)); // Pointer assignment com_ptr = p; // Show C# winform com_ptr->MainFormShowModal(); }
//Child dialog from C# COM - Copyright (C) Giuseppe Pischedda 2012 //Visual C++ code void CMainFrame::OnButton3() { // TODO: aggiungere qui il codice per la gestione dei comandi. // Interface pointer DialogFromCom::IDlgCOM *com_ptr2; // Smart pointer via the GUID of the object DialogFromCom::IDlgCOMPtr p(__uuidof(DialogFromCom::CDialogCOM)); // Pointer assignment com_ptr2 = p; // Get C# winform handle HWND hWnd = (HWND)com_ptr2->GetMainHandle(); // CWnd pointer CWnd* pTest = CWnd::FromHandle(hWnd); // CFrameWnd pointer CFrameWnd* pFrameWnd = (CFrameWnd*)AfxGetApp()->GetMainWnd(); // CView pointer CView* pView = pFrameWnd->GetActiveView(); // CWnd parent pointer CWnd *parent = GetActiveWindow(); // Set the C# winform parent pTest->SetParent(pView); // Show C# winform pTest->ShowWindow(TRUE); }
//Modeless dialog from C# COM - Copyright (C) Giuseppe Pischedda 2012 //Visual C++ code void CMainFrame::OnButton6() { // TODO: aggiungere qui il codice per la gestione dei comandi. DialogFromCom::IDlgCOM *com_ptr; DialogFromCom::IDlgCOMPtr p(__uuidof(DialogFromCom::CDialogCOM)); com_ptr = p; com_ptr->MainFormShow(); }
Giuseppe Pischedda 2012