C# Interop with MFC
Using .Net Components / Controls in MFC applications
Part 3.
Completiamo l'applicazione MFC aggiungendo i gestori degli Eventi dei componenti e controls .Net nel sorgente MFC.
.Net Events
Genericamente parlando, nel .Net Framework un evento è un messaggio che un oggetto "manda" per segnalare che è avvenuto un
"qualcosa" che era atteso succedesse. Questo "qualcosa" può accadere a causa dell'interazione di un utente (es: click su un bottone) oppure
per altre cause generate internamente durante il funzionamento del programma, (es: il programma ha concluso il download di un file da internet).
L'oggetto che genera un evento, (event sender), notifica che l'evento è avvenuto ad
un qualche altro oggetto in "ascolto" (event receiver); quest'ultimo ha il compito di "gestire" (handle) l'evento, eseguendo a sua volta, una qualche azione.
Event and Delegate
Un oggetto che genera un evento (event sender) non sa quale altro oggetto o metodo, successivamente, gestirà l'evento stesso,
per cui la comunicazione tra il sender ed il receiver è indiretta ed avviene tramite una sorta di puntatore a funzione.
In ambiente .Net non esistono puntatori a funzione come in C/C++, ma esiste un tipo di classe, Delegate,
che è l'intermediario tra sender e receiver.
Qui sotto la dichiarazione di un Delegate in C#:
public delegate void MyControlEventHandler(object sender, MyCustomEventArgs e); //Delegate
public void MyControlDoSomething(object sender, MyCustomEventArgs e) {...} // EventHandler method
Nella dichiarazione qui sopra, possiamo notare che per il Delegate la sintassi è somigliante ad un metodo C#, tranne la keyword delegate.
Normalmente i delegates vengono dichiarati con due parametri:
- object sender: l'oggetto che genera l'evento
- MyCustomEventArgs: i dati che (eventualmente) possono essere passati al gestore dell'evento nel momento in cui esso si verifica.
Un EventArgs creato dallo sviluppatore (es: MyCustomEventArgs) ha senso qualora si debbano passare dei dati (es: una struct etc.) al metodo che riceverà un certo evento; in tutti gli altri casi è sufficiente la versione base EventArgs.
Continuiamo l'applicazione MFC
Ora che sappiamo cosa si intende con event e delegate in .Net, possiamo riprendere l'applicazione MFC che è stata iniziata nella parte 2 di questo post,
nella quale implementeremo, oltre ai restanti components e controls della libreria CFComponentN18.dll, i relativi eventi.
Nota : In realtà è possibile creare nella classe proxy (vedi Part 2) un solo UserControl ed utilizzarlo
come una Windows Form. Sarebbe sufficiente mettere tutti i componenti del CFComponentsN18.dll nello UserControl e gestire quindi gli eventi dei vari
componenti da esso, tuttavia noi vogliamo gestire gli eventi .Net dal nostro codice C++ nel progetto MFC.
Inoltre dovremo ovviare al problema del BindingContext (vedi Part 2), pertanto, a tale scopo, creeremo nella classe proxy, (vedi sempre Part 2), 8 UserControls.
Riprendendiamo il progetto MFC creato nella Part 2).
In Visual Studio apriamo la dialog MFC da Resource View; sulla dialog MFC aggiungeremo (drag&drop dalla Toolbox MFC)
come segnaposti 8 GroupBox (ogni segnaposto deve essere di tipo static control).
Andremo quindi settare i relativi ID secondo lo schema introdotto nella parte 2.
Nota: 8 segnaposti in quanto nell'assembly CFComponentsN18.dll son presenti 1 component e 7 controls.
Nella dialog MFC infine ci servirà un Button ed un Edit Contol (entrambi componenti MFC).
Settiamo gli ID di ogni static control (io ho usato 8 GroupBox) della dialog MFC con un id significativo:
- IDC_CFCOMPONENT
- IDC_TXT_COGNOME
- IDC_TXT_NOME
- IDC_CTRL_CMB_SEX
- IDC_NASCITA_DTPICKER
- IDC_CTRL_CMB_REGIONI
- IDC_CTRL_CMB_PROVINCE
- IDC_CTRL_CMB_COMUNI
Implementiamo gli 8 UserControls nella ProxyClassLibrary
Ogni UserControl conterrà un singolo controllo dell'assembly CFComponentN18.dll.
I nomi degli UserControls di questo esempio:
- CfComponent
- TxtCognome
- TxtNome
- CmbSesso
- DtNascitaPicker
- CmbRegioni
- CmbProvince
- CmbComuni
Il risultato dovrebbe essere un qualcosa del genere:
Fatto ciò, dichiariamo nella classe della dialog MFC 8 nuovi membri ( i nostri 8 .Net controls).
//............
private:
CWinFormsControl<ProxyClassLibrary::CfComponent> m_cfComponent;
CWinFormsControl<ProxyClassLibrary::TxtCognome> m_txtCognome;
CWinFormsControl<ProxyClassLibrary::TxtNome> m_txtNome;
CWinFormsControl<ProxyClassLibrary::CmbSesso> m_cmbSesso;
CWinFormsControl<ProxyClassLibrary::DtNascitaPicker> m_dtNascitaPicker;
CWinFormsControl<ProxyClassLibrary::CmbRegioni> m_cmbRegioni;
CWinFormsControl<ProxyClassLibrary::CmbProvince> m_cmbProvince;
CWinFormsControl<ProxyClassLibrary::CmbComuni> m_cmbComuni;
Quindi nel file di implementazione della dialog MFC individuiamo la funzione DoDataExchange(CDataExchange* pDX)
e subito dopo la riga CDialogEx::DoDataExchange(pDX); aggiungiamo le righe:
DDX_ManagedControl(pDX, IDC_CFCOMPONENT, m_cfComponent);
DDX_ManagedControl(pDX, IDC_TXT_COGNOME, m_txtCognome);
DDX_ManagedControl(pDX, IDC_TXT_NOME, m_txtNome);
DDX_ManagedControl(pDX, IDC_CTRL_CMB_SEX, m_cmbSesso);
DDX_ManagedControl(pDX, IDC_NASCITA_DTPICKER, m_dtNascitaPicker);
DDX_ManagedControl(pDX, IDC_CTRL_CMB_REGIONI, m_cmbRegioni);
DDX_ManagedControl(pDX, IDC_CTRL_CMB_PROVINCE, m_cmbProvince);
DDX_ManagedControl(pDX, IDC_CTRL_CMB_COMUNI, m_cmbComuni);
Eseguiamo l'applicazione ed otterremo questo:
Gestire eventi .Net in MFC
Selezionando una regione italiana nel controllo RegioniDropDownList il controllo ProvinceDropDownList
deve aggiornarsi dinamicamente mostrando l'elenco delle province italiane appartenenti all regione selezionata e
di conseguenza, la stessa cosa avverrà nel controllo ComuniDropDownList selezionando una provincia nel controllo
ProvinceDropDownList.
Tuttavia in questa fase, eseguendo l'applicazione MFC, viene popolato solo il controllo RegioniDropDownList e selezionando
in questo una regione italiana, i due controlli collegati in cascata non faranno assolutamente nulla se non mostrare un
laconico invito, all'utente, a selezionare qualcosa che non esiste!
Vedi immagini qui sotto:
Questo è normale in quanto i 3 controlli vengono sincronizzati al verificarsi dei due eventi xxxSelectedValueChanged:
- evento RegioneSelectedValueChanged : viene popolato il controllo ProvinceDropDownList
- evento ProvinceSelectedValueChanged: viene popolato il controllo ComuniDropDownList
Affinchè il tutto funzioni correttamente, noi dovremo intercettare i 2 eventi, lato MFC, e scrivere in essi 1 riga di codice (indicheremo al
controllo corrente, l'ID dell'elemento correntemente selzionato), tutto il resto, connessione al database, queries etc.
verrà gestito internamente dai vari componenti .Net.
In pratica non dovremo fare altro che aggiungere delle funzioni membro alla classe della dialog che abbiano la stessa identica sintassi
dei delegates C# dei controlli .Net dell'assembly CFComponentN18.dll, dichiarando inoltre (sempre nella stessa classe)
che un certo metodo, è un metodo event handler.
Nella fase di inizializzazione dell'applicazione MFC, con l'operatore +=, assegneremo il nostro metodo all' event handler C#.
Ricapitolando:
- nel file header della classe della dialog dichiariamo la funzione C++ che diventerà così un event handler
- nel file di implementazione assegnamo, con l'operatore +=, all'event handler C#, la funzione C++ dichiarata nel file header
Per far ciò, MFC usa delle macro:
- BEGIN_DELEGATE_MAP
- EVENT_DELEGATE_ENTRY
- MAKE_DELEGATE
#include <vcclr.h>
#include <msclr\event.h>
Aggiungiamo nel file header della classe MFC una sezione public e le prime due macro:
//......
public:
// delegate map
BEGIN_DELEGATE_MAP(CMFCCSharpDemoDlg)
EVENT_DELEGATE_ENTRY(OnRegioniSelectedValueChanged, System::Object^,
CFComponentN18::RegioniDropDownList::RegioniDropDownList::RegioneSelectedEventArgs ^)
EVENT_DELEGATE_ENTRY(OnProvinceSelectedValueChanged, System::Object^,
CFComponentN18::ProvinceDropDownList::ProvinceDropDownList::ProvinciaSelectedEventArgs ^)
END_DELEGATE_MAP()
La macro EVENT_DELEGATE_ENTRY ha 3 argomenti:
- il nome della funzione C++ che useremo nel codice MFC quale event handler
- il primo parametro del metodo delegate C# (System::Object ^)
- il secondo parametro del metodo delegate C# (xxxSelectedEventArgs ^)
Dichiariamo quindi le funzioni C++ che assegneremo all'event handler dei rispettivi controlli .Net
void OnRegioniSelectedValueChanged(System::Object ^obj,
CFComponentN18::RegioniDropDownList::RegioniDropDownList::RegioneSelectedEventArgs ^e);
void OnProvinceSelectedValueChanged(System::Object ^obj,
CFComponentN18::ProvinceDropDownList::ProvinceDropDownList::ProvinciaSelectedEventArgs ^e);
Apriamo il file di implementazione della dialog MFC ed implementiamo le funzioni OnRegioniSelecteValueChanged
e OnProvinceSelectedValueChanged:
void CMFCCSharpDemoDlg::OnRegioniSelectedValueChanged(System::Object ^ obj, CFComponentN18::RegioniDropDownList::RegioniDropDownList::RegioneSelectedEventArgs ^ e)
{
m_cmbProvince.GetControl()->ProvinceDropDownList->UpdateData(e->ItemValue);
}
void CMFCCSharpDemoDlg::OnProvinceSelectedValueChanged(System::Object ^ obj, CFComponentN18::ProvinceDropDownList::ProvinceDropDownList::ProvinciaSelectedEventArgs ^ e)
{
m_cmbComuni.GetControl()->ComuniDropDownList->UpdateData(e->ItemValue);
}
Nota: come si può notare la sintassi delle funzioni è identica al prototipo di delegate ed il linguaggio è managed C++.
Nello stesso file di implementazione individuiamo la funzione OnInitDialog() ed
aggiungiamo il codice per assegnare gli event handlers ed inizializzare i nostri componenti .Net;
//.......
// TODO: Add extra initialization here
m_cfComponent.GetControl()->CFComponent->CognomeTextBox = m_txtCognome.GetControl()->CognomeTextBox;
m_cfComponent.GetControl()->CFComponent->NomeTextBox = m_txtNome.GetControl()->NomeTextBox;
m_cfComponent.GetControl()->CFComponent->SessoDropDownList = m_cmbSesso.GetControl()->SessoDropDownList;
m_cfComponent.GetControl()->CFComponent->DataNascitaPicker = m_dtNascitaPicker.GetControl()->DataNascitaPicker;
m_cfComponent.GetControl()->CFComponent->RegioniDropDownList = m_cmbRegioni.GetControl()->RegioniDropDownList;
m_cfComponent.GetControl()->CFComponent->ProvinceDropDownList = m_cmbProvince.GetControl()->ProvinceDropDownList;
m_cfComponent.GetControl()->CFComponent->ComuniDropDownList = m_cmbComuni.GetControl()->ComuniDropDownList;
m_cmbRegioni.GetControl()->RegioniDropDownList->RegioneSelectedValueChanged +=
MAKE_DELEGATE(CFComponentN18::RegioniDropDownList::RegioniDropDownList::RegioneSelectedEventHandler,
OnRegioniSelectedValueChanged);
m_cmbProvince.GetControl()->ProvinceDropDownList->ProvinciaSelectedValueChanged +=
MAKE_DELEGATE(CFComponentN18::ProvinceDropDownList::ProvinceDropDownList::ProvinciaSelectedEventHandler,
OnProvinceSelectedValueChanged);
Mandiamo in esecuzione l'applicazione e finalmente i controlli fanno ciò che devono!
Nel file header della classe della dialog MFC aggiungiamo una variabile membro riferita al controllo Edit Text di tipo CEdit
CEdit m_editCtrl;
Nel file di implementazione della dialog MFC implementiamo il button click del Button "Calculate";
void CMFCCSharpDemoDlg::OnBnClickedButtonCalculate()
{
// TODO: Add your control notification handler code here
CString szCFCode = m_cfComponent.GetControl()->CFComponent->GetCodiceFiscale;
m_editCtrl.SetWindowTextW(szCFCode);
}
Eseguiamo l'applicazione, riempiamo i campi e godiamoci il risultato!