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.


Applicazione nativa MFC con winforms C# via 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.
A dirla tutta quando si creano componenti COM in C# (ed in .Net in generale) in realtà si crea un assembly .Net dal quale si possono successivamente importare le librerie dei tipi (in un file TLB) attraverso un wrapper. Una volta creato il componente o il metodo COM, questo si deve registrare nel sistema operativo. Per gli oggetti COM nativi la registrazione nel sistema avviene eseguendo lo strumento a riga di comando regsvr32 (per deregistrare il componente regsvr32 /u). Per gli oggetti COM realizzati in .Net invece lo strumento è regasm.exe (che fa parte del .Net Framework SDK).
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.

L’obiettivo:
Vogliamo richiamare da un’applicazione nativa (MFC) una windows form, con un controllo ActiveX al suo interno, via COM Interop. Quindi dovremo creare un assembly .Net (una dll) aggiungere a questa un paio di winforms ed esporre a COM Interop, tramite il wrapper , i metodi per richiamare le windows form. Per complicare un po’ la cosa aggiungerei che la windows form usata dall’applicazione MFC dovrà, a seconda del contesto, essere modal, child o modeless , rispetto all’applicazione nativa. Infine aggiungerei (per esagerare...) una seconda windows form nella quale hostare un componente .Net.

I passi da seguire:
  • 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.

Strumenti:
  • 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).

1) Creazione della libreria di classi COM
Eseguire Visual Studio, creare un nuovo progetto in Visual C#, scegliere come modello di progetto “Libreria di Classi”. Dare un nome al progetto. Il mio si chiamerà banalmente DialogFromCOM. Salvare per esempio in una sottocartella sul desktop. Di default Visual Studio creerà un progetto con un file (tra gli altri) che chiamerà Class1.cs. Rinominiamo il file con un nome significativo, il mio lo chiamerò CDialogCOM.cs. Rinominando il file, VStudio rinominerà anche la classe. Se non l’avevamo ancora fatto aggiungiamo la direttiva
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.

Dichiarazione della COM 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
Implementazione della classe ereditata:
  • - 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
2) Aggiunta di una windows form al progetto
Aggiungere una windows form al progetto. Click destro sul nome del progetto e scegliere Aggiungi -> Nuovo Elemento -> Windows Form. Rinominare il file a scelta. Nel mio caso frmMain.cs. Dal menù Progetto -> Proprietà fare click su Informazioni assembly. Nella dialog mettere il flag su Rendi visibile a COM e confermare con OK. Ora fare click su Firma, mettere il flag su Firma assembly, nella combo che viene attivata scegliere Nuova, mettere il nome del file e la password. Confermare con OK. Compilare il progetto.
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.
Applicazione nativa MFC con winforms C# via COM


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).

Applicazione nativa MFC con winforms C# via COM




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.

Applicazione nativa MFC con winforms C# via COM


Tuttavia per poter registrare nella GAC il nostro assembly dobbiamo necessariamente firmarlo.

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.

Applicazione nativa MFC con winforms C# via COM


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.

Applicazione nativa MFC con winforms C# via COM



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).

3) Registrazione nella GAC e registrazione dei tipi.
Ora che abbiamo la nostra libreria COM firmata con strong-name, possiamo registrarla nella GAC.

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).

Applicazione nativa MFC con winforms C# via COM


Applicazione nativa MFC con winforms C# via COM



4) Ora non resta che richiamare le nostre windows forms da un’applicazione nativa MFC.
Per chi non avesse il supporto MFC, è consigliato installare una versione di prova di Visual Studio 2010 Professional o superiore. Avviare Visual Studio, scegliere nuovo progetto-> Visual C++ -> Applicazione MFC. Dare un nome al progetto. Il mio si chiamerà DialogComTest. Il progetto dovrà essere un progetto a documento singolo con supporto Document/View. Andare avanti nella procedura giudata fino alla scelta della classe per la vostra view. Scegliere come classe base CFormView. Verrà indicato che non sarà possibile avere il supporto stampa. Continuare.
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"

Applicazione nativa MFC con winforms C# via COM


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.

Applicazione nativa MFC con winforms C# via COM



//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();
	
	
	
}




Se il post ti è piaciuto puoi fare una donazione all'autore, l'importo è a tua libera scelta.

Grazie

Giuseppe Pischedda 2012