Utilisation de contrôles .NET (UserControl) comme ActiveX

Cet article fait suite à un atelier d'étude migration d'applications en VB6 vers .NET. La problématique apparu pendant cet atelier concernait l'utilisation de contrôles dévelopés en .NET dans l'application existante VB6 sous la forme d'ActiveX. En effet, le scénario retenu pour la migration de cette application dépassant le million de lignes de code était un portage du code progressif et donc une coexistence de binaires compilés en C# avec des binaires VB6. L'approche est de conserver l'application existante et de migrer au fur-et-à-mesure des codes VB6 vers .NET et de les utiliser dans l'application maître VB6 par le mécanisme d'interopérabilité.

Théoriquement cette solution présente plusieurs avantages et permet de répondre à la contrainte de la migration d'1 million de lignes de code sans effet "tunnel". Techniquement, lorsqu'il a fallu démontrer la faisabilité au travers d'un exemple, la solution est devenue un peu plus complexe: pas ou peu d'articles sur le sujet, pas tous complets et avec des solutions plus ou moins concrètes. Après quelques implémentations, nous avons pu avoir un premier résultat concluant.

Cet article a pour ojbectif de décrire précisément le code source .NET et VB6 permettant d'utiliser un UserControl .NET développé en C# dans une application VB6 ainsi que le processus de développement d'un tel composant.

Création d'un UserControl .NET interopérable

1. Création d'un projet dans Visual Studio 2005 de type Windows Control Library/Librairie de Contrôles Windows.

2. Dans les propriétés du projet, activer l'enregistrement pour l'interopérabilité COM (onglet Build/Register for COM Interop).

3. Ajouter un contrôle au projet (exemple: NetControlToActiveX).

4. Dans le code source du contrôle, implémenter une interface comme le montre l'exemple ci-dessous:

[ComVisible(true)]
[Guid("28F2EDD5-9421-4dc2-AEB2-A61AB7E039BC")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface INetControlToActiveX
{
string MyText { get; set; }
}

L'attribut ComVisible permet de rendre cette interface visible du côté COM. Ensuite, il est nécessaire d'attribuer un GUID à l'interface. Vous pouvez pour cela utiliser l'outil guidgen.exe (C:\Program Files\Microsoft Visual Studio 8\common7\tools\guidgen.exe). Précisez que l'interface est de type Dual, ce qui permet d'être instanciée en Late-Binding ou Early-Binding.

5. Modifier la déclaration du contrôle de la manière suivante (utiliser les noms appropriés en fonction de votre code):

[ComVisible(true)]
[ProgId(NetControlToActiveX.ProgId)]
[Guid(NetControlToActiveX.ClsId)]
[ClassInterface(ClassInterfaceType.None)]
public partial class NetControlToActiveX : UserControl, INetControlToActiveX
{
public const string ClsId = "AB83A8D6-F05C-4d07-8582-EA078E09C94B"
public const string ProgId = "InteropWCLCSharp.NetControlToActiveX"

...

6. Implémenter les méthodes déclarées dans l'interface (ici pour l'exemple INetControlToActiveX):

#region INetControlToActiveX Members
public string MyText
{
get
{
return label1.Text;
}
set
{
label1.Text = value;
}
}
#endregion

NOTE: Notre UserControl d'exemple contient un contrôle Label et un bouton. Un clic sur le bouton affiche "Hello World!", mais le texte du Label est modifiable par la propriété "MyText". Cet exemple a pour but de vous montrer comment utiliser le UserControl .NET depuis VB6.

Simple .NET UserControl

7. Implémenter les fonctions d'enregistrement/desenregistrement COM (vous pouvez copier/coller le code tel quel) dans votre UserControl:

#region COM Registration Functions
[ComRegisterFunction]
public static void RegisterClass(Type t)
{
string keyName = @"CLSID\" + t.GUID.ToString("B");
using (RegistryKey key =
Registry.ClassesRoot.OpenSubKey(keyName, true))
{
key.CreateSubKey("Control").Close();
using (RegistryKey subkey = key.CreateSubKey("MiscStatus"))
{
subkey.SetValue("", "131457");
}
using (RegistryKey subkey = key.CreateSubKey("TypeLib"))
{
Guid libid = Marshal.GetTypeLibGuidForAssembly(t.Assembly);
subkey.SetValue("", libid.ToString("B"));
}
using (RegistryKey subkey = key.CreateSubKey("Version"))
{
Version ver = t.Assembly.GetName().Version;
string version =
string.Format("{0}.{1}",
ver.Major,
ver.Minor);
if (version == "0.0") version = "1.0"
subkey.SetValue("", version);
}
}

}

[ComUnregisterFunction]
public static void UnregisterClass(Type t)
{
// Delete entire CLSID\{clsid} subtree
string keyName = @"CLSID\" + t.GUID.ToString("B");
Registry.ClassesRoot.DeleteSubKeyTree(keyName);

}

8. La partie concernant .NET est terminée. Modifiez le code de votre UserControl en fonction de vos besoins.

Utilisation du UserControl .NET dans une application VB6

Pour l'exemple, j'ai recréé une application vide en VB6 mais la même démarche s'applique à une application existante. L'application se compose d'une Form et d'un contrôle Frame. Nous allons instancier le contrôle .NET vu comme un ActiveX depuis VB6 et l'afficher dans le Frame, puis modifier son affichage avec un nouveau texte.

Le référencement de l'assembly .NET (Projet/Références) dans VB6 est optionnel. Dans notre exemple, nous référençons l'assembly afin d'utiliser des types forts plutôt que le type générique "Object" de VB6 et de bénéficier de l'intellisense.

1. Sur la Form en mode Design, ajoutez un contrôle Bouton et un contrôle Frame. Laisser les noms par défaut pour l'exemple.

2. Dans le code de la Form, modifiez le code de la manière suivante:

Option Explicit

Private m_NetControlToActiveX As InteropWCLCSharp.NetControlToActiveX

Private Sub Form_Load()
Dim ctrlInstance As Object
' Chargement dynamique d'ActiveX dans VB6
Set ctrlInstance = Me.Controls.Add("InteropWCLCSharp.NetControlToActiveX", "MyInteropControl")
ctrlInstance.Top = 200
ctrlInstance.Left = 100
ctrlInstance.Visible = True
' Positionnement du contrôle Container
Set ctrlInstance.Container = Frame1
' Liaison au UserControl.NET
Set m_NetControlToActiveX = Me.Controls("MyInteropControl").object
End Sub

La création dynamique de contrôle ActiveX en VB6 se fait en utilisant Me.Controls.Add(ProgId, InstanceName) (ligne de code: Set ctrlInstance = Me.Controls.Add("InteropWCLCSharp.NetControlToActiveX", "MyInteropControl")). Me est pour la Form courante. Il est important de positionner la propriété Visible à True de cette nouvelle instance (ligne de code: ctrlInstance.Visible = True).

Le positionnement dans le Frame est obtenu par le référencement de Frame1 par la propriété Container (ligne de code: Set ctrlInstance.Container = Frame1).

L'utilisation du type (ici InteropWCLCSharp.NetControlToActiveX) n'est possible que sur la propriété object d'un contrôle issu de la collection de contrôles de la Form (ligne de code: Set m_NetControlToActiveX = Me.Controls("MyInteropControl").object).

3. Implémenter le Click du bouton pour modifier la propriété MyText:

Private Sub Command1_Click()
m_NetControlToActiveX.MyText = "Hello from VB6!"
End Sub

Intellisense in VB6 

4. L'exemple d'interopérabilité est terminé. Nous en sommes au point où nous pouvons utiliser un UserControl .NET depuis une application VB6, le positionner dans un container et modifier des valeurs. Un point non abordé dans cet article est l'interopérabilité des événements exposés en VB6 depuis le contrôle. Les références en fin de l'article donnent des réponses sur l'utilisation de l'attribut ComSourceInterfaces. Une recherche sur cet attribut dans un moteur de recherche sur Internet devrait vous donner toutes les solutions.

Ci-dessous les copies d'écran de l'application finale:

L'appui sur le bouton "Say Hello!" affiche le message implémenté dans le UserControl.

Hello from .NET 

L'appui sur le bouton "Mon Bouton" affiche le message implémenté dans l'application VB6.

Hello from VB6 

Références

- https://www.thescripts.com/forum/thread398225.html

- https://blogs.msdn.com/johnrdurant/archive/2005/08/10/net-to-activex.aspx

- https://www.c-sharpcorner.com/UploadFile/dsandor/ActiveXInNet11102005040748AM/ActiveXInNet.aspx

VSControl.jpg