Putting It TogetherA Real-World Example


This next section will take you through the process of creating a rudimentary framework for building applications that can be extended via plug-ins. These applications can be web applications or Windows applications; it doesn't make any difference except how you present the user interface to your users. In this section, you will be putting to use your newly acquired knowledge of assemblies and AppDomains to create a real-world example and to do something fun with the new technology.

Building Application Plug-Ins

This next section will show you how to take all the techniques that you've learned throughout this chapter and put them to use in a real-world application: plug-ins.

If you've been using any computer over the past few years, you probably know about plug-ins. They enable you to play games with your friends over an instant messenger application; they make it possible for you to change the look and feel of your favorite media player; and they expand the functionality of existing applications by adding new menu items, tools, features, windows, and more. Users love plug-ins because they get to have access to the core features of an application, and they are in control over which extras they put in their application. In addition to being great for end users, plug-ins provide developers with the ability to create add-ins to the other applications with shortened development time. An application that supports plug-ins will probably have a much stronger chance of building a thriving user community than applications that don't.

To create your plug-in application, create a new Visual Studio .NET solution called Plugins. In this solution, create a class library called PluginAPI, a console application called PluginTester, and another class library called SamplePlugin. To make the console application the startup project, you can create that one first.

Most plug-in applications work on the assumption that when a third-party developer creates a plug-in, that developer abides by some pre-established rules. In most languages, those rules are defined by interfaces. Our plug-in application will be no different.

Every plug-in produced for the sample application will implement the IPlugin interface. This allows the host application (the application that locates and launches plug-in modules) to easily identify plug-ins and treat them all identically, regardless of who created the plug-in or what functionality it provides.

Vendors often produce a suite of plug-ins that provide some logically related functionality and want to bundle them all in the same assembly. This example will allow for this as well, assuming that a given assembly can contain one or more plug-in classes.

The code here won't get too complex; otherwise the details of the code might bog you down and prevent you from learning the key points of the chapter. As a result, this plug-in is going to do only one thing: identify itself. After you've learned how this plug-in framework functions, you can easily add your own functionality that suits your application.

Every plug-in framework should start with the development API or SDK. Application vendors that support plug-ins will typically distribute code that enables the creation of plug-ins. In this case, all we need is the IPlugin interface. Go ahead and create the IPlugin interface in the PluginAPI project shown in Listing 12.6.

Listing 12.6. The IPlugin Interface
 using System; namespace SAMS.CSharpUnleashed.Plugins.API {   /// <summary>   /// Summary description for IPlugin.   /// </summary>   public interface IPlugin   {     string PluginName { get; }   } } 

Pretty simple, right? All that's been done is indicate that every plug-in that implements the IPlugin interface must have a property called PluginName that supports at least a get-accessor.

The next step on the way to creating a plug-in application is to create the plug-in itself. Given the simple interface just shown, the plug-ins should be fairly short. To make sure that the solution can support multiple plug-ins, create two: a blue plug-in and a red plug-in.

Add a reference to the PluginAPI project from the SamplePlugin project. This is what a third-party developer would do when creating a plug-in for your application.

Now take a look at the code in Listing 12.7 for the BluePlugin and RedPlugin classes (BluePlugin.cs and RedPlugin.cs, respectively).

Listing 12.7. The RedPlugin and BluePlugin Sample Plug-In Classes
 using System; using System.Runtime.Remoting; using System.Reflection; using SAMS.CSharpUnleashed.Plugins.API; namespace SAMS.CSharpUnleashed.SamplePlugin {   /// <summary>   /// Summary description for BluePlugin.   /// </summary>   public class BluePlugin : MarshalByRefObject, IPlugin    {     public BluePlugin()     {          }          #region IPlugin Members     public string PluginName     {       get       {        return PluginTool.GetResourceString( "BluePlugin" );       }     }     #endregion   } } using System; using System.Runtime.Remoting; using System.Reflection; using SAMS.CSharpUnleashed.Plugins.API; namespace SAMS.CSharpUnleashed.SamplePlugin {   /// <summary>   /// Summary description for RedPlugin.   /// </summary>   public class RedPlugin : MarshalByRefObject, IPlugin   {     public RedPlugin()     {          }     #region IPlugin Members     public string PluginName     {       get       {         return PluginTool.GetResourceString( "RedPlugin" );       }     }     #endregion   } } 

Obviously these won't compile yet because you haven't yet written the PluginTool class. Given the previous examples in this chapter involving resources, you can probably guess what the GeTResourceString method does. The PluginTool class (also part of the SamplePlugin project) is shown in Listing 12.8.

Listing 12.8. The PluginTool Class
 using System; using System.Reflection; using System.Resources; namespace SAMS.CSharpUnleashed.SamplePlugin {   /// <summary>   /// Summary description for PluginTool.   /// </summary>   internal class PluginTool   {     internal static string GetResourceString( string id )     {       ResourceManager rm = new ResourceManager(         "SAMS.CSharpUnleashed.SamplePlugin.Strings",         Assembly.GetExecutingAssembly());       return rm.GetString( id );      }   } } 

Create the Strings.resx file by adding it to the SamplePlugin project and create two named strings: RedPlugin and BluePlugin. The values used in this sample were This is the Red Plugin and This is the Blue Plugin, respectively, but you can use anything you like. Feel free to get more complicated and make your plug-in identify itself in more than one language using code from earlier in the chapter as a reference.

Now you should be ready to write the code that would appear in the host application. This code will iterate through all the files in a given directory (probably a directory called Plugins or something similar) and retrieve a list of all the plug-ins available in all the files in that directory.

Before you write that code, there is a small lesson to be learned. Earlier in the chapter, it was mentioned that after you've loaded an assembly, it cannot be unloaded until its containing AppDomain is unloaded. For applications that have only one AppDomain, this means that the assembly will remain loaded until the application is shut down.

For many applications, this isn't a problem. However, a plug-in application needs to iterate through many, many assemblies to obtain the list of available plug-ins, and over the course of an execution can launch many of those plug-ins. If the application could not release the memory associated with iterating through the available plug-ins or with launched plug-in modules, it would become bloated very quickly and would be operating very inefficiently.

One way around this is to do all the work of assembly loading related to plug-ins in a completely separate AppDomain. You've already seen how to create a new AppDomain and instantiate a type into that AppDomain. In that case, the type will have a method that loads a list of identified plug-ins from a given directory. When that method is done, the calling code can simply unload the temporary AppDomain and the memory associated with the search operation will be released.

Add a new class called PluginLoader to the PluginTester console application. It is shown in Listing 12.9.

Listing 12.9. The PluginLoader Class
 using System; using System.Runtime.Remoting; using System.Reflection; using System.IO; using System.Collections; using SAMS.CSharpUnleashed.Plugins.API; namespace PluginTester {   /// <summary>   /// Summary description for PluginLoader.   /// </summary>   public class PluginLoader : MarshalByRefObject   {     public ArrayList GetPluginsFromDirectory( string dir )     {       Console.WriteLine(         "About to search for Plugins from within the AppDomain {0}\n",          AppDomain.CurrentDomain.FriendlyName);       ArrayList pluginNames = new ArrayList();       string[] files = System.IO.Directory.GetFiles( dir, "*.dll");       foreach (string filename in files)       {         Assembly pluginModule = Assembly.LoadFrom(filename);         foreach (Type t in pluginModule.GetTypes())         {            Type iface =               t.GetInterface("SAMS.CSharpUnleashed.Plugins.API.IPlugin");            if (iface != null)            {              IPlugin plug =                 (IPlugin)pluginModule.CreateInstance( t.FullName );              pluginNames.Add( plug.PluginName );            }         }      }     return pluginNames;   }  } } 

There's quite a bit going on in the preceding code. The first step is to grab the list of all the DLL files in the directory and iterate through them.

TIP

Although the code used a file that ends with the .DLL extension, you can rename the file anything you like, such as SamplePlugin.plg or SamplePlugin.plugin. An advantage to that naming convention is that with a unique file extension, you can associate an action and an icon with it, allowing your application to be launched when someone double-clicks or downloads a plug-in module.


With the filename in hand, the code can create an assembly from that filename. Remember that Assembly.LoadFrom not only creates an assembly, but it also loads that assembly into the current AppDomain. With the Assembly reference, the code then iterates through all the types stored in that assembly, looking for any type that implements the IPlugin interface. Your code has to iterate through because the programmers can call their classes anything they like, with any namespace they like. Because of that, you have to explicitly search for classes that implement IPlugin. It might be a little extra effort, but the result is an extremely flexible and reliable plug-in solution.

Finally, after the code has found a class that implements IPlugin, it creates an instance of that class in the current domain and then simply adds the plug-in module's name to the output ArrayList.

The last thing you need to do is wrap the GetPluginsFromDirectory method call in its own AppDomain to enable the application to control the allocation of memory. You can do this in your own class if you like or you can even do it with an overload of GetPluginsFromDirectory that asks for the name of an AppDomain as well as a directory. For this sample, the code was just put in Class1.cs in PluginTester as shown in Listing 12.10.

Listing 12.10. The PluginTester Console Application
 using System; using System.Runtime.Remoting; using System.Collections; using System.Reflection; using SAMS.CSharpUnleashed.Plugins.API; namespace PluginTester {   /// <summary>   /// Summary description for Class1.   /// </summary>   class Class1   {     /// <summary>     /// The main entry point for the application.     /// </summary>     [STAThread]     static void Main(string[] args)     {       AppDomain ad = AppDomain.CreateDomain("PluginLoader");       ObjectHandle handle = ad.CreateInstance("PluginTester",          "PluginTester.PluginLoader");       PluginLoader pl = (PluginLoader)handle.Unwrap();       ArrayList pluginNames = pl.GetPluginsFromDirectory(@"C:\Plugins");       AppDomain.Unload(ad);       foreach (string name in pluginNames)       {         Console.WriteLine("Plugin Located: {0}", name );       }       Console.ReadLine();     }   } } 

The preceding code creates a new AppDomain called PluginLoader. Then, just as was done earlier, it creates an ObjectHandle from a type within an assembly and unwraps it to create an instance of the PluginLoader class in the secondary AppDomain. After the PluginLoader has been instantiated in the other domain (it must be a MarshalByRefObject due to remoting, which you'll learn about later in the book), you can simply invoke the method and then dispose of the domain via AppDomain.Unload().

The output of running this console application looks something like the following:

 About to search for Plugins from within the AppDomain PluginLoader Plugin Located: The Blue Plugin Plugin Located: The Red Plugin 

It might look like a lot of work, but creating a framework like this has quite a few places where you can build very reusable and scalable code and use it for multiple projects. The benefit of enabling your application to work with third-party plug-ins can easily outweigh any complexity in creating the host application and the plug-in API.



    Visual C#. NET 2003 Unleashed
    Visual C#. NET 2003 Unleashed
    ISBN: 672326760
    EAN: N/A
    Year: 2003
    Pages: 316

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net