Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

1

22.09.2011, 17:39

C# Plugin Entwicklung

Hallo,
ich überlege schon eine Weile mein Programm Plugin fähig um zu gestalten, dabei habe ich diese Ansätze:
Jedes Plugin (welches das Interface IGkPlugin implementiert) hat eine List of Modules. Jedes Modul hat seine spezifische Aufgabe und erbt von GkModule oder seinen Tochterklassen (Die GkApi beinhaltet ein Klassenparadigma der GkModules).

Möchte man beispielsweise ein Drucker-Plugin hinzufügen, so erstellt man eine neue Dll mit einer Plugin-Klasse, die das IPlugin Implementiert und die jeweiligen Druckermodule, welche von GkFinancialPrinterModule<OrderViewModel> abgeleitet sind, in einer List beinhaltet:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 public class PrintPlugin : IGkPlugin
{
    private List<GkModule> modules;

    public PrintPlugin()
    {
        modules = new List<GkModule>();
        modules.Add( new GkPrintOrdersPerDayModule() ); // Das jeweilige Drucker Module
    }

    public List<GkModule> Modules
    {
        get { return modules; }
    }

    public string Author
    {
        get { return string.Empty; }
    }
    
    public void Install()
    {
        // Nothing to install
    }

    public void Remove()
    {
        // nothing to delete
    }
}


In der Host-App gibt es einen GkPluginManager, welcher für das installieren/deinstallieren, laden und freigeben der Plugins zuständig ist. Außerdem erhält man über diesen die relevanten Module.
Hier eine Beispielimplementierung für die Einbindung der Druckerplugins:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public FinancialControlView( FinanceWorkspace dataContext )
    {
        InitializeComponent();
        DataContext = dataContext;
        
        // Initialisiert die aktuelle Lokale mit ihren Eigenschaften (€ oder $ ...)
        this.Language = XmlLanguage.GetLanguage( Thread.CurrentThread.CurrentCulture.Name );

        var plugins = GkPluginManager.Current.GetModules<GkFinancialPrinterModule<OrderViewModel>>();

        foreach (var plugin in plugins)
        {
            plugin.SetContext( dataContext.StartPoint, dataContext.EndPoint, dataContext.OrderCollection ); 
            Button btn = new Button { 
                Content = plugin.Name, 
                Command = new RelayCommand(p => plugin.PrintDocument()),
                Width = 120,
                Height = 50,
                Margin = new Thickness(5)
            };
            wrapPanel.Children.Add( btn );                           
        }
    }

Bevor ich diesen Ansatz jetzt endgültig so umsetzen werde, wollte ich mal eure Meinung hören. Gibt es vielleicht Verbesserungen, ist mein System zu unflexibel? Gibt es dabei vielleicht noch wichtiges zu beachten? Ist es überhaupt empfehlenswert, mein Programm so 'modular' wie möglich zu gestalten (Mein Gedanke dabei ist, das Programm für den Kunden so angepasst wie möglich bereitzustellen - und das mit 'geringem Aufwand')?

Eine "schwäche" zB. dir mir Aufgefallen ist, ist die SetContext Methode. Normalerweise wäre das ja die Aufgabe eines Konstruktors, jedoch kann ich diesen ja nicht erzwingen. Momentan weisen Exception darauf hin, falls die Klasse noch nicht initialisiert sein sollte. Eine weitere unklare Sache ist, ob die Instanz der GkModules erst erstellt werden soll, wenn das Modul wirklich benötigt wird oder schon bei der Initialisierung des GkPluginManagers.

Das sind viele Fragen :)
Würde mich über Antworten sehr freuen!

dot

Supermoderator

Beiträge: 9 757

Wohnort: Graz

  • Private Nachricht senden

2

23.09.2011, 00:41

Klingt schon ganz gut :)
Du könntest per Reflection sicherstellen, dass ein entsprechender Konstrutktor vorhanden ist. Oder (die saubere Lösung) dich einer Factory bedienen.

Ein alternatives Design zur Modulliste wäre, in deiner dll einen PluginServer zu verpacken. Dein PluginManager sucht dann in der dll nach einem PluginServer und instanziert diesen. Der PluginServer hat dann eine Methode RegisterPlugins(), die der PluginManager aufruft. Die Methode bekommt dabei ein Interface, bei dem Plugins registriert werden können (es kann sich dabei natürlich um den PluginManager selbst handeln). So kann der PluginServer dem PluginManager mitteilen, was für Plugins in der dll stecken und wie sie instanziert werden können (übergibt z.B. eine Factory für das Plugin). Da der Manager dann Factories hat, kann er nun ein Plugin instanzieren, wann immer es benötigt wird. Vorteil dieser Lösung ist, dass das System in seiner Natur vollkommen static-typesafe ist (bei deiner Modulliste wirst du zwangsweise einen Downcast von GkModule auf abgeleitetes Interface haben). Wenn du es potentiell mit sehr vielen Plugins zu tun bekommst, wird es sich anbieten, nur die aktuell wirklich benötigten Plugins instanziert zu halten. Evtl. solltest du in diesem Fall überhaupt dafür sorgen, dass momentan nicht benutzte Plugin dlls entladen werden können oder garnicht erst geladen werden müssen.

Dieser Beitrag wurde bereits 8 mal editiert, zuletzt von »dot« (23.09.2011, 01:18)


TrommlBomml

Community-Fossil

Beiträge: 2 117

Wohnort: Berlin

Beruf: Software-Entwickler

  • Private Nachricht senden

3

23.09.2011, 10:15

Du könntest per Reflection sicherstellen, dass ein entsprechender Konstrutktor vorhanden ist.
Das würde ich eher nicht tun. Reflection ist toll, aber da würde ich an der Stelle zu einer Init-Methode im Interface raten. Wenn du einige an Plugins hast, kann sich das bemerkbar machen, reflection ist halt nicht das fixeste. und ich halte es aus Pluginsicht sauberer. Jeder, der ein Plugin schreibt, weiss, dass die Init der Punkt zum initialisieren ist - das ausführen des "doofen code mit Konstruktor + Init" vollführt die ladende Assembly.

dot

Supermoderator

Beiträge: 9 757

Wohnort: Graz

  • Private Nachricht senden

4

23.09.2011, 10:26

Ja ich würde es auch nicht mit Reflection lösen. Aber ehrlich gesagt, fänd ich eine Init() Methode noch sehr viel hässlicher. Macht aber eh nix, denn es gibt ja eine saubere Lösung: Factory.

Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von »dot« (23.09.2011, 10:35)


TrommlBomml

Community-Fossil

Beiträge: 2 117

Wohnort: Berlin

Beruf: Software-Entwickler

  • Private Nachricht senden

5

23.09.2011, 13:44

Ja ich würde es auch nicht mit Reflection lösen. Aber ehrlich gesagt, fänd ich eine Init() Methode noch sehr viel hässlicher. Macht aber eh nix, denn es gibt ja eine saubere Lösung: Factory.
Finde ich persönlich nicht. das macht das erzeugen des Plugins enorm einfach. du musst den Konkreten Typ nicht kennen, sondern nur das Interface. Damit lässt sich ein simpler AssemblyReader entwerfen, der einfach in einem Pluginverzeichnis alle Assemblies abklappert und nach Klassen sucht, die dieses Interface implementieren - da braucht man dann reflection, das lässt sich aber mit FullNames + Timestamps prima cachen. Das macht das Laden von Plugins total easy:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using System;using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
 
namespace Hyperlinktets
{
    public interface MyPluginInterface
    {
        void Init();
    }
 
    public class PlugInManager
    {
        public List Plugins { get; private set; }
 
        public PlugInManager(string pluginPath)
        {
            Plugins = new List();
 
            string[] files = Directory.GetFiles(pluginPath);
            foreach (string file in files)
            {
                string extension = Path.GetExtension(file);
                if (extension != null && extension.ToLowerInvariant() == ".dll")
                {
                    try
                    {
                        Assembly assembly = Assembly.Load(file);
 
                        IEnumerable myPluginClasses = assembly.GetTypes().Where(t => t.GetInterfaces().Any(i => i is MyPluginInterface));
                        foreach (var myPluginClass in myPluginClasses)
                        {
                            MyPluginInterface plugin = (MyPluginInterface)assembly.CreateInstance(myPluginClass.FullName);
                            plugin.Init();
                            Plugins.Add(plugin);
                        }
                    }
                    catch (Exception ex)
                    {
                        Log.Write(string.Format("Could not load assembly: {0}\n Error: {1}"), file, ex.Message);
                    }
                }
            }
        }
    } }


Caching fehlt noch. Find ich schicker. MIt der Factory ist es meiner Meinung nach umständlicher. Und so wie ich factories verstanden habe braucht man doch pro konkreten Objekttyp eine Konkrete Factorymethode...oder?

dot

Supermoderator

Beiträge: 9 757

Wohnort: Graz

  • Private Nachricht senden

6

23.09.2011, 13:55

Ja ich würde es auch nicht mit Reflection lösen. Aber ehrlich gesagt, fänd ich eine Init() Methode noch sehr viel hässlicher. Macht aber eh nix, denn es gibt ja eine saubere Lösung: Factory.
Finde ich persönlich nicht. das macht das erzeugen des Plugins enorm einfach. du musst den Konkreten Typ nicht kennen, sondern nur das Interface.

Genau das is doch der Zweck einer Factory (ich mein natürlich eine Abstract Factory)!? Du hast einfach nur eine XYZPluginFactory die dir ein XYZPlugin liefert. Alles Interfaces, die konkreten Implementierungen kennt nur die Implementierung der Factory, alles komplett statisch typesafe, keine hässlichen init() Methoden, bis auf das Erzeugen des PluginServer braucht man nirgendwo Reflection.

7

24.09.2011, 08:31

So kann der PluginServer dem PluginManager mitteilen, was für Plugins in der dll stecken und wie sie instanziert werden können (übergibt z.B. eine Factory für das Plugin).
Das klingt ja schonmal ganz interessant. Das Problem ist nur, dass ja bekannt sein muss, welche Argumente der Factory übergeben werden müssen. Meinst du etwa, dass ich das Factory-Interface in der API definiere? So viel habe ich verstanden:

C#-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PrinterFactory<T> : IGkPluginPrinterFactory
{
 T CreateInstance(IEnumerable<OrderViewModel orders>) {...}
}

public class PrinterPluginServer : GkPluginServer
{
  ...
  public void RegisterPlugin(GkPluginManager pm)
  {
    pm.AddFactory(IGkPluginPrinterFactory, new PrinterFactory<PrinterModule1>());
    pm.AddFactory(IGkPluginPrinterFactory, new PrinterFactory<PrinterModule2>());
    ....
  }
....
}


Der PluginManager hat dann eine HashMap (oder auch OrderdDictionary) des Typs FactoryInterface als Key und der Factory als Value.

dot

Supermoderator

Beiträge: 9 757

Wohnort: Graz

  • Private Nachricht senden

8

24.09.2011, 09:09

Meinst du etwa, dass ich das Factory-Interface in der API definiere?

Ja, natürlich ;)

9

01.10.2011, 17:08

Ich habe die letzten Tagen ein wenig mit diesem Plugin-System rumgespielt und bin auch ganz zufrieden damit. Jetzt wollte ich mich mal nach Alternativen umsehen und habe sehr zu meiner Freude das Managed Extensibility Framework entdeckt. Dieses Framework stellt natürlich mein bisheriges System in den Schatten - zumindest auf den ersten Blick. So leicht und flexibel dieses Framework auch ist, sorge ich mich ein wenig um die Performance und besonders um die Startzeit, die jetzt schon ziemlich lange dauert :(
Falls also jemand von euch schonmal mit diesem Framework gearbeitet hat, würde ich mich über ein Statement bezüglich meiner Befürchtungen (Performance und Startzeit) freuen!

Werbeanzeige