Click here to Skip to main content
15,896,557 members
Articles / Desktop Programming / Windows Forms

Plug-ins in C# 2.0: Generics Enabled Extension Library

Rate me:
Please Sign up or sign in to vote.
4.67/5 (39 votes)
7 Nov 2007CPOL7 min read 93K   1.3K   139  
A follow up to my previous article, this article takes the plug-in concept and encapsulates it in a Generics enabled library, including support for source code compilation at runtime.
/*
 * User: jondick
 * Date: 3/19/2007
 * Time: 6:13 PM
 */

using System;
using System.Collections.Generic;
using System.CodeDom.Compiler;
using System.Reflection;

namespace ExtensionManager
{
	/// <summary>
	/// A Generics enabled Framework for discovering and hosting Compiled and Source File Extensions
	/// </summary>
	public class ExtensionManager<ClientInterface, HostInterface>
    {
        #region Constructor
        public ExtensionManager()
        {
        }
        #endregion


        #region Events, Delegates, Event Helpers
        public delegate void AssemblyLoadingEventHandler(object sender, AssemblyLoadingEventArgs e);
        public event AssemblyLoadingEventHandler AssemblyLoading;
        private void OnAssemblyLoading(AssemblyLoadingEventArgs e)
        {
            if (this.AssemblyLoading != null)
                this.AssemblyLoading(this, e);
        }

        public delegate void AssemblyLoadedEventHandler(object sender, AssemblyLoadedEventArgs e);
        public event AssemblyLoadedEventHandler AssemblyLoaded;
        private void OnAssemblyLoaded(AssemblyLoadedEventArgs e)
        {
            if (this.AssemblyLoaded != null)
                this.AssemblyLoaded(this, e);
        }

        public delegate void AssemblyFailedLoadingEventHandler(object sender, AssemblyFailedLoadingEventArgs e);
        public event AssemblyFailedLoadingEventHandler AssemblyFailedLoading;
        private void OnAssemblyFailedLoading(AssemblyFailedLoadingEventArgs e)
        {
            if (this.AssemblyFailedLoading != null)
                this.AssemblyFailedLoading(this, e);
        }


        
        #endregion


        #region Private Variables
        private Dictionary<string, SourceFileLanguage> sourceFileExtensionMappings = new Dictionary<string, SourceFileLanguage>();
        private List<Extension<ClientInterface>> extensions = new List<Extension<ClientInterface>>();
        private List<string> compiledFileExtensions = new List<string>();
        private List<string> sourceFileReferencedAssemblies = new List<string>();
        #endregion


        #region Properties
        /// <summary>
        /// Contains file extensions (string) mapped to what Language they should be compiled in (SourceFileLanguage).
        /// To populate this List with the defaults, call LoadDefaultFileExtensions()
        /// </summary>
        public Dictionary<string, SourceFileLanguage> SourceFileExtensionMappings
        {
            get { return sourceFileExtensionMappings; }
            set { sourceFileExtensionMappings = value; }
        }

        /// <summary>
        /// List of Instances of all Loaded Extensions (both compiled and source file)
        /// </summary>
        public List<Extension<ClientInterface>> Extensions 
		{
			get { return extensions; }
			set { extensions = value; }
		}

        /// <summary>
        /// List of file extensions to try loading as Compiled Assemblies.
        /// To populate this List with the defaults, call LoadDefaultFileExtensions()
        /// </summary>
        public List<string> CompiledFileExtensions
        {
            get { return compiledFileExtensions; }
            set { compiledFileExtensions = value; }
        }

        /// <summary>
        /// List of Namespaces that Source Files may reference when being compiled.
        /// By Default this list is empty.
        /// </summary>
        public List<string> SourceFileReferencedAssemblies
        {
            get { return sourceFileReferencedAssemblies; }
            set { sourceFileReferencedAssemblies = value; }
        }
        #endregion


        #region Public Methods
		public void UnloadExtension(Extension<ClientInterface> extension)
		{
			Extension<ClientInterface> toRemove = null;

			foreach (Extension<ClientInterface> extOn in this.Extensions)
			{
				if (extOn.Filename.ToLower().Trim() == extension.Filename.ToLower().Trim())
				{
					toRemove = extOn;
					break;
				}
			}

			Extensions.Remove(toRemove);
		}

        /// <summary>
        /// Loads the SourceFileExtensionMappings and CompiledFileExtensions Lists with default values.
        /// Default Values Include:
        /// SourceFileExtensionMappings - .cs = CSharp, .vb = Vb, .js = Javascript
        /// CompiledFileExtensions - .dll
        /// </summary>
        public void LoadDefaultFileExtensions()
        {
            sourceFileExtensionMappings.Add(".cs", SourceFileLanguage.CSharp);
            sourceFileExtensionMappings.Add(".vb", SourceFileLanguage.Vb);
            sourceFileExtensionMappings.Add(".js", SourceFileLanguage.Javascript);
            compiledFileExtensions.Add(".dll");
        }

        /// <summary>
        /// Searches a given folder for files matching the SourceFileExtensionMappings and CompiledFileExtensions and
        /// attempts to load instances of those files.  Non-Recursive.
        /// </summary>
        /// <param name="folderPath">Full path to load files from</param>
        public void LoadExtensions(string folderPath)
        {
            if (!System.IO.Directory.Exists(folderPath))
                return;

            foreach (string fileOn in System.IO.Directory.GetFiles(folderPath))
            {
                LoadExtension(fileOn);
            }
        }

        /// <summary>
        /// Attempts to load an instance of the given file if it matches SourceFileExtensionMappings or CompiledFileExtensions
        /// </summary>
        /// <param name="filename">Full name of file to load</param>
        public void LoadExtension(string filename)
        {
			//Fire off the loading event, gives consumer a chance to cancel the loading
            AssemblyLoadingEventArgs eargs = new AssemblyLoadingEventArgs(filename);
            OnAssemblyLoading(eargs);

			//If the event consumer cancelled it, no need to continue for this file
            if (eargs.Cancel)
                return;

			//Get the extension of the file
            string extension = (new System.IO.FileInfo(filename)).Extension.TrimStart('.').Trim().ToLower();

			//Check to see if the extension is in the list of source file extensions
			//This allows us to pair up an extension with a particular language it should be compiled in
			//Primative but otherwise how do we know what to compile it as?  We could do some deep analysis of file content,
			// but that is beyond the scope of this library in my opinion
            if (SourceFileExtensionMappings.ContainsKey(extension) || SourceFileExtensionMappings.ContainsKey("." + extension))
            {
                SourceFileLanguage language = SourceFileLanguage.CSharp;

				//Get the matching language
                if (SourceFileExtensionMappings.ContainsKey(extension))
                    language = SourceFileExtensionMappings[extension];
                else
                    language = SourceFileExtensionMappings["." + extension];

				//Obviously it's a source file, so load it
                this.loadSourceFile(filename, language);
            }
            else if (CompiledFileExtensions.Contains(extension) || CompiledFileExtensions.Contains("." + extension))
            {
				//It's in the compiled file extension list, so just load it
                this.loadCompiledFile(filename);
            }
            else
            {
				//Unknown extension, raise the failed loading event
                AssemblyFailedLoadingEventArgs e = new AssemblyFailedLoadingEventArgs(filename);
                e.ExtensionType = ExtensionType.Unknown;
                e.ErrorMessage = "File (" + filename + ") does not match any SourceFileExtensionMappings or CompiledFileExtensions and cannot be loaded.";
                this.OnAssemblyFailedLoading(e);
            }
        }
        #endregion


        #region Private Methods
        private void loadSourceFile(string filename, SourceFileLanguage language)
		{
            bool loaded = false;
            string errorMsg = "";

			//Try compiling the script first
          	CompilerResults res = compileScript(filename, sourceFileReferencedAssemblies, this.getCodeDomLanguage(language));

			//Check for compilation errors
            if (res.Errors.Count <= 0)
            {
				//No errors, then loop through the types in the assembly
				//We don't stop after the first time we find our interface, this way the 
				//Assembly or source file could have multiple types with the desired interface
				//Instead of a 1 interface per file relationship
                foreach (Type t in res.CompiledAssembly.GetTypes())
                {
					//Get the string name of the ClientInterface interface
                    string typeName = typeof(ClientInterface).ToString();

					//Try getting the clientinterface from the type
                    if (t.GetInterface(typeName, true) != null)
                    {
						try
						{
							//Load an instance of this particular Extension and add it to our extensions list
							Extension<ClientInterface> newExt = new Extension<ClientInterface>(filename, ExtensionType.SourceFile, (ClientInterface)res.CompiledAssembly.CreateInstance(t.FullName, true));
							newExt.InstanceAssembly = res.CompiledAssembly;

							extensions.Add(newExt);
							loaded = true;
						}
						catch (Exception ex)
						{
							//Some problem in actually creating an instance
							errorMsg = "Error Creating Instance of Compiled Source File (" + filename + "): " + ex.Message;
						}
                    }
                }

				//We got through without loading an instance, so we didn't find types with the expected interface
                if (!loaded && String.IsNullOrEmpty(errorMsg))
                    errorMsg = "Expected interface (" + typeof(ClientInterface).ToString() + ") was not found in any types in the compiled Source File";
            }
            else
            {
				//Compile time errors
                errorMsg = "Source File Compilation Errors were Detected";
            }

            if (!loaded)
            {
				//Instance was never created, so let's report it and why
                AssemblyFailedLoadingEventArgs e = new AssemblyFailedLoadingEventArgs(filename);
                e.ExtensionType = ExtensionType.SourceFile;
                e.SourceFileCompilerErrors = res.Errors;
                e.ErrorMessage = errorMsg;
                this.OnAssemblyFailedLoading(e);
            }
		}

        private void loadCompiledFile(string filename)
        {
            bool loaded = false;
            string errorMsg = "";
            Assembly compiledAssembly = null;

			//Load the assembly to memory so we don't lock up the file
			byte[] assemblyFileData = System.IO.File.ReadAllBytes(filename);

			//Load the assembly
            try { compiledAssembly = Assembly.Load(assemblyFileData); }
            catch { errorMsg = "Compiled Assembly (" + filename + ") is not a valid Assembly File to be Loaded."; }

            if (compiledAssembly != null)
            {
				//Go through the types we need to find our clientinterface
                foreach (Type t in compiledAssembly.GetTypes())
                {
					//Just the string name of our ClientInterface since it's unknown at compile time
                    string typeName = typeof(ClientInterface).ToString();

					//Try getting the interface from the current type
                    if (t.GetInterface(typeName, true) != null)
                    {
						try
						{
							//Load an instance of this particular Extension and add it to our extensions list
							Extension<ClientInterface> newExt = new Extension<ClientInterface>(filename, ExtensionType.Compiled, (ClientInterface)compiledAssembly.CreateInstance(t.FullName, true));
							newExt.InstanceAssembly = compiledAssembly;

							extensions.Add(newExt);
							loaded = true;
						}
						catch (Exception ex)
						{
							//Creating an instance failed for some reason, pass along that exception message
							errorMsg = "Error Creating Instance of Compiled Assembly (" + filename + "): " + ex.Message;
						}
                    }
                }

				//If no instances were loaded at this point, means we never found types with the ClientInterface
                if (!loaded && String.IsNullOrEmpty(errorMsg))
                    errorMsg = "Expected interface (" + typeof(ClientInterface).ToString() + ") was not found in Compiled Assembly (" + filename + ")";
            }

            if (!loaded)
            {
				//Nothing was loaded, report it
                AssemblyFailedLoadingEventArgs e = new AssemblyFailedLoadingEventArgs(filename);
                e.ExtensionType = ExtensionType.Compiled;
                e.ErrorMessage = errorMsg;
                this.OnAssemblyFailedLoading(e);
            }
        }
	
		private CompilerResults compileScript(string filename, List<string> references, string language)
		{			
			System.CodeDom.Compiler.CodeDomProvider cdp = System.CodeDom.Compiler.CodeDomProvider.CreateProvider(language);
						
			// Configure parameters
			CompilerParameters parms = new CompilerParameters();
			parms.GenerateExecutable = false; //Don't make exe file
			parms.GenerateInMemory = true; //Don't make ANY file, do it in memory
			parms.IncludeDebugInformation = false; //Don't include debug symbols
			
			//Add references passed in 
			if (references != null)
				parms.ReferencedAssemblies.AddRange(references.ToArray());
						
			// Compile			
			CompilerResults results = cdp.CompileAssemblyFromFile(parms, filename);
						
			return results;
        }

        private string getCodeDomLanguage(SourceFileLanguage language)
        {
            string result = "C#";

            switch (language)
            {
                case SourceFileLanguage.CSharp:
                    result = "C#";
                    break;
                case SourceFileLanguage.Vb:
                    result = "VB";
                    break;
                case SourceFileLanguage.Javascript:
                    result = "JS";
                    break;
            }

            return result;
        }
        #endregion

    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
Canada Canada
Currently I'm an Oracle DBA for a School Board, having recently completed my undergrad at the University of Guelph with a Bachelor of Computing.

I obviously enjoy programming Smile | :)

Contact Me:
(MSN: jondick at gmail dot com)
(IRC: Dalnet: #c#, #asp.net, #vb.net)
(IRC: FreeNode: #linuxpeople)

Comments and Discussions