Cet article propose d’aborder un sujet technique concernant la migration d’une application exemple développée en MFC. L’une des complexités majeures d’une migration d’application basée sur une conception orientée objet est le fait que les hiérarchies de classes représentent des structures fortement couplées et que donc la migration d’une partie de l’application reste un exercice très complexe si nous n’utilisons pas une technique permettant d’introduire du découplage. L’application exemple utilisée dans cet article est développée en C++ sous Visual Studio 2008 et peut servir de référence en termes de démarche généralisable sur un projet de migration MFC/C++ natif vers .NET. L’approche retenue pour cette migration exemple est une migration étape par étape qui permet de mixer le code natif avec le code .NET via une couche d’interopérabilité C++/CLI. En adoptant cette approche, le projet de migration par réécriture (il n’existe pas d’outil de conversion automatique des MFC vers Windows Forms/WPF) évite l’effet tunnel qui est généralement fatal à ce type de projet. Ainsi, l’application en cours de migration peut être compilée et exécutée au fur et à mesure des développements sans que tout le code ait été réécrit en .NET.
De plus, le projet de migration doit permettre d’adopter les dernières techniques de conception et d’implémentation (patterns, etc.). Les développements seront donc réalisés dans un esprit d’amélioration (re-conception du code pour supporter le pattern MVC par exemple et découpler l’interface du code de gestion de l’IHM, ce qui aura pour effet de pérenniser l’application en la rendant évolutive et ouverte à une phase de migration ultérieure vers WPF). Ainsi, la migration doit permettre de transformer le code réécrit mais aussi d’adopter une conception intermédiaire permettant de bénéficier de l’existant tout en ouvrant l’application aux évolutions à venir.
L’application MFC
L’application exemple MFC est très simple et sert de support à l’explication de la démarche de migration étape par étape et aux patterns utilisés. L’application MFC est de type « Dialog based », de style « standard » et « use MFC as a Shared DLL ».
Note : L’auteur n’ayant pas l’expérience de développement en MFC, l’application exemple donnée dans cet article peut-être sujette à améliorations. Les notions de C++ remontant à « très loin », le code présenté dans cet article reste très discutable mais il permet d’avoir un exécutable sur lequel opérer le processus de migration.
Afin d’ouvrir l’application à .NET, nous avons modifié la configuration du projet pour intégrer le paramètre de compilation (/clr).
Nous allons montrer comment migrer l’IHM d’une classe Dialog MFC vers un UserControl .NET Windows Forms de manière à ce que le code sous-jacent reste natif. Pour cela, corsons le code afin que la classe Dialog MFC ait un paramètre supplémentaire dans son constructeur pour lui passer la référence d’un type natif qui sera utilisé dans le code de la classe Dialog MFC.
Le code de la classe CSampleMFCAppDlg:
// CSampleMFCAppDlg dialog
class CSampleMFCAppDlg : public CDialog
{
// Construction
public:
CSampleMFCAppDlg(SampleNativeClass* pNativeInst, CWnd* pParent = NULL); // standard constructor
// Dialog Data
enum { IDD = IDD_SAMPLEMFCAPP_DIALOG };
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
// Implementation
protected:
HICON m_hIcon;
CString m_sampleStr;
SampleNativeClass* m_pNativeInst;
// Generated message map functions
virtual BOOL OnInitDialog();
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
DECLARE_MESSAGE_MAP()
public:
afx_msg void OnBnClickedUpdateButton();
};
Son implémentation (partielle) :
// CSampleMFCAppDlg dialog
CSampleMFCAppDlg::CSampleMFCAppDlg(SampleNativeClass* pNativeInst, CWnd* pParent /*=NULL*/)
: CDialog(CSampleMFCAppDlg::IDD, pParent)
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
m_sampleStr = _T("Hello World");
m_pNativeInst = pNativeInst;
}
void CSampleMFCAppDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Text(pDX, IDC_EDIT1, m_sampleStr);
}
BEGIN_MESSAGE_MAP(CSampleMFCAppDlg, CDialog)
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
//}}AFX_MSG_MAP
ON_BN_CLICKED(IDC_UPDATE_BUTTON, &CSampleMFCAppDlg::OnBnClickedUpdateButton)
END_MESSAGE_MAP()
void CSampleMFCAppDlg::OnBnClickedUpdateButton()
{
char* buffer = new char[2];
CString strVal;
GetDlgItem(IDC_EDIT1)->GetWindowText(strVal);
GetDlgItem(IDC_STATIC1)->SetWindowText((LPCWSTR)strVal);
int value = m_pNativeInst->DoSomething(2);
sprintf(buffer, "%d\0", value);
CString buffer1(buffer);
GetDlgItem(IDC_STATIC2)->SetWindowText(buffer1);
delete buffer;
}
Ainsi que la déclaration et l’implémentation d’une classe native en C++ :
class SampleNativeClass
{
public:
SampleNativeClass(void);
~SampleNativeClass(void);
int DoSomething(int x) {return x * x; }
};
L’exécution du l’application MFC exemple donne la fenêtre dialogue suivante :

Migration de l’IHM par réécriture vers .NET
Comme nous l’avons mentionné, il n’existe pas d’outils de conversion MFC vers Windows Forms ou du moins s’il en existe ces derniers sont trop peu performants pour que nous en ayons trouvé la moindre trace de publicité sur Internet. Ainsi, nous utiliserons les méthodes amplement décrites dans divers articles français et anglais pour intégrer un contrôle .NET dans une application MFC (voir les références de l’article pour les liens en question).
Le contrôle .NET se présente de la manière suivante et a été re-designé pour correspondre à l’IHM de la dialogue MFC (il aurait pu être reconçu d’une manière différente pour correspondre plus avantageusement aux nouvelles ergonomies) :

Nous modifierons ensuite le code MFC comme décrit dans les nombreux articles d’intégration de UserControl .NET à MFC (nous laisserons volontairement les éléments existants de l’IHM MFC pour montrer l’intégration des deux technologies à titre d’exemple ; Dans la réalité, le contrôle .NET remplacera entièrement les éléments de l’IHM MFC).
Le code SampleMFCAppDlg.h devient alors :
…
// Implementation
protected:
HICON m_hIcon;
CString m_sampleStr;
SampleNativeClass* m_pNativeInst;
// Ajout du placeholder MFC pour afficher le UserControl .NET
CWinFormsControl<MigratedControlLibrary::SampleControl> m_sampleControl;
…
Et le code de la fonction DoDataExchange est modifié pour intégrer le chargement du contrôle .NET :
void CSampleMFCAppDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Text(pDX, IDC_EDIT1, m_sampleStr);
// Ajouter le code de chargement du contrôle .NET
DDX_ManagedControl(pDX, IDMC_SampleControl, m_sampleControl);
}
Il ne faut pas oublier non plus d’ajouter la déclaration suivante au fichier stdafx.h du projet MFC :
#include <afxwinforms.h> // MFC Windows Forms support
Nous avons maintenant une application MFC qui affiche un UserControl .NET :

Pour le moment, le UserControl .NET (SampleControl) ne fait rien car il n’interopère pas avec l’existant natif. C’est là que les choix de conception et d’implémentation sont décisifs pour nous permettre de continuer cette migration étape par étape avec un objectif de résultat à 100% .NET en C#.
Implémentation d’une variante du pattern MVC
La migration vers .NET est une occasion qui nous permet de faire évoluer le code pour utiliser des techniques de conception plus récentes. Ainsi, nous utiliserons une variante du pattern MVC basée sur la vue passive pour découpler la vue (le UserControl) de ses actions. De cette manière, nous pourrons tester unitairement le contrôleur IHM et intégrer les tests unitaires de l’IHM de l’application en cours de migration aux scénarii de tests unitaires.
La philosophie du pattern MVC qui est de répartir l’interaction avec l’IHM en 3 rôles sera respectée. Cette variante nous permettra d’initialiser un contrôleur C++/CLI depuis le code natif et qui sera ensuite utilisé par le UserControl .NET (ici équivalent à View/Vue).

Cette variante nous permettra d’avoir la conception logique suivante :

Le référencement de projets suit la même logique dans Visual Studio. Nous ajouterons le projet .NET UIControllerLibrary (Projet de type Class Library) pour les implémentations des contrôleurs d’IHM.
La classe SampleController sera le contrôleur du UserControl .NET (SampleControl) :
public abstract class Controller { }
public abstract class SampleController : Controller { }
Et le code du UserControl évolue pour référencer le contrôleur comme décrit dans la variante du pattern MVC.
…
using UIControllerLibrary;
namespace MigratedControlLibrary
{
public partial class SampleControl : UserControl
{
private SampleController _sampleController = null;
public SampleController SampleController
{
get { return _sampleController; }
set { _sampleController = value; }
}
public SampleControl()
{
InitializeComponent();
}
}
}
Note : L’implémentation « abstract » du contrôleur spécialisé sera expliquée dans la suite de l’article.
L’application MFC est considérée comme mixant plusieurs couches applicatives (si à l’époque les couches n’avaient pas été isolées) et au moins la couche d’IHM (Presentation Layer). En plus du projet MigratedControlLibrary, nous allons référencer maintenant le projet UIControllerLibrary depuis le projet MFC.
Il « ne reste plus alors » qu’à « brancher » le code natif au code managé et surtout à migrer par réécriture un maximum de code UI depuis le MFC vers le contrôleur.
Implémentation du contrôleur interopérable
Le contrôleur interopérable est l’élément clé de la conception décrite dans cet article car c’est lui qui permet de découpler le code natif du code .NET et qui apporte la souplesse de la migration étape par étape. Pour faire interopérer le code natif et le code managé, nous allons utiliser des « wrappers » C++/CLI (voir les références pour un lien vers la présentation de wrappers C++/CLI).
Nous ajoutons une classe C++/CLI au projet MFC et nous la faisons hériter de la classe contrôleur spécialisée développée en C#. Ci-dessous le code de déclaration du contrôleur interopérable en C++/CLI (fichier SampleInteropController.h) :
#pragma once
ref class SampleInteropController : UIControllerLibrary::SampleController
{
public:
SampleInteropController(void) { }
};
Il faut ensuite « connecter » l’instance de ce contrôleur à notre UserControl (View) .NET. Pour cela, nous modifions le code de SampleMFCAppDlg.cpp :
…
#include "SampleInteropController.h"
…
Et enfin :
void CSampleMFCAppDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Text(pDX, IDC_EDIT1, m_sampleStr);
// Ajouter le code de chargement du contrôle .NET
DDX_ManagedControl(pDX, IDMC_SampleControl, m_sampleControl);
// Connecter le UserControl .NET (View) au contrôleur interopérable.
SampleInteropController^ controller = gcnew SampleInteropController();
m_sampleControl->Controller = (UIControllerLibrary::SampleController^)controller;
}
Migration par réécriture du code UI natif MFC vers le contrôleur .NET
Ici, nous pourrions envisager l’utilisation d’un outil de conversion C++ vers C# si tant est que la réussite de la conversion représente un taux supérieur à 60%. Moins de 60% reviendrait à devoir pallier manuellement aux manquements du convertisseur automatique et représenterait potentiellement une charge de travail supérieure à celle nécessaire à la réécriture « propre » en C#, car il ne faut pas négliger le facteur gain de productivité lorsque le développeur passe en C#.
Le code natif suivant :
void CSampleMFCAppDlg::OnBnClickedUpdateButton()
{
char* buffer = new char[2];
CString strVal;
GetDlgItem(IDC_EDIT1)->GetWindowText(strVal);
GetDlgItem(IDC_STATIC1)->SetWindowText((LPCWSTR)strVal);
int value = m_pNativeInst->DoSomething(2);
sprintf(buffer, "%d\0", value);
CString buffer1(buffer);
GetDlgItem(IDC_STATIC2)->SetWindowText(buffer1);
delete buffer;
}
Est migré et réécrit en .NET dans le contrôleur correspondant (SampleController, en suivant la logique de la variante du pattern MVC) :
using System;
namespace UIControllerLibrary
{
public abstract class Controller { }
public abstract class SampleController : Controller
{
public event EventHandler<UpdateOutputLabelEventArgs> UpdateOutputLabel;
public void OnBnClickedUpdateButton(string input)
{
RaiseUpdateOutputLabel(input);
//char* buffer = new char[2];
//int value = m_pNativeInst->DoSomething(2);
//sprintf(buffer, "%d\0", value);
//CString buffer1(buffer);
//GetDlgItem(IDC_STATIC2)->SetWindowText(buffer1);
//delete buffer;
}
private void RaiseUpdateOutputLabel(string output)
{
if (UpdateOutputLabel != null)
{
UpdateOutputLabel(this, new UpdateOutputLabelEventArgs(output));
}
}
}
public sealed class UpdateOutputLabelEventArgs : EventArgs
{
private string _output;
public UpdateOutputLabelEventArgs(string output)
{
_output = output;
}
public string Output
{
get { return _output; }
set { _output = value; }
}
}
}
Le code du UserControl est, quant-à lui, modifié pour intégrer l’évolution du code de l’IHM suivant le pattern :
…
using UIControllerLibrary;
namespace MigratedControlLibrary
{
public partial class SampleControl : UserControl
{
private SampleController _sampleController = null;
public SampleControl()
{
InitializeComponent();
}
public SampleController Controller
{
get { return _sampleController; }
set
{
_sampleController = value;
_sampleController.UpdateOutputLabel += new EventHandler<UpdateOutputLabelEventArgs>(sampleController_UpdateOutputLabel);
}
}
void sampleController_UpdateOutputLabel(object sender, UpdateOutputLabelEventArgs e)
{
outputLabel.Text = e.Output;
}
private void updateButton_Click(object sender, EventArgs e)
{
_sampleController.OnBnClickedUpdateButton(inputText.Text);
}
}
}
L’exécution de l’application MFC nous donnera la sortie suivante :

Ce qui donne satisfaction. Mais vous aurez noté que l’ensemble du code UI n’a pas encore été réécrit (voir le code en commentaire):
public void OnBnClickedUpdateButton(string input)
{
RaiseUpdateOutputLabel(input);
//char* buffer = new char[2];
//int value = m_pNativeInst->DoSomething(2);
//sprintf(buffer, "%d\0", value);
//GetDlgItem(IDC_STATIC2)->SetWindowText((LPCWSTR)buffer);
//delete buffer;
}
Et pour cause, la partie non migrée par réécriture fait appel à un code natif que nous ne souhaitons pas migrer (mettons pour l’exemple que ce code natif représente plusieurs milliers de lignes de code et que nous préférons interopérer avec car il donne satisfaction et sa réécriture serait trop longue par rapport aux délais de livraison d’une première version de notre application partiellement migrée en .NET C#).
Le code du contrôleur s’enrichit alors de la manière suivante (noter DoSomething, UpdateOutputSomethingEventArgs, RaiseUpdateOutputSomething, UpdateOutputSomething) :
using System;
namespace UIControllerLibrary
{
public abstract class Controller { }
public abstract class SampleController : Controller
{
public event EventHandler<UpdateOutputLabelEventArgs> UpdateOutputLabel;
public event EventHandler<UpdateOutputSomethingEventArgs> UpdateOutputSomething;
public void OnBnClickedUpdateButton(string input)
{
RaiseUpdateOutputLabel(input);
RaiseUpdateOutputSomething(DoSomething(2).ToString());
}
protected abstract int DoSomething(int x);
private void RaiseUpdateOutputLabel(string output)
{
if (UpdateOutputLabel != null)
{
UpdateOutputLabel(this, new UpdateOutputLabelEventArgs(output));
}
}
private void RaiseUpdateOutputSomething(string something)
{
if (UpdateOutputSomething != null)
{
UpdateOutputSomething(this, new UpdateOutputSomethingEventArgs(something));
}
}
}
…
public sealed class UpdateOutputSomethingEventArgs : EventArgs
{
private string _something;
public UpdateOutputSomethingEventArgs(string something)
{
_something = something;
}
public string Something
{
get { return _something; }
set { _something = value; }
}
}
}
Le code du UserControl .NET évolue aussi pour intégrer la mise à jour de l’IHM sur l’événement UpdateOutputSomething.
…
public SampleController Controller
{
get { return _sampleController; }
set
{
_sampleController = value;
_sampleController.UpdateOutputLabel += new EventHandler<UpdateOutputLabelEventArgs>(sampleController_UpdateOutputLabel);
_sampleController.UpdateOutputSomething += new EventHandler<UpdateOutputSomethingEventArgs>(sampleController_UpdateOutputSomething);
}
}
void sampleController_UpdateOutputSomething(object sender, UpdateOutputSomethingEventArgs e)
{
doSomethingOutput.Text = e.Something;
}
…
Il reste à faire le pont avec le contrôleur interopérable et pour cela à modifier le constructeur pour refléter le comportement de la classe CSampleMFCAppDlg. Le code C++/CLI de SampleInteropController.h devient :
ref class SampleInteropController : UIControllerLibrary::SampleController
{
public:
SampleInteropController(SampleNativeClass* pNativeInst) { m_pNativeInst = pNativeInst; }
protected:
SampleNativeClass* m_pNativeInst;
};
Et le code MFC permettant de récupérer l’instance native devient :
void CSampleMFCAppDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Text(pDX, IDC_EDIT1, m_sampleStr);
// Ajouter le code de chargement du contrôle .NET
DDX_ManagedControl(pDX, IDMC_SampleControl, m_sampleControl);
// Connecter le UserControl .NET (View) au contrôleur interopérable.
SampleInteropController^ controller = gcnew SampleInteropController(m_pNativeInst);
m_sampleControl->Controller = (UIControllerLibrary::SampleController^)controller;
}
Et enfin, il est nécessaire de surcharger la méthode déclarée dans le contrôleur .NET à partir du contrôleur d’interopérabilité C++/CLI pour encapsuler l’appel au code natif :
ref class SampleInteropController : UIControllerLibrary::SampleController
{
public:
SampleInteropController(SampleNativeClass* pNativeInst) { m_pNativeInst = pNativeInst; }
virtual int DoSomething(int x) override { return m_pNativeInst->DoSomething(x);}
protected:
SampleNativeClass* m_pNativeInst;
};
Ainsi, le code du contrôleur interopérable est et doit être réduit à sa plus simple expression : l’encapsulation des méthodes natives.
L’exécution de l’application MFC nous donne enfin :

Où sommes-nous et prochaines étapes
Nous avons migré par réécriture une partie du code d’une application MFC en commençant par l’IHM. Le projet de migration tel que vu par l’utilisateur n’a d’intérêt que si celui-ci apporte des nouveautés visibles et notamment au travers de l’interface et de l’apport de nouvelles fonctionnalités visuelles. Nous avons montré comment mixer du code .NET à du code natif en utilisant C++/CLI, mécanisme tout à fait simple et documenté. L’élément clé de la démarche présentée ici est l’implémentation d’une variante du pattern MVC permettant d’interopérer avec le code natif au travers d’un contrôleur C++/CLI héritant de la déclaration d’un contrôleur .NET et réduisant la couche d’interopérabilité (le contrôleur C++/CLI) à sa plus simple expression : en effet, il serait tactiquement inefficace de continuer à implémenter du code en C++ ou C++/CLI si le but de la migration était bel et bien d’avoir une application 100% .NET en C#. Cela ne ferait que déplacer le problème et non le réduire.
Ce principe de conception permet de faire une migration étape par étape en conservant une application native MFC utilisable. Le pattern présenté dans l’article permet maintenant d’implémenter des tests unitaires sur les méthodes exposées par le contrôleur managé. Enfin, pour atteindre une application 100% .NET C#, il faudra continuer à appliquer le même principe d’interopérabilité à base de C++/CLI jusqu’à ce que tout le code ait été réécrit ou convertit en C#. Nous pourrons alors implémenter un contrôleur managé reprenant l’interface du contrôleur C++/CLI pour utiliser les types réécrits en .NET C#. Nous pourrons aussi modifier la déclaration du contrôleur managé afin qu’il ne soit plus abstrait (« abstract ») et obtenir un contrôleur instanciable par du code .NET. Pour finir complètement la migration, il sera nécessaire d’implémenter la couche de présentation depuis un nouveau projet Windows Forms. Les UserControls développés pour l’application MFC seront alors pleinement réutilisés ce qui devrait faciliter et réduire le temps de réécriture des fenêtres Windows Forms.
Quid de WPF ? La logique présentée ici convient parfaitement à une migration Windows Forms vers WPF. A la place du « placeholder » Windows Forms pour MFC, nous utiliserons un contrôle « ElementHost » (type System.Windows.Forms.Integration.ElementHost) pour afficher un contrôle WPF dans une application Windows Forms. La migration étape par étape de Windows Forms vers WPF suivra les mêmes principes que ceux édictés dans cet article.
Références
· C++/CLI Language Specification: http://www.ecma-international.org/publications/standards/Ecma-372.htm
· Building a Managed Wrapper with C++/CLI: http://www.courseptr.com/downloads/bonus/009638_BonusCh02.pdf
· Intégrer des contrôles utilisateurs Windows Forms avec MFC: http://www.dotnetguru.org/articles/dossiers/mfcwinforms/article.htm
· Pattern MVC sur Wikipedia: http://en.wikipedia.org/wiki/Model-view-controller
· Pattern « Passive View »: http://www.martinfowler.com/eaaDev/PassiveScreen.html
Remerciements
J’aimerai remercier Simon Ferquel (http://simonferquel.net/blog/) pour l’implémentation d’un prototype technique montrant l’interopérabilité .NET avec MFC au travers d’un wrapper C++/CLI.
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :