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:
- in preda ad un furioso attacco di follia riscriviamo in C++ la parte dell'applicazione relativa al database.
- oppure, risparmiando tempo e parecchia fatica, possiamo usare i componenti C# direttamente in MFC.
Esiste, guardacaso :), un componente .Net creato dal sottoscritto, freeware tra le altre cose, scritto in Visual C#
per .Net 4.7, che fa esattamente ciò che ho descritto più su, cioè si connette ad un database tramite ADO.Net,
fa tutto il lavoro di calcolo e restituisce una stringa con il risultato voluto.
Il componente in questione è il Codice Fiscale Components 2018 for .Net, un insieme di componenti ed UserControls per il calcolo del codice fiscale delle persone fisiche, da scaricare da questo stesso sito a
questo link
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:
- CFComponentN18.dll
- dataX_2018.sdf
- Licenza.txt
- Readme.txt
- System.Data.SqlServerCe.dll
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:
- 1) Installare Microsoft .Net Framework 4.7
- 2) Installare Il runtime di Sql Server Compact 4 SP1
- 3) Il pacchetto zip del Codice Fiscale Components 2018 (vedi sopra)
- 4) MFC installato in Visual Studio 2017
-
5) Copiare il file dataX_2018.sdf nella directory:
"C:\Users\user_name\AppData\Local" dove user_name è il nome dell'utente dove volete eseguire l'applicazione.
In fase di sviluppo user_name corrisponderà al nome dell'utente in cui avete installato Visual Studio.
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.
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:
- Common Language Runtime Support : Common Language Runtime Support (/clr)
- .Net Target Framework Version : 4.7
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.
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.
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:
- 1 Component
- 7 Controls
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.
Per i dettagli sul funzionamento del componente in ambiente .Net consultare la pagina web al link
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.
Mandiamo in run il progetto e...
ooops... il controllo .Net è si presente ma l'elenco delle regioni italiane
che dovrebbe mostrare non 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:
- A) creare un nuovo BindingContext manualmente ogni volta e successivamente assegnare il DataSource al controllo
- B) delegare al runtime .Net la gestione di un BindingContext al posto nostro
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
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...
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!!