Dein VB NET Plugin-System im Eigenbau – DAS ultimative Beispiel!
Inhaltsverzeichnis
- 1 Erfahre wie Du Dein eigenes VB NET Plugin-System bauen kannst
- 2 Als YouTube-Video
- 3 Hintergrundgeschichte von Plugins – Plug & Play
- 4 VB NET Plugin-System – Marke Eigenbau „first“
- 5 Die Projektmappe für unser VB NET Plugin-System vorbereiten
- 6 Beispiel-Anwendung für das VB NET Plugin-System
- 7 Ein erweiterbares Dashboard – Anfang der Anwendung
- 8 Ein Plugin-Manager als Infrastruktur für das VB NET Plugin-System
- 9 Das Plugin selbst – Die VB NET Plugin-System Basis
- 10 Ein UsageWidget-Plugin für unsere Anwendung
- 11 Der EventAggregator – losgelöste Kommunikation
- 12 Der Einstiegspunkt für unser Programm
- 13 Das Hauptformular
- 14 Fazit – VB NET Plugin-System
- 15 Downloads
- 16 Weiterführende Links
Erfahre wie Du Dein eigenes VB NET Plugin-System bauen kannst
Im heutigen Beitrag realisieren wir ein sehr tolles VB NET Plugin-System – yay!
Plugins sind nicht nur essentieller Bestandteil der Programmierung, sondern auch eine tolle Übung.
Es zeigt wie Bestandteile unterschiedlicher Systeme ineinandergreifen können und dadurch erweiterbar sind.
Sie können gezielt gewisse Bereiche des Programms nach außen hin verfügbar machen.
So hat jeder Plugin-Entwickler die Chance, seinen Senf zum Ganzen beitragen zu können, ohne der eigentliche Hersteller zu sein.
Ich selbst (und vermutlich viele Andere) kamen in Vergangenheit öfter mit der Verwendung von Plugins in Berührung.
Einerseits kennt man Sie natürlich schon als essentiellen Bestandteil aus CMS wie WordPress.
Dort sind Plugins das täglich Brot und viele CMS-Nutzer wären ohne Sie tatsächlich aufgeschmissen.
Andererseits haben selbstverständlich auch viele clientseitige Programme ähnliche Funktionalitäten implementiert.
Lass‘ uns in diesem Beitrag ein solches VB NET Plugin-System gemeinsam umsetzen!
Als YouTube-Video
Schaue Dir meinen Beitrag gern in visueller Form auf YouTube an:
Hintergrundgeschichte von Plugins – Plug & Play
Man hat zu diesem Zeitpunkt ein vermutlich schon bestehendes Programm und möchte einen Schritt weiter gehen.
Die bisherige Funktionalität ist eventuell schon ausgereift und das Programm erhält gute Resonanz.
Download-Zahlen und Nutzer-Statistiken stimmen und immer mehr Vorschläge prasseln auf Dich als Entwickler ein.
Ich denke, dass jeder Entwickler irgendwann mal an diesen Punkt kommt:
„Jetzt wäre es cool, ein eigenes Plugin-System zu haben, aber wie!?“.
Mit Visual Basic NET gibt es dafür zum Glück einige von Haus aus zur Verfügung gestellte Tools.
Diese ermöglichen uns die kinderleichte Umsetzung eigener Plugins.
Somit kann unsere Anwendung schön durch die Kreativität anderer Softwareentwickler erweitert werden.
Neben dieser gerade erwähnten Standard-Tools vom NET Framework, gibt es natürlich weitere Alternativen.
Die Einen sind vermutlich einfacher, weniger komplex, aber auch eventuell für den Lernprozess hilfreicher.
Ganz nach dem Motto „back to the roots“ werden wir uns zuerst mit der Eigenbau-Variante beschäftigen.
Ich denke, es hilft vor allem, die Hintergrund-Prozesse eines solchen Plugin-Systems zu verstehen.
VB NET Plugin-System – Marke Eigenbau „first“
Um die eventuellen ersten Schritte im Bereich der Plugin-Entwicklung so rudimentär wie möglich zu halten, starten wir bei 0.
Ich denke, dass wir zuerst einen Blick in die allgemeine Struktur einer Anwendung werfen müssen.
Damit können wir dann genauer verstehen, wie man dann eventuelle weitere Bestandteile laden kann.
Diese Bestandteile müssen sich anhand gewisser Regeln in das Gesamtbild der Anwendung einfügen.
Natürlich kann ich hier nicht zu tief ins Detail gehen, da wir sonst den bekannten Rahmen sprengen würden..
NET Assembly Exkurs
Grundsätzlich kann man folgende „Quick-Facts“ festhalten:
- .NET Anwendungen setzen sich aus Bausteinen zusammen
- Dies gilt natürlich auch für dynamische Programmbibliotheken – DLLs
- Die genannten Bausteine bezeichnet man als Assemblies (Singular Assembly)
- „To assemble“ bedeutet aus dem Englischen nichts anderes als „zusammensetzen„
Ein NET Programm besteht also aus einer oder mehreren Assemblies, Welche somit wiederum als Bausteine betitelt werden können.
Diese Bausteine sind mehr oder weniger „Brocken„, versehen mit Informationen für die CLR (Common Language Runtime).
Die Brocken bestehen aus für die CLR vorkompiliertem Code und darin enthaltenen Metadaten.
Mit diesen Metadaten weiß die CLR, wie Sie mit der Anwendung und deren Bestandteilen verfahren kann/muss.
Wie gesagt könnte man hier noch tiefer tauchen, aber ich denke das reicht für einen kleinen Exkurs.
Eine „externe“ Assembly laden
Wir können aus diesem Exkurs also folgende Schlussfolgerung ziehen:
Um unser Programm dynamisch erweitern zu können, müssen wir zusätzliche Assemblies laden.
Mit diesen zusätzlichen Assemblies findet dann weiterer, strukturierter Code seinen Weg in unser Programm.
Auch hierzu bietet uns das NET Framework natürlich von Haus aus komfortable Möglichkeiten.
Eine der dazu verwendeten Methoden befindet sich im „System.Reflection„-Namespace.
Genauer genommen, spreche ich von der statischen „Assembly.LoadFrom„-Funktion.
Die Funktion wird durch die „System.Runtime„-Programmbibliothek bereitgestellt – also einer Assembly.
Laden wir nun mit Hilfe der „LoadFrom“-Funktion einmal eine Assembly.
Der Einfachheit halber habe ich ich die DLL einfach auf den Desktop gelegt.
Dim pathToDll = "C:\Users\Anwender\Desktop\MyPlugin.dll" Dim assembly = Assembly.LoadFrom(pathToDll)
Die Projektmappe für unser VB NET Plugin-System vorbereiten
Für unser Beispiel werden wir 2-3 Projekte benötigen:
- Das eine Projekt wird das Skelett, bzw. der Bauplan für unser Plugin sein. Ebenso wird dort „die Anwendung“ sein.
- Im zweiten Projekt werden wir den Plugin-Bauplan umsetzen, also eine Plugin-Klasse bauen, Welche den Bauplan implementiert.
Die Projekte werde ich bewusst in der gleichen Projektmappe unterbringen, damit es einfacher ist.
Man könnte natürlich auch alternativ verschiedene Projektmappen verwenden.
Neue Projektmappe für das VB NET Plugin-System erstellen
Nun beginnen wir mit der Erstellung der Projektmappe, wo dann später die anderen Projekte folgen werden.
Öffne dazu wie gewohnt Visual Studio und wähle „Neues Projekt erstellen„:
Danach kannst Du in dem Suchfeld oben nach dem Projekt-Typ „projektmappe“ suchen.
Wähle dann einfach den passenden Eintrag aus der rechten Auflistung.
Wir wählen hier bewusst die leere Projektmappe aus, da die anderen Projekte gleich noch folgen:
Im nächsten Fenster kannst Du Dich dann für einen sinnvollen Namen der Projektmappe entscheiden.
Damit das nicht zu kompliziert wird (also was Namespacing, Hersteller, usw. betrifft), machen wir’s hier ganz einfach.
Verwende daher eventuell einfach den Namen „PluginBeispiel„.
Klicke final auf den „Erstellen„-Knopf und wir sind bereit loszulegen.
Beispiel-Anwendung für das VB NET Plugin-System
Nun sind wir an dem Punkt angelangt, wo wir eine Anwendung benötigen.
Dieses Anwendung soll dann durch unsere Plugin-Infrastruktur erweitert werden können.
Damit das funktioniert, können andere Entwickler dann unsere Plugin-Baupläne implementieren und ein Plugin umsetzen.
Unsere Anwendung muss dann nur noch dafür Sorgen, dass die jeweiligen Plugins geladen werden.
Dabei spielen natürlich Abhängigkeiten, Zeitpunkte und zugreifbare Kontexte eine Rolle.
Möchte ich Plugins z. B. nur beim Start Gelegenheit geben in das Programm-Geschehen einzugreifen?
Oder sollen Plugins auch mitten in der Ausführung zum Beispiel auf die Datenbank zugreifen können.
Wie Du siehst, verlangt so ein Plugin-System einiges an Planung ab, da wir uns diese Fragen zuerst beantworten müssen.
Der Projektmappe ein Windows Forms Projekt hinzufügen
Erstelle also nun für den Anfang eine neue Windows Forms App, Welche Du als neues Projekt innerhalb der Projektmappe anlegst.
Das kannst Du oben rechts, durch einen Rechtsklick auf die Projektmappe im Projektmappen-Explorer erledigen.
Ich werde das Projekt für unser Beispiel als „MyCRM“ betiteln.
Nachdem Du diesen Schritt erledigt hast, springen wir auch direkt zum nächsten Punkt.
Ich habe mir für dieses Tutorial etwas Besonderes überlegt, da ich die üblichen Beispiele aus dem Netz zu langweilig finde.
Wir werden also nicht nur ein VB NET Plugin-System an sich schreiben, sondern auch ein wenig Konfiguration einführen.
Aus der Anwendung heraus soll es möglich sein, gewisse Plugins aktivieren und deaktivieren zu können.
Ich denke, dass diese Variante durchaus eher an der Praxis orientiert ist, als die üblichen Beispiele.
Wäre ja auch langweilig, wenn ich wie jeder das: „Hey ich geb‘ zig mal einen String aus“-Beispiel bringe, oder!?
Ein erweiterbares Dashboard – Anfang der Anwendung
Natürlich versuche ich auch hier (wie immer) den Mittelweg zwischen „anschauliches Beispiel“ und „einfach zu verstehen“ zu wählen.
Das gelingt mir glaube ich meistens ganz gut, daher werde ich auch hier einfach mal loslegen.
Im ersten Schritt bezüglich unserer Anwendung, werden wir die Haupt–Funktionalität umsetzen.
Dabei denke ich in erster Linie natürlich (wie oben im Bild zu sehen) an Tabs wie „Kunden“, „Rechnungen“, etc.
Zusätzlich kam mir ein kleines Dashboard in den Sinn, was man ja auch aus den verschiedensten Anwendungen kennen sollte.
Dort können z. B. verschiedene Informationen durch die verfügbaren Widgets dargestellt werden.
Je nach Kreativität des jeweiligen Entwicklers können weitere Elemente dynamisch hinzugefügt werden.
An dieser Stelle ist der Vorstellung kaum eine Grenze gesetzt, außer der Grenzen, die wir selbst bestimmen.
Wir selbst definieren die Bereiche der Anwendung, worauf die Plugins letztendlich Zugriff haben.
Erste Einfälle für Plugins, Welche auch als Dashboard–Widgets fungieren könnten, sind Folgende:
- Das Wetter (relevant für Tourenplanung, etc.)
- Die letzten 5 Rechnungen
- Informationen über den letzten Anruf
- und beliebig viel mehr..
Lass uns allerdings nun im nächsten Abschnitt erstmal eine kleine Basis an Plugin-Infrastruktur bauen.
Ein Plugin-Manager als Infrastruktur für das VB NET Plugin-System
Nun gehen wir in unserem VB NET Plugin-System den nächsten Schritt, indem wir eine Infrastruktur bauen.
Wie ich auch im Code kommentiert habe/kommentieren werde, hasse ich eigentlich diese typischen „Manager„-Klassen.
Diese verstoßen meistens gegen das „Single Responsibility“-Prinzip und sind daher alles Andere als sauber.
In diesem Beispiel dürfte es aber kontextual dennoch passen und es soll ja auch nur als Basis dienen.
Als Basis für Deine eigenen, künftigen, kreativen Gedanken das System zu erweitern.
Beachte hier bitte erneut den Hinweis, dass ich nur die essentiellen Themen behandeln kann.
Sonst müsste ich zusätzlich noch Themen wie „Dependency Injection“ und und und abarbeiten.
Dies mache ich aber in getrennten Videos und Beiträgen, wo ich dann mit mehr Zeit darauf eingehen kann.
Das IPluginManager-Interface
Zuerst schauen wir uns an, was ein Plugin-Manager in unserem Kontext können muss.
Erneut, dies ist nur ein Beispiel, Welches ich in kurzer Zeit zusammengeschustert habe.
Daher wird es keine „non mega plus ultra„-Lösung sein, sondern ein möglicher Startpunkt.
Wir verwenden jedoch natürlich im ersten Schritt ein Interface, um ein grobes Funktions-Skelett zu definieren.
Imports System.IO Namespace Contracts Public Interface IPluginManager ReadOnly Property Directory As DirectoryInfo ReadOnly Property Plugins As List(Of IPlugin) ReadOnly Property ActivePlugins As List(Of IPlugin) ReadOnly Property InactivePlugins As List(Of IPlugin) Sub EnsurePluginFolder() Sub LoadActivePluginsList() Sub LoadPlugins() Sub ActivatePlugin(plugin As IPlugin) Sub DeactivatePlugin(plugin As IPlugin) Function IsPluginActive(plugin As IPlugin) As Boolean Function IsPluginActive(pluginId As String) As Boolean End Interface End Namespace
Eigenschaften
Directory
Die Directory-Eigenschaft beinhaltet eine Instanz der Directory-Klasse. Damit können wir ganz einfach die Assembly-Dateien finden.
Plugins
Hier speichern wir eine Auflistung aller Plugins, Welche wir beim Start der Anwendung sammeln.
ActivePlugins
Wie der Name suggeriert, eine Auflistung, worin sich alle aktiven Plugins befinden.
InactivePlugins
Das Gegenstück zur vorherigen Eigenschaft, hier befinden sich alle inaktiven Plugins.
Methoden
EnsurePluginFolder
Eine Hilfs-Methode, um sicherzustellen, dass der Plugin-Ordner existiert. Könnte man eventuell auch drauf verzichten..
LoadActivePluginsList
Diese Methode lädt die durch Konfiguration aktivierten Plugins in eine vorgesehene ID-Liste.
LoadPlugins
Die Methode lädt die vorhandenen Plugin-Typen und instanziiert Diese. Anschließend könnten wir nur gewissen Instanzen dann eventuelle Abhängigkeiten geben. Mit Sicherheit gibt es hier noch bessere Varianten, ich beschreibe aber später, Welche ich verwendet habe. Nach Bestückung der Abhängigkeiten, packen wir die Plugins in jeweilige Listen oben (allgemein, aktiv, inaktiv).
ActivatePlugin
Wie der Name dieser Methode schon vermuten lässt, aktiviert Sie ein jeweiliges Plugin. Dazu zählt einerseits die Verwaltung durch den Manager selbst und andererseits die Aktivierung des Plugins selbst. Mehr dazu weiter unten!
DeactivatePlugin
Das Gegenstück zur vorherigen Methode, schaue am besten dort :)!
IsPluginActive
Diese Hilfs–Funktion unterstützt uns beim Check, ob ein jeweiliges Plugin aktiviert ist. Dazu gibt es zwei Überladungen, einmal mit dem Plugin und einmal mit der Plugin-ID als Parameter.
PluginManager-Implementierung
Schauen wir uns im nächsten Schritt den Code zur Implementierung des obigen Interfaces an.
Imports System.IO Imports System.Reflection Imports Autofac Imports MyCRM.Contracts ' usually i hate these typical "manager" classes ' but in this case it's usage context is limited so.. ' could also be composed of sub-components like loader, settings, whatever.. Public Class PluginManager Implements IPluginManager ' should be configurable whatever.. Public ACTIVE_PLUGINS_CONFIG_FILE As String = Path.Combine(Application.StartupPath, "plugins_active.txt") Public ReadOnly Property Directory As DirectoryInfo Implements IPluginManager.Directory Public ReadOnly Property Plugins As List(Of IPlugin) Implements IPluginManager.Plugins Public ReadOnly Property ActivePlugins As List(Of IPlugin) Implements IPluginManager.ActivePlugins Public ReadOnly Property InactivePlugins As List(Of IPlugin) Implements IPluginManager.InactivePlugins Protected PluginTypes As List(Of Type) Protected ReadOnly ActivePluginIds As List(Of String) Sub New(directory As String) Me.ActivePluginIds = New List(Of String) Me.Directory = New DirectoryInfo(directory) Me.Plugins = New List(Of IPlugin) Me.ActivePlugins = New List(Of IPlugin) Me.InactivePlugins = New List(Of IPlugin) Me.PluginTypes = New List(Of Type) End Sub Public Sub EnsurePluginFolder() Implements IPluginManager.EnsurePluginFolder If Not Directory.Exists Then Directory.Create() End If End Sub Public Sub LoadPluginTypes() Dim libraryFiles = Directory.GetFiles("*.dll", SearchOption.AllDirectories) Dim assemblies = libraryFiles.Select(Function(libraryFile) Assembly.LoadFrom(libraryFile.FullName)) Dim assemblyTypes = assemblies.SelectMany(Function(assemblyType) assemblyType.GetTypes()) ' or use IsAssignableFrom.. Dim pluginTypes = assemblyTypes.Where(Function(x) x.GetInterface(NameOf(IPlugin)) <> Nothing) Me.PluginTypes.AddRange(pluginTypes) End Sub Public Sub LoadActivePluginsList() Implements IPluginManager.LoadActivePluginsList If Not File.Exists(ACTIVE_PLUGINS_CONFIG_FILE) Then File.Create(ACTIVE_PLUGINS_CONFIG_FILE) Else Dim activePluginIds = File.ReadAllLines(ACTIVE_PLUGINS_CONFIG_FILE) Me.ActivePluginIds.AddRange(activePluginIds) End If End Sub ' maybe use some kind of caching mechanism.. Public Sub LoadPlugins() Implements IPluginManager.LoadPlugins Dim pluginInstances = PluginTypes.Select(Function(x) CType(Activator.CreateInstance(x), IPlugin)).ToList() If pluginInstances.Count = 0 Then Return End If Dim eventAggregator = Program.Container.Resolve(Of IEventAggregator) Plugins.AddRange(pluginInstances) For Each plugin In Plugins AttachEventAggregatorIfAware(plugin, eventAggregator) If ActivePluginIds.Contains(plugin.GetId()) Then ActivePlugins.Add(plugin) plugin.Load() Else InactivePlugins.Add(plugin) End If Next End Sub ''' <remarks>could be done different, dont know.. not flexible but well..</remarks> Private Sub AttachEventAggregatorIfAware(plugin As IPlugin, eventAggregator As IEventAggregator) Dim pluginType = plugin.GetType() Dim awareInterface = pluginType.GetInterface(NameOf(IEventAggregatorAware)) Dim isAware = awareInterface IsNot Nothing If isAware Then DirectCast(plugin, IEventAggregatorAware).EventAggregator = eventAggregator End If End Sub Protected Async Function AddPluginToActiveFileListAsync(plugin As IPlugin) As Task Dim lines = (Await File.ReadAllLinesAsync(ACTIVE_PLUGINS_CONFIG_FILE)).ToList() If Not lines.Contains(plugin.GetId()) Then lines.Add(plugin.GetId()) Await File.WriteAllLinesAsync(ACTIVE_PLUGINS_CONFIG_FILE, lines) End If End Function Protected Async Function RemovePluginFromActiveFileListAsync(plugin As IPlugin) As Task Dim lines = (Await File.ReadAllLinesAsync(ACTIVE_PLUGINS_CONFIG_FILE)).ToList() If lines.Contains(plugin.GetId()) Then lines.Remove(plugin.GetId()) Await File.WriteAllLinesAsync(ACTIVE_PLUGINS_CONFIG_FILE, lines) End If End Function ' could be made as task.. Public Async Sub ActivatePlugin(plugin As IPlugin) Implements IPluginManager.ActivatePlugin ' maybe check if inactive before ?? plugin.Activate() Await AddPluginToActiveFileListAsync(plugin) InactivePlugins.Remove(plugin) ActivePlugins.Add(plugin) End Sub Public Async Sub DeactivatePlugin(plugin As IPlugin) Implements IPluginManager.DeactivatePlugin ' maybe check if active before ?? plugin.Deactivate() Await RemovePluginFromActiveFileListAsync(plugin) ActivePlugins.Remove(plugin) InactivePlugins.Add(plugin) End Sub Public Function IsPluginActive(plugin As IPlugin) As Boolean Implements IPluginManager.IsPluginActive Return IsPluginActive(plugin.GetId()) End Function Public Function IsPluginActive(pluginId As String) As Boolean Implements IPluginManager.IsPluginActive Return ActivePlugins.SingleOrDefault(Function(x) x.GetId() = pluginId) IsNot Nothing End Function End Class
Neben den im Interface definierten Dinge, gibt es noch weitere Sachen, die ich zur Implementierung hinzugenommen habe.
Wo die aktivierten Plugins „gemerkt“ werden
Das erste ins Auge fallende „Ding“ ist dabei die „Konstante“ namens „ACTIVE_PLUGINS_CONFIG_FILE“.
Zugegebenermaßen handelt es sich nicht um eine richtige Konstante, da Sie eben nicht die „Const“-Anweisung verwendet.
Das liegt daran, dass ich den Pfad so gesehen als „fix“ ansehe, Ihn aber mehr oder weniger dennoch dynamisch ziehe.
Ich verwende die „Path.Combine“-Funktion um den Pfad zur Konfiguration der aktiven Plugins zu setzen.
PluginTypes
In dieser Auflistung speichern wir die rohen Typen der Plugins, um Diese dann später zu nutzen. Ist praktisch nur als Zwischenspeichern vorgesehen.
ActivePluginIds
Hier befinden sich die Plugin-IDs der aktiven Plugins, auch als Zwischenspeicher gedacht. Könnte man sich ggf. auch sparen, aber für den Anfang passt es so.
AttachEventAggregatorIfAware
Diese Helfer-Funktion soll uns beim „Anfügen“ des „EventAggregators“ an jeweilige Plugins helfen. Dies passiert nur, wenn ein jeweiliges Plugin Gebrauch vom gleich noch folgenden „IEventAggregatorAware„-Interface macht. Somit kann das Plugin signalisieren, was es braucht. Auf die Schnelle ist mir nichts Anderes eingefallen, da wir in unserem Fall nicht den Konstruktor verwenden können. Das würde heißen, dass das jeweilige Plugin vom DI-Container „resolved“ werden müsste, wofür es registriert sein muss. Eben das geht leider nicht, da wir ja nicht wissen, welche Plugins unser Programm erwartet. Vielleicht fällt Dir, mir, oder wem auch immer noch was anderes ein :)! Wer das „EventAggregator-Pattern“ nicht kennt: Es ist eine Möglichkeit zur losgelösten Kommunikation zwischen verschiedenen Objekten.
AddPluginToActiveFileListAsync
Diese Funktion hilft bei der Verwaltung der aktiven Plugins, indem Sie die jeweilige Plugin-ID zur passenden Datei hinzufügt.
RemovePluginFromActiveFileListAsync
Die Funktion macht das Gegenteil der Vorherigen: Sie entfernt die jeweilige Plugin-ID wieder von der Auflistung der aktivierten Plugins.
Das Plugin selbst – Die VB NET Plugin-System Basis
Oben haben wir schonmal den Plugin-Manager besprochen, das bringt uns aber bei fehlenden Plugins wenig.
Daher besprechen wir nun das Skelett und die Implementierung eines jeweiligen Plugins selbst.
Schaue Dir also einmal folgende Plugin-Schnittstelle namens „IPlugin“ an:
Namespace Contracts Public Interface IPlugin ''' <summary>An id like 'vendor-plugin-some_number' which shouldn't change over time to uniquely identify the plugin</summary> Function GetId() As String ''' <summary>A valid Image filepath or empty string/null if not having an image</summary> Function GetIcon() As String ''' <summary>The displayable plugin name</summary> Function GetName() As String ''' <summary>The displayable plugin description</summary> Function GetDescription() As String ' could be Version Class Type depending on use case.. Function GetVersion() As String ''' <summary>Process internal handling of activation</summary> Sub Activate() ''' <summary>Process internal handling of deactivation</summary> Sub Deactivate() ''' <summary>If plugin is activated, this will get for example executed at app start</summary> Sub Load() Event Activated As EventHandler Event Deactivated As EventHandler End Interface End Namespace
Methoden
Du wirst sicherlich festgestellt haben, dass das Plugin-Interface keine Eigenschaften besitzt – aus gutem Grund.
Dazu folgt gleich eine Erklärung, wirf nun den Blick auf die Interface-Methoden:
GetId
Diese Funktion dient der eindeutigen Identifikation des Plugins. Am besten setzt Sie sich aus dem Hersteller-Namen & einer Art Plugin-Kennung zusammen. Ein Beispiel dafür, findest Du weiter unten in der Beispiel-Implementierung namens „UsageWidget„.
GetIcon
Mit dieser Funktion kann ein Pfad zu einem lokalen Bild wiedergegeben werden. Dieses wird dann zum Beispiel für die Plugin-Auflistung im Programm verwendet. Falls kein Bild benötigt wird, kann man hier einfach „Nothing„, oder einen leeren String zurückgeben.
GetName
Die Implementierung und spätere Überschreibung dieser Methode sollte den Namen des Plugins in lesbarer Form wiedergeben. Dieser Name wird ebenfalls in der Plugin-Auflistung zur Darstellung verwendet.
GetDescription
Hiermit kann man dem Plugin eine für den Nutzer lesbare Beschreibung bereitstellen. Eventuell könnte man hier auch eine HTML-Variante verwenden und die durch einen Browser darstellen. Damit hätte man dann sogar die Möglichkeit auf die Seite des Autors zu verweisen.
GetVersion
Hier geben wir die aktuell erstellte Version des Plugins wieder. Man könnte natürlich auch die spezielle Version-Klasse verwenden.
Die anderen Methoden sehen wir in der Implementierung der gleich folgenden, abstrakten Klasse.
Die PluginBase – Eine abstrakte IPlugin-Implementierung
Bevor wir mit der abstrakten Klasse fortfahren, erinnere Dich, dass wir Methoden, statt Eigenschaften verwenden.
Die Plugin-Daten sollen zur Laufzeit nicht verändert werden können, da nur der Entwickler Sie festlegt.
Beim Programmieren des Plugins werden die Methoden in der implementierenden Klasse überschrieben.
Doch schauen wir uns zuerst einmal die dafür notwendige, abstrakte Basis-Klasse an.
Dort deklarieren wir die Funktionen als „MustOverride„, damit wir darauf bestehen, dass erbende Klassen diese überschreiben müssen.
Zusätzlich haben wir die beiden Basis–Implementierungen der „Activate“- & „Deactivate“-Funktionen.
Diese rufen die Überschreibungs-pflichtigen „OnActivate“ & „OnDeactivate“ Methoden auf.
Danach lösen Sie das „Activated„- & „Deactivated„-Ereignis aus.
Die beiden Ereignisse habe ich erstmal nur so eingefügt, aktiv werden Sie in dem Projekt noch nicht verwendet.
Imports MyCRM.Contracts Public MustInherit Class PluginBase Implements IPlugin Sub New() End Sub MustOverride Function GetId() As String Implements IPlugin.GetId MustOverride Function GetIcon() As String Implements IPlugin.GetIcon MustOverride Function GetName() As String Implements IPlugin.GetName MustOverride Function GetDescription() As String Implements IPlugin.GetDescription MustOverride Function GetVersion() As String Implements IPlugin.GetVersion Public Sub Activate() Implements IPlugin.Activate OnActivate() RaiseEvent Activated(Me, EventArgs.Empty) End Sub MustOverride Sub OnActivate() Public Sub Deactivate() Implements IPlugin.Deactivate OnDeactivate() RaiseEvent Deactivated(Me, EventArgs.Empty) End Sub MustOverride Sub OnDeactivate() MustOverride Sub Load() Implements IPlugin.Load Public Event Activated As EventHandler Implements IPlugin.Activated Public Event Deactivated As EventHandler Implements IPlugin.Deactivated End Class
Ein UsageWidget-Plugin für unsere Anwendung
Nachdem wir uns nun die ganze Basis des VB NET Plugin-Systems angeschaut haben, gehen wir zu einem konkreten Beispiel.
Es handelt sich bei unserem konkreten Plugin um eine Art „Nutzungs-Tracking„.
Das Plugin benötigt eine Referenz zum oben erwähnten „EventAggregator„, Welcher durch den „PluginManager“ injiziert wird.
Die Implementierung
Hier siehst Du die beispielhafte Implementierung der abstrakten „PluginBase“-Klasse.
Die einzelnen Funktionen hatte ich ja bereits oben erklärt.
Daher werde ich nun im nächsten Schritt die Plugin-spezifischen Eigenheiten erklären.
Imports MyCRM ' to expand this class like in a real life situation: ' - develop things here.. ' - build this project ' - take the produced dll file into a separate folder like usage-widget and ' - put it inside the plugins folder of the MyCRM folder ' NOTE: I put this project inside the same folder as the other project for ease.. ' for sure it's possible and recommended in real life situations ' to start developing widgets in separate projects anyways, because as extension developer ' you dont have access to the original code anyways.. - which is the point of plugins.. Public Class UsageWidget Inherits PluginBase Implements IEventAggregatorAware Public Const VendorName As String = "RobbelRoot" Public Const PluginName As String = NameOf(UsageWidget) Public Property EventAggregator As IEventAggregator Implements IEventAggregatorAware.EventAggregator Sub New() End Sub Public Overrides Function GetId() As String Return $"{VendorName}-{PluginName}" End Function Public Overrides Function GetIcon() As String Return Nothing End Function Public Overrides Function GetName() As String Return PluginName End Function Public Overrides Function GetDescription() As String Return "A simple plugin for managing some kind of application usage data" End Function Public Overrides Function GetVersion() As String Return "1.0.0" End Function Public Overrides Sub OnActivate() Debug.WriteLine($"[{NameOf(UsageWidget)}] Yay I got activated!") EventAggregator.Subscribe("app.booted", AddressOf OnAppBooted) End Sub Public Overrides Sub OnDeactivate() Debug.WriteLine($"[{NameOf(UsageWidget)}] Ney :(, I got deactivated!") EventAggregator.Unsubscribe("app.booted", AddressOf OnAppBooted) End Sub Public Overrides Sub Load() EventAggregator.Subscribe("app.booted", AddressOf OnAppBooted) End Sub Private Sub OnAppBooted(parameter As Object) Debug.WriteLine($"[{PluginName}] {NameOf(OnAppBooted)}") End Sub End Class
OnActivate
Die „OnActivate„-Methode wird entsprechend aufgerufen, wenn das Plugin aktiviert wird. Das passiert für gewöhnlich durch den „PluginManager“ im Plugin-Konfigurationsbildschirm. In unserem Beispiel abonnieren wir via „EvenAggregator“ das „app.booted“-Ereignis. Dadurch führen wir die im Plugin definierte Methode namens „OnAppBooted“ aus.
OnDeactivate
Diese Methode ist das Gegenstück zur „OnActivate„-Methode, Welche beim Deaktivieren des Plugins aufgerufen wird. Hier deabonnieren wir das vorherig erwähnte Ereignis.
Load
Die Load-Methode wird beim Start der Anwendung aufgerufen, falls das Plugin aktiviert ist. Prinzipiell unterscheidet Sie sich nicht arg vom „OnActivate“, ist nur praktisch Logik mäßig notwendig. Ein Aktivieren des Plugins ist in unserem Kontext hier zumindest kein Laden beim Start..
Der EventAggregator – losgelöste Kommunikation
Wie oben bereits angesprochen, ist das „EventAggregator-Pattern“ eine Art Nachrichten-, bzw. Ereignis-System.
Dieses System kann man zur losgelösten Kommunikation zwischen verschiedenen Objekt nutzen.
Der Vorteile liegen unter Anderem darin, dass die Objekte keine direkte, oder streng typisierte Abhängigkeit voneinander haben.
Es gibt bereits Bibliotheken, Welche dies auch implementieren, allerdings habe ich hier einen schnellen Eigenbau produziert.
Ganz einfach, weil ich weitere gedownloadete Abhängigkeiten vermeiden wollte.
Natürlich ist der Eigenbau hier nicht so ausführlich wie andere Implementierungen.
Das betrifft zum Beispiel den Umgang mit unterschiedlichen Threads, etc. – er reicht allerdings hier aus.
Das IEventAggregator-Interface
Das „EventAggregator“-Interface und die Erklärung zu den Methoden findet Ihr hier drunter.
Namespace Contracts Public Interface IEventAggregator Sub Subscribe(eventName As String, action As Action(Of Object)) Sub Unsubscribe(eventName As String, action As Action(Of Object)) Sub Publish(eventName As String, Optional parameter As Object = Nothing) End Interface End Namespace
Methoden
Subscribe
Diese Methode dient zum Abonnieren von hiesigen Ereignissen. Dabei erwartet die Methode als ersten Parameter den jeweiligen Namen des Ereignisses. Anschließend haben wir die optionale Möglichkeit ein Objekt mit gewissen Daten mitzuliefern.
Unsubscribe
Mit der nächsten Methode machen wir das Abonnement wieder rückgängig.
Publish
Hiermit können wir ein jeweiliges Ereignis auslösen, damit alle Abonnenten mit Ihren Handlern benachrichtigt werden. Wie erwähnt kann ein optionaler Parameter für die Ereignisdaten mitgeliefert werden.
Die Implementierung des EventAggregators
Hier findest Du meine beispielhafte Implementierung des „IEventAggregator“-Interfaces.
Ich verwende ein „Dictionary“ um einem Ereignis die jeweiligen Handler zuzuweisen.
Je nach Aktion wird ein zugeordneter Handler erstellt/hinzugefügt, oder eben wieder entfernt.
Imports MyCRM.Contracts Public Class EventAggregator Implements IEventAggregator Protected eventNamesToActions As IDictionary(Of String, List(Of Action(Of Object))) Sub New() eventNamesToActions = New Dictionary(Of String, List(Of Action(Of Object))) End Sub Public Sub Subscribe(eventName As String, action As Action(Of Object)) Implements IEventAggregator.Subscribe If Not eventNamesToActions.ContainsKey(eventName) Then eventNamesToActions.Add(eventName, New List(Of Action(Of Object))) End If eventNamesToActions(eventName).Add(action) End Sub Public Sub Unsubscribe(eventName As String, action As Action(Of Object)) Implements IEventAggregator.Unsubscribe If eventNamesToActions.ContainsKey(eventName) Then Dim list = eventNamesToActions(eventName) list.Clear() eventNamesToActions.Remove(eventName) End If End Sub Public Sub Publish(eventName As String, Optional parameter As Object = Nothing) Implements IEventAggregator.Publish If eventNamesToActions.ContainsKey(eventName) Then Dim handlers = eventNamesToActions(eventName) For Each handler In handlers handler(parameter) Next End If End Sub End Class
Der Einstiegspunkt für unser Programm
Da wir in unserem Beispiel hier am besten die „Dependency-Injection“ verwenden, hier ein kleines Beispiel.
Installiere dafür die „autofac„-Bibliothek, z. B. über das entsprechende NuGet-Paket.
Lege diese Klasse ins Wurzelverzeichnis Deines Windows Forms Projekts:
Imports System.IO Imports System.Reflection Imports Autofac Imports MyCRM.Contracts Public Class Program Public Shared Container As IContainer Public Shared Sub Main(args As String()) PrepareDI() RunApp() End Sub Private Shared Sub PrepareDI() Dim builder = New ContainerBuilder() builder.RegisterType(Of frmMain) builder.RegisterType(Of PluginListItem) builder.RegisterType(Of EventAggregator).As(Of IEventAggregator)().SingleInstance() builder.Register(Function(c) Dim pluginDir = Path.Combine(Application.StartupPath, "plugins") Dim pluginManager = New PluginManager(pluginDir) pluginManager.EnsurePluginFolder() pluginManager.LoadPluginTypes() pluginManager.LoadActivePluginsList() pluginManager.LoadPlugins() Return pluginManager End Function).As(Of IPluginManager)().SingleInstance() RegisterViews(builder) Container = builder.Build() End Sub Private Shared Sub RegisterViews(builder As ContainerBuilder) Dim asm = Assembly.Load(NameOf(MyCRM)) Dim isInViewsNamespace = Function(x) x.Namespace = $"{NameOf(MyCRM)}.{NameOf(Views)}" builder.RegisterAssemblyTypes(asm).Where(isInViewsNamespace) End Sub Private Shared Sub RunApp() Dim frm = Container.Resolve(Of frmMain) Application.Run(frm) End Sub End Class
Schaue Dir für weitere Details am besten das Programm selbst an, dafür kannst Du es wie immer herunterladen.
Sonst würde das Tutorial vermutlich noch 2000 weitere Wörter bekommen!
Das Hauptformular
Nun kommt das Hauptformular, wo wir dann über den „EventAggregator“ mit dem Plugin „telefonieren„.
Beachte, dass ich den „PluginManager“ hier als Abhängigkeit laden lasse, damit dessen Konstruktion ausgeführt wird.
Das könnte man natürlich/vermutlich auch anders lösen, aber da darfst Du dann gerne selbst Deine kreative Hand anlegen.
Im „Load„-Ereignishandler kommuniziere ich dann das „app.booted“ Ereignis an alle Abonnenten.
Unsere Plugin-Methode wird dies natürlich durch das Abonnement mitbekommen und entsprechend verarbeiten.
Imports Autofac Imports MyCRM.Contracts Imports MyCRM.Views Public Class frmMain Private _pluginManager As IPluginManager Private _eventAggregator As IEventAggregator Sub New(pluginManager As IPluginManager, eventAggregator As IEventAggregator) InitializeComponent() _pluginManager = pluginManager _eventAggregator = eventAggregator End Sub Private Sub frmMain_Load(sender As Object, e As EventArgs) Handles MyBase.Load ' maybe display some kind of loading screen, depending on needed time later _eventAggregator.Publish("app.booted") tcMain.SelectedTab = tpSettings End Sub Private Sub btnPluginSettings_Click(sender As Object, e As EventArgs) Handles btnPluginSettings.Click ShowPluginSettings() End Sub ' as method, cuz other menu items could trigger displaying it.. Private Sub ShowPluginSettings() splcSettings.Panel2.Controls.Clear() Dim pluginSettings = Program.Container.Resolve(Of PluginSettingsView)() splcSettings.Panel2.Controls.Add(pluginSettings) End Sub Private Sub tcMain_Selected(sender As Object, e As TabControlEventArgs) Handles tcMain.Selected If e.TabPage Is tpSettings Then ShowPluginSettings() End If End Sub Private Sub btnLogout_Click(sender As Object, e As EventArgs) Handles btnLogout.Click Application.Exit() End Sub Private Sub btnExit_Click(sender As Object, e As EventArgs) Handles btnExit.Click Application.Exit() End Sub End Class
Das Plugin final laden lassen
Lege nun das durch Visual Studio erstellte Plugin (die fertige DLL) in das passende Verzeichnis.
Zur Erinnerung: Es befindet sich im Ordner der Anwendung unter „plugins„.
Dort kannst Du noch einen weiteren Ordner wie z. B. „usage-widget“ erstellen.
Packe die DLL dann anschließend in diesen Ordner und es wird beim nächsten Programmstart verfügbar.
Fazit – VB NET Plugin-System
Am Ende des Tutorials angekommen hoffe ich, dass ich Dir ein eigenes Plugin-System näher bringen konnte.
Dabei habe ich es so gut wie möglich versucht, verständlich und schnell zu sein, allerdings wurde doch noch ein Riesen-Beitrag draus – ups.
Lass‚ Dich final nicht verwirren und lies Dir die einzelnen Passagen aufmerksam und zur Not mehrfach durch.
Lade Dir jedoch vor allem das Beispielprojekt herunter, wo Du dann alles zusammen und im Einsatz sehen kannst.
Da das Tutorial doch so lang geworden ist, werde ich die Umsetzung des Dashboard–Widgets wohl in einem zweiten Teil erläutern.