FileSystemWatcher mit MEF / Neue Plugins automatisch nachladen

Wie bereits angekündigt, gibt es nochmal einen Beitrag zu MEF. Vorab möchte ich gleich erwähnen, dass es sich hierbei um ein Beispielprogramm handelt. Ob das ganze so Praxistauglich ist lasse ich jedem offen.

Um was geht’s eigentlich. Ich bin vor einigen Tagen über die FileSystemWatcher Klasse im .NET Framework gestoßen. Mir ist schnell aufgefallen, dass man damit gut das Dateisystem auf Änderungen überwachen kann. Spontan sind mir dazu gleich mehrere Anwendungsfälle eingefallen. Ich hatte dann die Idee, dass mein Program über MEF mit neuen FileSystemWatcher Klassen gefüttert werden kann. Doch jedes Mal die laufende Applikation beenden, nur weil ein neuer FileSystemWatcher hinzukommt ist auch unpraktisch. Wäre es nicht toll, wenn die Applikation nicht einfach selbst einen FileSystemWatcher implementiert, der das PlugIns Verzeichnis überwacht und sobald dort ein neues PlugIn hineinkopiert wird, dieses automatisch lädt? Dann stellte ich mir die Frage – geht das? Ja, tut es – und das möchte ich euch hier kurz zeigen.

Projekt: FileSystemInspector
Systemumgebung: Visual Studio 2010, .NET Framework 4.0
Voraussetzung: MEF Grundkenntnisse (Falls diese noch nicht vorhanden sind kann ich nur auf meinen Eintrag hier verweisen.)

Teil 1: Unsere Interface Assembly
Wie auch beim letzten Mal gibt es eine eigenständige Assembly, die das Interface für unsere PlugIns enthält.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using System.ComponentModel.Composition;
using System.IO;
 
namespace FileSystemInspector.Inferfaces
{
    [InheritedExport]
    public interface IFileSystemInspector
    {
        Guid ID { get; }
        string Name { get; }
        Version Version { get; }
        DateTime Date { get; }
        FileSystemWatcher FileSystemWatcher { get; set; }
 
        void Start();
        void Stop();
    }
}

Die Guid wird dazu verwendet beim neu laden der Plugins den FileSystemWatcher wieder eindeutig zu identifizieren. Den Namen lassen wir uns an zwei Stellen ausgeben, damit wir wissen um welchen FileSystemWatcher es sich handelt. Version und Datum werden an sich nicht benötigt. Dafür aber natürlich ein FileSystemWatcher. Die Start und Stop Funktionen dienen uns später dazu den FileSystemWatcher zu aktivieren bzw. zu deaktivieren.

Teil 2: Ein FileSystemInspector
Hier sehen wir die Implementierung eines FileSystemInspectors. Natürlich wird für jeden FileSystemInspector eine eigene Assembly angelegt.

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
using System;
using System.IO;
using System.Reflection;
using FileSystemInspector.Inferfaces;
 
namespace FileSystemInspector.Default
{
    public class DefaultFileSystemInspector : IFileSystemInspector
    {
        public Guid ID { get { return new Guid("C6BB10A2-70D6-47A3-8AF5-26494CA7E375");} }
        public string Name { get { return "Default Inspector"; } }
        public Version Version { get { return Assembly.GetExecutingAssembly().GetName().Version ; } }
        public DateTime Date { get { return new DateTime(2011, 06, 13); } }
        public FileSystemWatcher FileSystemWatcher { get; set; }
 
        public DefaultFileSystemInspector()
        {
            FileSystemWatcher FileSystemWatcher = new FileSystemWatcher();
            FileSystemWatcher.EnableRaisingEvents = false;
            FileSystemWatcher.Path = @"C:\";
            FileSystemWatcher.Filter = "*.*";
            FileSystemWatcher.IncludeSubdirectories = true;
            FileSystemWatcher.NotifyFilter = NotifyFilters.Attributes | NotifyFilters.LastAccess |
                                             NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size | 
                                             NotifyFilters.FileName | NotifyFilters.DirectoryName;
        }
 
        public void Start()
        {
            FileSystemWatcher.EnableRaisingEvents = true;
        }
 
        public void Stop()
        {
            FileSystemWatcher.EnableRaisingEvents = false;
        }
    }
}

Im Konstruktor werden die Einstellungen für den FileSystemWatcher gesetzt. In diesem Fall überwachen wir das Laufwerk C:\ auf alle Änderungen. Unterverzeichnisse werden dabei auch berücksichtigt.

Teil 3: Laden der PlugIns
In unserer Windows Forms Applikation legen wir uns dann wieder eine Klasse zum laden der PlugIns an. Ich zeige euch jetzt erst einmal den kompletten Code. Die Erklärungen folgen dann unten.

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.IO;
using System.Reflection;
using FileSystemInspector.Inferfaces;
 
namespace FileSystemInspector
{
    public class FileSystemInspectorComposer
    {
        #region Fields
 
        [ImportMany(typeof(IFileSystemInspector), AllowRecomposition = true)]
        public List<IFileSystemInspector> FSInspectorList { get; set; }
 
        DirectoryCatalog _catalog = null;
        FileSystemWatcher _fileSystemWatcher = null;
        string _plugInsPath = string.Empty;
 
        #endregion
 
        #region Events
 
        public event Action FSInspectorListChanged;
        public event FileSystemEventHandler FileChanged;
        public event FileSystemEventHandler FileCreated;
        public event FileSystemEventHandler FileDeleted;
        public event RenamedEventHandler FileRenamed;
 
        #endregion
 
        #region Constuctor
 
        public FileSystemInspectorComposer()
        {
            // Create plugins path
            _plugInsPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "PlugIns");
 
            // Filesystem watcher
            _fileSystemWatcher = new FileSystemWatcher();
            _fileSystemWatcher.Path = _plugInsPath;
            _fileSystemWatcher.Filter = "*.dll";
            _fileSystemWatcher.NotifyFilter = NotifyFilters.CreationTime;
            _fileSystemWatcher.Changed += new FileSystemEventHandler(InternalFileChanged);
        }
 
        public void Initialize()
        {
            // Load plugins
            _catalog = new DirectoryCatalog(_plugInsPath);
            CompositionContainer compositionContainer = new CompositionContainer(_catalog);
            compositionContainer.ComposeParts(this);
            AddEventHandler();
 
            _fileSystemWatcher.EnableRaisingEvents = true;
        }
 
        #endregion
 
        #region Monitor changes
 
        private void RefreshCatalog()
        {
            try
            {
                _catalog.Refresh();
 
                AddEventHandler();
 
                if (FSInspectorListChanged != null)
                    FSInspectorListChanged();
            }
            catch { }
        }
 
        void InternalFileChanged(object sender, FileSystemEventArgs e)
        {
            // Refresh should be called delayed to ensure the file is completly copied
            RefreshCatalog();
        }
 
        void AddEventHandler()
        {
            foreach (IFileSystemInspector inspector in FSInspectorList)
            {
                inspector.FileSystemWatcher.Changed += FileChanged;
                inspector.FileSystemWatcher.Created += FileCreated;
                inspector.FileSystemWatcher.Deleted += FileDeleted;
                inspector.FileSystemWatcher.Renamed += FileRenamed;
            }
        }
 
        #endregion
    }
}

So. Dann kommen wir mal zu den Erklärungen. Eines der wichtigsten Elemente kommt gleich am Anfang. Bei der Definition der Liste, die die PlugIns später aufnimmt, ist unbedingt ‚AllowRecomposition = true‚ zu setzen, da sonst die Plugins zur Laufzeit nicht neu geladen werden können. Hier nochmal der ensprechende Aufruf

1
2
[ImportMany(typeof(IFileSystemInspector), AllowRecomposition = true)]
public List<IFileSystemInspector> FSInspectorList { get; set; }

Ab Zeile 26 definieren wir uns ein paar Events. Eines dient nur dazu der GUI mitzuteilen, dass wir neue Plugins geladen haben. Die vier weiteren Events sind Eventhandler des FileSystemWatchers. Diese werden jedem geladenen PlugIn zugewiesen (Ab Zeile 88). In unserer GUI können wir dann auf diese Events reagieren.

Im Konstruktor (Ab Zeile 36) definieren wir uns unseren internen FileSystemWatcher, der unser PlugIn Verzeichnis überwachen soll. Dieser soll uns aber nur informieren, wenn eine neue Datei im Verzeichnis hinzugekommen ist. Die gleich im Anschluss definierte Funktion Initialize übernimmt dann den Initialen Ladevorgang der PlugIns und aktiviert unseren internen FileSystemWatcher.

Die letzte interessante Funktion ist dann RefreshCatalog. Diese wird immer dann aufgerufen, wenn ein neues PlugIn ins Verzeichnis kopiert wird. In dieser Funktion wird der Katalog aktualisiert, die Eventhandler für die FileSystemWatcher neu zugewiesen und die GUI benachrichtigt, dass es ein neues PlugIn geladen wurde.

Teil 4: Das UserInterface

Bei der GUI habe ich meine ganze Kreativität einfließen lassen 😉

FileSystemInspector

Auf der linken Seite werden die verschiedenen FileSystemInspector aufgelistet. Über die Checkbox kann man diese aktivieren / deaktivieren. Auf der rechten Seite findet man dann die Ausgaben.


Downloads:
Das komplette Visual Studio 2010 Projekt: Download
Nur das Programm und ein PlugIn: Download


Fazit:
Ich muss sagen, dass das Nachladen der Plugins super funktioniert – also gerade bei einer so kleinen Applikation. Ob das bei einer komplexeren Anwendung immer noch Sinn gibt, muss jeder für sich entscheiden. Was leider nicht (oder evtl. schon, aber nicht einfach) funktioniert, ist das dynamische entfernen/aktualisieren von PlugIns. Dadurch dass die PlugIns geladen wurden sind diese ja in Benutzung und können daher nicht gelöscht oder überschrieben werden.

Schreibe einen Kommentar

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