Dein VB NET Plugin-System im Eigenbau – DAS ultimative Beispiel!

VB NET Plugin-System
VB NET Plugin-System

Inhaltsverzeichnis

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

VB NET Plugin-System Hintergrund
VB NET Plugin-System Hintergrund

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 rootswerden 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“

VB NET Plugin-System Marke Eigenbau
VB NET Plugin-System Marke Eigenbau

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 assemblebedeutet 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

VB NET Plugin-System Projektmappe vorbereiten
VB NET Plugin-System Projektmappe 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„:

Neues Visual Studio Projekt erstellen
Neues Visual Studio Projekt erstellen

Danach kannst Du in dem Suchfeld oben nach dem Projekt-Typprojektmappe“ 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:

Eine neue leere Projektmappe in Visual Studio anlegen
Eine neue leere Projektmappe in Visual Studio anlegen

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

Eine durch Plugins erweiterbare Beispiel-Anwendung
Eine durch Plugins erweiterbare Beispiel-Anwendung

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.

Projekt zu Projektmappe in Visual Studio hinzufügen
Projekt zu Projektmappe in Visual Studio hinzufügen

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

Beispiel VB NET Plugin System
Beispiel VB NET Plugin System

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 HauptFunktionalitä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 DashboardWidgets 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

Eine eigene Plugin-Manager Klasse
Eine eigene Plugin-Manager Klasse

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 HilfsFunktion 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 „Dingist 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 fixansehe, 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-Containerresolved“ 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

Eine VB NET Plugin Schnittstelle
Eine VB NET Plugin Schnittstelle

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 BasisImplementierungen 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

Die Plugin Implementierung - ein UsageWidget
Die Plugin Implementierung – ein UsageWidget

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

Das EventAggregator Entwurfsmuster
Das EventAggregator Entwurfsmuster

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-widgeterstellen.

Packe die DLL dann anschließend in diesen Ordner und es wird beim nächsten Programmstart verfügbar.

Fazit – VB NET Plugin-System

VB NET Plugin-System Fazit
VB NET Plugin-System Fazit

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.

LassDich 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 DashboardWidgets wohl in einem zweiten Teil erläutern.

Downloads

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert