/*************************************************************************************************************************** * ModuleLoader * By: Rob Diaz-Marino, University of Calgary * Date: Jan 27, 2010 * Description: * This file contains the code for a class that handles runtime DLL loading for your own plug-in modules. * Each module must have one class that implements the IModule interface. The ModuleManager finds this class and * stores an instance of it. This instance is used to dispatch further interfaces for your program to interact with * your module, so design accordingly. * * ModuleManager assumes all your module DLLs are in a single directory and are named using a particular pattern * so that they can be distinguished from other support DLLs. It is built to expect that your program will store a list * of known modules, however this can be optional. Notifications are thrown at each step of the module discovery * process to offer your program (or your user) the chance to disable loading of any particular modules. * * ModuleManager supports synchronous and asynchronous loading, however note that when using asynchronous loading and * updating GUI components, you must use thread safe calling techniques to avoid illegal cross-thread operations. * For your convenience, static ThreadSafeCall and ThreadSafeComponentCall methods are included in the ModuleManager * class. These can be used to invoke methods or set properties on a target Control or Component. ***************************************************************************************************************************/ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Configuration; using System.Windows.Forms; using System.Reflection; using System.ComponentModel; using System.Threading; namespace ModuleLoader { // ModuleManager delegates public delegate void LoadProgressUpdatedHandler(int percent, string message); public delegate void LoadFailedHandler(ModuleManager.Entry entry); public delegate void LoadCompletedHandler(); public delegate void ModuleNotifyHandler(List entries); // Thread-safe calling delegates public delegate void ThreadSafeControlCallDelegate(Control control, string method, object[] parameters); public delegate void ThreadSafeComponentCallDelegate(Component control, string method, object[] parameters); public interface IModule { string ModuleName { get; } } public class ModuleManager : IDisposable { #region Subclasses public class Entry : IComparable { internal Assembly assembly = null; internal IModule module = null; // Persistent Settings private FileInfo fileinfo = null; private long filesize = 0; public bool LoadEnabled = true; private DateTime lastmodified = DateTime.MinValue; public bool NotifyModified = true; // Non-persistent settings private bool isnew = false; internal bool loadfailed = false; internal Exception loadfailreason = null; #region Constructors & Destructors public Entry(FileInfo fileinfo) { this.FileInfo = fileinfo; isnew = true; } #endregion #region Properties public FileInfo FileInfo { get { return fileinfo; } set { fileinfo = value; if (fileinfo == null) { filesize = 0; lastmodified = DateTime.MinValue; } else { filesize = fileinfo.Length; lastmodified = fileinfo.LastWriteTime; } } } public long FileSize { get { return filesize; } } public DateTime LastModified { get { return lastmodified; } } public bool IsModified { get { if (fileinfo == null) return false; return fileinfo.LastWriteTime != lastmodified || fileinfo.Length != filesize; } } public bool IsNew { get { return isnew; } } public bool IsMissing { get { if (fileinfo == null) return true; else return !File.Exists(fileinfo.FullName); } } public bool LoadFailed { get { return loadfailed; } } public Exception LoadFailedReason { get { return loadfailreason; } } public IModule Module { get { return module; } } public bool IsLoaded { get { return module != null; } } #endregion #region Methods public bool Load() { if (IsLoaded) return true; // Attempt to load the module try { assembly = Assembly.LoadFrom(fileinfo.FullName); } catch (Exception ex) { // Update the status of this entry to indicate failure module = null; assembly = null; loadfailed = true; loadfailreason = ex; return false; } // Attempt to find the module entry point (only one expected) try { // Get all the types defined in the DLL foreach (System.Type type in assembly.GetTypes()) { foreach (Type classinterface in type.GetInterfaces()) { if (classinterface.Equals(typeof(IModule))) { // Create an instance of the module module = (IModule)Activator.CreateInstance(type); // Update the status of this entry loadfailed = false; loadfailreason = null; return true; } } } module = null; assembly = null; loadfailed = true; loadfailreason = new Exception("Entry point not found for " + fileinfo.Name + ". Expected to find one class that implements IModule interface."); return false; } catch (Exception ex) { // Update the status of this entry to indicate failure module = null; assembly = null; loadfailed = true; loadfailreason = ex; return false; } } #endregion #region Overrides public override bool Equals(object obj) { if (obj is Entry) { return fileinfo.FullName.Equals(((Entry)obj).fileinfo.FullName); } return false; } public override int GetHashCode() { return base.GetHashCode(); } #endregion #region IComparable Members public int CompareTo(Entry other) { return fileinfo.FullName.CompareTo(other.fileinfo.FullName); } #endregion } private class WorkerProgress { public Entry CurrentEntry = null; } #endregion // Events public event LoadProgressUpdatedHandler LoadProgressUpdated; public event LoadFailedHandler LoadFailed; public event LoadCompletedHandler LoadCompleted; public event ModuleNotifyHandler NotifyNewModules; public event ModuleNotifyHandler NotifyUpdatedModules; public event ModuleNotifyHandler NotifyMissingModules; // Collections private List knownmodules = new List(); private List loadedmodules = new List(); private List newmodules = new List(); private List changedmodules = new List(); private List failedmodules = new List(); // Components private BackgroundWorker worker = null; #region Constructor and Destructor /// /// /// /// The settings object for the application. public ModuleManager() { } public void Dispose() { // Cancel out all event handlers to prevent memory leaks LoadProgressUpdated = null; LoadFailed = null; LoadCompleted = null; NotifyNewModules = null; NotifyUpdatedModules = null; NotifyMissingModules = null; } #endregion #region Properties public int KnownModuleCount { get { return knownmodules.Count; } } public int LoadedModuleCount { get { return loadedmodules.Count; } } public int NewModuleCount { get { return newmodules.Count; } } public int ChangedModuleCount { get { return changedmodules.Count; } } public int FailedModuleCount { get { return failedmodules.Count; } } #endregion #region Enumerators public IEnumerator LoadedModules { get { return loadedmodules.GetEnumerator(); } } public IEnumerator NewModules { get { return newmodules.GetEnumerator(); } } public IEnumerator FailedModules { get { return failedmodules.GetEnumerator(); } } public IEnumerator ChangedModules { get { return changedmodules.GetEnumerator(); } } public IEnumerator KnownModules { get { return knownmodules.GetEnumerator(); } } #endregion #region Thread Safe Calling /// /// Handles a method call or sets a property value /// /// The control that has the member to call. /// The method or propery name. /// The parameters for the call. Properties will set using index 0. public static void ThreadSafeCall(Control control, string member, object[] parameters) { if (control == null) return; if (control.InvokeRequired) { ThreadSafeControlCallDelegate theDelegate = new ThreadSafeControlCallDelegate(ThreadSafeCall); control.BeginInvoke(theDelegate, new object[] { control, member, parameters }); } else { MethodInfo mi = control.GetType().GetMethod(member); if (mi != null) mi.Invoke(control, parameters); else { PropertyInfo pi = control.GetType().GetProperty(member); if (pi != null) pi.SetValue(control, parameters[0], null); } } } public static void ThreadSafeCall(Component control, string member, object[] parameters) { ThreadSafeComponentCallDelegate theDelegate = new ThreadSafeComponentCallDelegate(ThreadSafeComponentCall); Application.OpenForms[0].BeginInvoke(theDelegate, new object[] { control, member, parameters }); } public static void ThreadSafeComponentCall(Component control, string member, object[] parameters) { MethodInfo mi = control.GetType().GetMethod(member); if (mi != null) mi.Invoke(control, parameters); else { PropertyInfo pi = control.GetType().GetProperty(member); if (pi != null) pi.SetValue(control, parameters[0], null); } } #endregion #region Methods /// /// Searches through a given directory for new modules. WARNING: Should only be called after info on known modules has been loaded! /// /// The directory to search. /// The filename pattern to search. private List DiscoverNewModules(DirectoryInfo directory, string filter) { List found = new List(); // Check for valid directory if (!Directory.Exists(directory.FullName)) throw new DirectoryNotFoundException("The directory " + directory.FullName + " was not found!"); // Add all matching files foreach (FileInfo fi in directory.GetFiles(filter)) { found.Add(new Entry(fi)); } // Remove all known files foreach (Entry e in knownmodules) found.Remove(e); return found; } /// /// Returns a list of modules that have been modified since the last load. /// /// The list of known modules to check. /// The subset of modules that have been modified. private List DiscoverChangedModules(List known) { List changed = new List(); foreach (Entry e in known) { if (!File.Exists(e.FileInfo.FullName)) continue; if (e.IsModified && e.NotifyModified) changed.Add(e); } return changed; } /// /// Returns a list of modules that no longer exist from a list of known modules. /// /// The list of known modules to check. /// The subset of modules that are missing. private List DiscoverMissingModules(List known) { List missing = new List(); foreach (Entry e in known) { if (e.IsMissing) missing.Add(e); } return missing; } /// /// Loads a list of modules from their entry definitions. /// /// The list to load. /// The subset of successfully loaded (or purposely ignored) modules. private void Load(List entries) { for (int i = 0; i < entries.Count; i++) { // Notify progress if (LoadProgressUpdated != null) LoadProgressUpdated((i + 1) * 100 / entries.Count, "Loading " + entries[i].FileInfo.Name + "..."); entries[i].Load(); if (entries[i].IsLoaded) loadedmodules.Add(entries[i]); if (entries[i].LoadFailed && LoadFailed != null) LoadFailed(entries[i]); // Notify load failure } if (LoadProgressUpdated != null) LoadProgressUpdated(100, "Loading complete!"); FinishLoad(); } /// /// Determines the load list and loads the modules. Invokes several callbacks to confirm /// /// /// public void Load(DirectoryInfo di, string filter, List known, bool asynch) { // Load the list of already known modules knownmodules.AddRange(known); // Handle missing modules List missing = DiscoverMissingModules(knownmodules); if (NotifyMissingModules != null && missing.Count > 0) NotifyMissingModules(missing); // Handle modified modules List updated = DiscoverChangedModules(knownmodules); if (NotifyUpdatedModules != null && updated.Count > 0) NotifyUpdatedModules(updated); // Handle new modules List added = DiscoverNewModules(di, filter); if (NotifyNewModules != null && added.Count > 0) NotifyNewModules(added); knownmodules.AddRange(added); // Create the final list of modules List loadlist = new List(); foreach (Entry e in knownmodules) { if (e.LoadEnabled && !e.IsMissing) loadlist.Add(e); } if (loadlist.Count > 0) { if (asynch) { // Launch the Background Worker to load the modules asynchronously worker = new BackgroundWorker(); worker.WorkerReportsProgress = true; worker.DoWork += new DoWorkEventHandler(worker_DoWork); worker.ProgressChanged += new ProgressChangedEventHandler(worker_ProgressChanged); worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted); worker.RunWorkerAsync(loadlist); } else Load(loadlist); } else { if (LoadProgressUpdated != null) LoadProgressUpdated(100, "No modules to load..."); if (LoadCompleted != null) LoadCompleted(); } } private void FinishLoad() { // Compile the additional lists foreach (Entry ent in knownmodules) { if (ent.LoadFailed) failedmodules.Add(ent); else if (ent.IsNew) newmodules.Add(ent); else if (ent.IsModified) changedmodules.Add(ent); } // Notify of completion if (LoadCompleted != null) LoadCompleted(); } #endregion #region Background Worker Event Handlers void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { worker.Dispose(); worker = null; FinishLoad(); } void worker_ProgressChanged(object sender, ProgressChangedEventArgs e) { // Notify progress WorkerProgress prog = (WorkerProgress)e.UserState; if (LoadProgressUpdated != null) { if (prog.CurrentEntry == null) { LoadProgressUpdated(100, "Loading complete!"); return; } else LoadProgressUpdated(e.ProgressPercentage, "Loading " + prog.CurrentEntry.FileInfo.Name + "..."); } if (prog.CurrentEntry.IsLoaded) loadedmodules.Add(prog.CurrentEntry); if (prog.CurrentEntry.LoadFailed && LoadFailed != null) LoadFailed(prog.CurrentEntry); // Notify load failure } void worker_DoWork(object sender, DoWorkEventArgs e) { List loadlist = (List)e.Argument; WorkerProgress prog = new WorkerProgress(); for (int i = 0; i < loadlist.Count; i++) { prog.CurrentEntry = loadlist[i]; // Notify progress worker.ReportProgress((i + 1) * 100 / loadlist.Count, prog); // Load the module loadlist[i].Load(); // Pause to allow other processes to catch up Thread.Sleep(500); // Notify result worker.ReportProgress((i + 1) * 100 / loadlist.Count, prog); } // Notify update prog.CurrentEntry = null; worker.ReportProgress(100, prog); } #endregion } }