Modern C# Interop with MFC

A modern way to use pure .Net Components and Controls in MFC applications

Part 2.

Creiamo un'applicazione MFC che utilizza components ed user controls scritti in C#.



Simulazione di un problema reale
Immaginate di dover connettere una vostra applicazione MFC ad un certo database SQL via ADO.Net.
Immaginate ora di avere già sottomano o, ancora meglio, di aver scritto un componente od un user control in .Net (per esmpio C#), che accede proprio a quel database, fa tutto il "lavoro sporco" e ci restituisce il risultato voluto.

Dato che MFC, si sa, non può conettersi (nativamente) ad un database ADO.Net avremo due possibili soluzioni:

Una volta scaricato il pacchetto zip del componente decomprimetelo per comodità in una cartella sul desktop, dovrà essere in vista quando lo useremo.
Dopo aver decompresso il file zip nella cartella saranno presenti i files:

Nota: come avrete notato c'è il riferimento a SqlServerCe, cioè SQL Server Compact ed anche il database dataX_2018.sdf.
Se non avete nella vostra macchina di sviluppo almeno il runtime di SqlServer Compact 4 SP1 è necessario scaricarlo ed installarlo (dal sito Microsoft) prima di continuare, viceversa, dato che il componente .Net a runtime si dovrà connettere al database SqlServerCE, esso non funzionerà.


Un breve recap prima di iniziare

Per creare la nostra applicazione dovranno essere soddisfatti i seguenti prerequisiti:

Assicuriamoci di aver completato i 5 punti delle operazioni preliminari prima di proseguire.

Partenza
Iniziamo creando un progetto VC++ e quindi un'applicazione MFC di tipo Dialog, con il classico wizard di Visual Studio.
Il tipo di applicazione (Dialog) è l'unica cosa che andremo a settare, per il resto va bene ciò che il wizard di Visual Studio ha già proposto di default.

MFC and C# MFC and C#


Una volta creato il progetto avremo la classica applicazione MFC nativa per win32. Ma noi dobbiamo fare in modo che diventi invece un'applicazione gestita (o managed se si preferisce).

Perciò andiamo alle impostazioni del progetto e selezionando Project → Properties
e nella sezione Project Default settiamo:

MFC and C#


Nota: la versione del Framework deve obbligatoriamente corrispondere a quella usata per compilare il component e/o lo UserControl .Net che vogliamo usare nella nostra app MFC che nel nostro caso è la, appunto, 4.7.


Dopo le modifiche al progetto MFC, se guardiamo il nodo References in Solution Explorer in Visual Studio, vedremo che è stato aggiunto un reference a mscorlib, il che significa che il nostro progetto MFC, è in grado di "comprendere" il managed C++ ed emetterà, quando compilato, un assembly che contiene codice gestito.

MFC and C#


A questo punto aggiungiamo un reference alla dll CFComponentN18.dll facendo click destro sul nodo e quindi dal menu contestuale References → Add Reference...

Fatto ciò, Visual Studio importerà nel nostro progetto MFC il Component e gli UserControls contenuti nell'assembly CFComponentN18.dll.

MFC and C# MFC and C# MFC and C#


Nota: diversamente a quanto normalmente avviene in ambiente .Net ( Visual C#, VB.Net etc.), purtroppo non è consentito (o per lo meno ancora non lo è) aggiungere dei componenti (.Net) alla Toolbox (MFC) in VStudio e fare quindi drag&drop di questi dalla Toolbox alla dialog MFC per utilizzarli;
Components o UserControls (.Net) in ambiente MFC, infatti, devono essere dichiarati membri, di tipo CWinFormsControl, della classe della dialog MFC.
I membri di tipo CWinFormsControl verranno creati a runtime durante l'inizializzazione della dialog MFC;
però prima di vedere in dettaglio come fare, è necessaria una breve digressione sulla differenza tra Components e Controls e della logica del CFComponentsN18.

.Net Components vs Controls.
Nell'universo .Net un Component è un oggetto che non necessita di una interfaccia utente, rimane "invisibile" all'utente dell'applicazione. Viceversa un Control è un oggetto che ha un'interfaccia utente, l'utente dell'applicazione interagisce col controllo (es. ComboBox).
Tutti i Controls sono anche Components, ma non tutti i Components sono anche Controls; questo avviene perchè nella catena discendente delle classi .Net, Components e Controls si trovano a differenti liveli di ereditarietà; pertanto se scriviamo una classe che discende direttamente da Component, questa non potrà fruire, ovviamente, delle le stesse proprietà e metodi di una classe più in basso nella catena ereditaria come quella di un controllo utente (ComboBox per esempio) in quanto è dalla classe System.ComponentModel.Component che discende la classe System.Windows.Forms.Control e solo da quest 'ultima i controlli utente (TextBox, ComboBox etc.).

Guardacaso, l'assembly che useremo, CFComponentN18.dll, contiene proprio:

L'assembly CFComponentN18
E' un assembly .Net 4.7 scritto in C#. Al suo interno c'è un Component (vedi qui sopra), che discende direttamente da System.ComponentModel.Component, e 7 Controls, di cui:
2 discendono dal controllo System.Windows.Forms.TextBox,
1 da System.Windows.Forms.DatetTimePicker e
4 dal controllo System.Windows.Forms.ComboBox.
Nota oltre che dalle citate classi basi, component e controls, discendono da varie Interfacce e classi C# create ad hoc; chi fosse interessato può scaricare il sorgente e dare un'occhiata più approfondita.



Houston, we've had a problem here
Ora che abbiamo le idee un pò più chiare (si spera), siamo quasi pronti a proseguire nello scrivere la nostra applicazione MFC di esempio.
Manca ancora un passaggio importante, senza il quale ci troveremmo di fronte ad un grosso problema.
Per capire di cosa si tratta faremo un test preliminare sul componente .Net.

Da Solution Explorer in Visual Studio apriamo il file stdafx.h ed aggiungiamo l'include file

 #include <afxwinforms.h>

Quindi apriamo il file header dove è dichiarata la classe della dialog MFC (es: myProjectDlg.h) e dichiariamo il primo membro di tipo CWinFormsControl:

 CWinFormsControl<CFComponentN18::RegioniDropDownList::RegioniDropDownList> m_cmbRegioni;

Nota: RegioniDropDownList è un controllo derivato da ComboBox che a runtime si connette al database e viene popolato con l'elenco delle regioni italiane.


Passiamo da Solution Explorer a Resource View ed apriamo la nostra dialog nell'editor.
Cancelliamo il controllo statico ("To do:...") e dalla Toolbox droppiamo un nuovo controllo statico nella dialog, un GroupBox andrà benissimo.
Fatto ciò, assicuriamoci che il controllo appena aggiunto sia selezionato, andiamo in Properties e cambiamo la properietà ID dando un nome significativo, per esempio IDC_CTRL_CMB_REGIONI.
Come avrete già capito, il controllo statico serve come segnaposto nella dialog, a runtime è lì che avremo il controllo .Net.

Torniamo in Solution Explorer ed apriamo il file di implementazione della dialog (es: myProjectDlg.cpp);
Individuiamo la funzione membro della dialog

DoDataExchange(CDataExchange* pDX)

e quindi al suo interno aggiungiamo:

DDX_ManagedControl(pDX, IDC_CTRL_CMB_REGIONI, m_cmbRegioni);

Note: Assicuriamoci di selezionare la funzione membro della classe della dialog in quanto nel file cpp ci sono 2 metodi DoDataExchange(CDataExchange* pDX) uno per classe della dialog MFC ed uno per la classe CAboutDialog.

MFC and C# MFC and C# MFC and C# MFC and C#


Mandiamo in run il progetto e... ooops... il controllo .Net è si presente ma l'elenco delle regioni italiane che dovrebbe mostrare non c'è!!!

MFC and C#


Il problema è creato indirettamente da una delle proprità del controllo (ricordiamoci che deriva un ComboBox) e precisamente la proprietà: DataSource.
La proprietà in questione serve per assegnare (ovviamente) un'origine di dati al controllo, la quale, non necessariamente è una tabella od una query proveniente da un database, ma può benissimo anche essere una semplice lista di stringhe e così via.
Quando alla proprietà DataSource del controllo viene assegnata un'origine di dati, il controllo viene popolato e "ridisegnato" da Windows, ma il runtime .Net che dietro le quinte gestisce effettivamente l'oggetto DataSource, per fare correttamente il suo lavoro, necessita di un cosiddetto BindingContext, un oggetto che in una Windows Form è sempre presente (e gestito) ma che nel nostro caso, dato che non abbiamo alcuna Windows Form, non viene nemmeno creato.

A questo punto si aprono 2 scenari:

Inutile dire che l'opzione B), oltre che la più comoda è la migliore in quanto meno prona ad errori più o meno banali che, però, potrebbero potenzialmente introdurre bugs nell'applicazione.

Resta solo da capire come "dotare di Windows Form", in ambito MFC, un componente od un controllo .Net
In realtà è molto semplice e richiede l'aggiunta di un nuovo progetto .Net e la scrittura di qualche riga di codice.

ProxyClassLibary
Nota Il progetto che aggiungerò alla soluzione è un progetto ClassLibrary di Visual C#, ma se fosse managed C++ o VB.Net non ci sarebbe alcuna differenza.
In esso inseriremo un nuovo UserControl e su questo, nel designer di Visual Studio, dropperemo dalla Toolbox uno dei controlli dell'assembly CFComponentN18.dll che nel frattempo avremo aggiunto alla stessa Toolbox.

Aggiungiamo (alla soluzione Visual Studio in cui abbiamo il progetto MFC) un progetto .Net, per esempio in C#, di tipo ClassLibrary (.Net Framework), e chiamiamolo ProxyClassLibary.
Una volta creato il nuovo progetto eliminiamo il file Class1.cs, non ci serve.
Aggiungiamo al progetto ProxyClassLibrary un nuovo elemento, in Solution Explorer click destro su ProxyClassLibrary e quindi Add →New Item... → User Control.
Il nome del controllo è ininfluente, ma è sempre bene dare un nome significativo, in questo caso può andare bene CmbRegioni

MFC and C# MFC and C# MFC and C#


Nel progetto ProxyClassLibrary sarà ora presente il file CmbRegioni.cs che inizialmente contiene il seguente codice

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ProxyClassLibrary
{
    public partial class CmbRegioni : UserControl
    {
        public CmbRegioni()
        {
            InitializeComponent();
        }
    }
}

Facendo doppio click sul file CmbRegioni.cs in Solution Explorer in Visual Studio, verrà aperta la finestra dell'editor del controllo utente appena creato.


Aggiungiamo i controlli dell'assembly CFComponentN18.dll nella Toolbox


Facciamo drag&drop del componente RegioniDropDownList nell'area vuota del componente appena creato.
Fatto ciò facciamo click destro nel designer e scegliamo <> View Code.
Al codice già presente aggiungiamo una propretà pubblica che useremo per accedere al controllo da MFC.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using CFComponentN18.RegioniDropDownList;

namespace ProxyClassLibrary
{
    public partial class CmbRegioni : UserControl
    {
        public CmbRegioni()
        {
            InitializeComponent();
        }

        
        public RegioniDropDownList RegioniDropDownList   //this property in new
        {
            get { return regioniDropDownList1; }
            set { regioniDropDownList1 = value; }
        }

    }
        
}

Ora è necessario aggiungere al progetto MFC un nuovo reference al progetto ProxyClassLibrary come al solito: click destro sul nodo References → Add Reference...

MFC and C# MFC and C# MFC and C# MFC and C# MFC and C# MFC and C#


E' venuto il momento fare una modifica alla dichiarazione della variabile membro m_cmbRegioni nella classe della dialog MFC.

//.......
//.......
//........

CWinFormsControl<ProxyClassLibrary::CmbRegioni> m_cmbRegioni;


Mandiamo in esecuzione e.... BINGO!!!.... tutto funziona come deve!!

MFC and C#


Processing request, please wait...


MFC with C#, demo project  (zip) 


MFC with C#, demo bin  (exe) 







Giuseppe Pischedda 2018


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

Grazie