Extensibilité en .NET 3.5

Un système de plugins en VB.NET 2008

Introduction

Dans ce nouvel article, nous allons réaliser un mini-éditeur de texte, extensible à l'aide d'un système de plugins, un peu à la manière des addins de Visual Studio ou des filtres de Photoshop. Nous travaillerons avec la version 3.5 du Framework .NET.



Notre fenêtre d'exemple comporte un simple champ texte multi-lignes. Dans la barre de menus, une entrée "Plugins" permet de lister les plugins installés. Le premier plugin, "Insert Date Time", permet tout simplement d'insérer la date courante à la position du curseur. Le second, "Insert Machine Name", insère le nom de la station de travail.

Ajouter un nouveau plugin se limitera pour l'utilisateur final à ajouter une nouvelle DLL (assembly) dans le répertoire contentant l'exécutable de l'application. Aucun fichier de configuration supplémentaire ne devra être modifié.



L'application principale ne contient aucune référence vers les plugins installés. Ceux-ci sont chargés dynamiquement au démarrage de l'application.

L'interface IPluginFunctionality

Nous allons commencer par créer une nouvelle librairie de classes CommonPluginTypes.dll qui contiendra une interface que tous les nouveaux plugins devront implémenter afin d'être compatible avec notre application. Dans notre cas, une simple fonction Insert() retournant une variable de type String suffira.

Nous allons également créer un nouvel attribut personnalisé qui permettra aux développeurs de plugins de spécifier directement dans leur code le nom complet du plugin tel qu'il apparaîtra dans le menu déroulant. Nous aurions également pû y ajouter un texte d'aide, l'auteur du plugin, sa société, etc.

Note :
Dans le code ci-dessous, remplacez les caractères [] par <>. Réduisez la police de votre navigateur si vous ne voyez pas le code en entier.


Public Interface IPluginFunctionality
Function Insert() As String
End Interface

[AttributeUsage(AttributeTargets.Class)] _
Public NotInheritable Class PluginInfoAttribute
Inherits System.Attribute

Private pluginName As String

Public Sub New()
End Sub

Public Property Name() As String
Get
Return pluginName
End Get
Set(ByVal value As String)
pluginName = value
End Set
End Property
End Class


Création d'un nouveau plugin

Nous allons maintenant développer un nouveau plugin. Pour celà, il nous suffit de créer une nouvelle librairie de classes baptisée par exemple DateTimePlugin.dll. Celle-ci contiendra une seule classe, DateTimePluginModule, qui implémentera la méthode Insert() comme suit :

Imports CommonPluginTypes

[PluginInfo(Name:="Insert Date Time")] _
Public Class DateTimePluginModule
Implements IPluginFunctionality

Public Function Insert() As String _
Implements CommonPluginTypes.IPluginFunctionality.Insert
Return DateTime.Now.ToString("dd-MM-yyyy hh:mm:ss">
End Function
End Class


N'oubliez-pas d'ajouter une référence à CommonPluginTypes.dll, sinon l'interface IPluginFunctionality ne sera pas connue. L'attribut PluginInfo que nous avons défini précédemment nous permet de préciser le nom complet du plugin.

Construction de l'éditeur de texte

Nous allons maintenant réaliser l'application de test. Créez un nouveau projet de type "Windows Forms" et renommez le formulaire par défaut MainForm. Ouvrez le formulaire en mode design et ajoutez les contrôles suivants :

  • txtEditor : un contrôle de type TextBox avec la propriété Multiline = True
  • mnuMain : un contrôle de type ToolStripMenu
  • mnuPlugins : ToolStripItem dans mnuMain qui servira à lister les plugins chargés
  • lblToolStripStatus : un contrôle de type Label placé dans la barre de statut

A nouveau, ajoutez une référence vers CommonPluginTypes.dll. Nous allons commencer par définir une liste générique, loadedPlugins, qui nous permettra de maintenir la liste des plugins chargés au démarre de l'application.


Imports System.IO
Imports System.Reflection
Imports CommonPluginTypes

Public Class MainForm
Private loadedPlugins As New List(Of IPluginFunctionality)

Private Sub QuitToolStripMenuItem_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles QuitToolStripMenuItem.Click
Application.Exit()
End Sub

Private Sub MainForm_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles MyBase.Load
loadExternalModules()

lblToolStripStatus.Text = String.Format("{0} plugin(s) loaded", _
loadedPlugins.Count)
End Sub

...

End Class


La méthode loadExternalModules() sera chargée de lister les assemblies présentes dans le répertoire de l'application et de vérifier si celles-ci contiennent des classes qui implémentent l'interface IPluginFunctionality. Pour chaque DLL trouvée, nous allons appeler la méthode loadExternalModule().


Private Sub loadExternalModules()
Dim pluginPath As String = Application.ExecutablePath

pluginPath = pluginPath.Substring(0, pluginPath.LastIndexOf("\"))

If Not Directory.Exists(pluginPath) Then
MessageBox.Show(String.Format("{0} not found", pluginPath), _
"Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
Return
End If

For Each assemblyName As String In Directory.GetFiles(pluginPath)
If assemblyName.ToLower().EndsWith(".dll") Then
loadExternalModule(assemblyName)
End If
Next
End Sub

Cette nouvelle méthode va essayer de charger en mémoire l'assembly dont le chemin est passé en paramètre. Si cette opération échoue, c'est que la DLL en question n'est probalement pas une assembly .NET.

Pour découvrir les classes contenues dans l'assembly et vérifier si celles-ci implémentent IPluginFunctionality, nous aurions pu utiliser les différentes classes et méthodes de System.Reflection. A la place, comme nous utilisons le Framework 3.5, nous allons exécuter une requête LINQ qui va nous renvoyer une collection d'objets implicitement typés.

Private Sub loadExternalModule(ByVal pluginPath As String)
Dim pluginAssembly As Assembly = Nothing
Dim pluginInterface As IPluginFunctionality

Try
pluginAssembly = Assembly.LoadFrom(pluginPath)
Catch ex As Exception
MessageBox.Show(String.Format("{0} cannot be loaded", pluginPath), _
"Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
Return
End Try

Dim classTypes = From t In pluginAssembly.GetTypes() _
Where t.IsClass _
And (t.GetInterface("IPluginFunctionality") IsNot Nothing) _
Select t

For Each c As Object In classTypes
Dim o As Object = pluginAssembly.CreateInstance(c.FullName)
pluginInterface = CType(o, IPluginFunctionality)
addMenuItem(pluginInterface)
Next
End Sub

Pour chacune des classes compatibles, nous allons créer une nouvelle instance de type IPluginInterface, que nous allons ajouter à notre collection générique ainsi qu'au menu déroulant "Plugins", à l'aide de la méthode qui suit :

Private Sub addMenuItem(ByVal pluginIterface As IPluginFunctionality)
loadedPlugins.Add(pluginIterface)

Dim mnuItem As ToolStripItem = mnuPlugins.DropDownItems.Add("")
With mnuItem
.Text = getPluginName(pluginIterface)
.Tag = mnuPlugins.DropDownItems.Count - 1
AddHandler .Click, AddressOf pluginItem_Click
End With
End Sub

Chaque nouvelle entrée dans le menu se voit ajouter un event handler commun. J'ai choisi d'ajouter l'index du plugin correspondant dans la propriété Tag du menu. Comme l'objet instancié implémente IPLuginFunctionality, nous pouvons sans problème appeler la fonction Insert() correspondante (polymorphisme) :

Private Sub pluginItem_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs)
Dim mnuItem As ToolStripItem = CType(sender, ToolStripItem)
Dim pluginInterface As IPluginFunctionality = loadedPlugins.Item(CInt(mnuItem.Tag))

txtEditor.SelectedText = pluginInterface.Insert()
End Sub

Reste à extraire la valeur de l'attribut personnalisé PluginInfo.Name, à l'aide de la fonction getPluginName(), pour l'utiliser dans notre menu déroulant.

Private Function getPluginName(ByVal pluginInterface _
As IPluginFunctionality) As String
Dim type As Type = pluginInterface.GetType()
Dim customAtts As Object() = type.GetCustomAttributes(False)
Dim pluginName As String = "No Name"

For Each c As PluginInfoAttribute In customAtts
pluginName = c.Name
Next

Return pluginName
End Function

Résumé

La réflectivité est un des aspects les plus puissants de la programmation orientée-objet. La technique du "late binding" permet d'instancier un type d'objet et d'invoquer ses membres sans connaissance prélable de ces derniers. Cet article vous a permis de découvrir une technique d'extension simple telle qu'on peut la rencontrer dans les environnements de développement (Eclipse, Visual Studio), les CMS (Joomla, DotNetNuke) ou encore les applications bureautiques (Office, Photoshop). Cet article vous a également donné un exemple concret d'utilisation des attributs personnalisés en .NET.

Commentaires