Monday, November 23, 2009

Playing Nice with Other Assemblies using MEF Catalogs

Looking over the previous three posts, we have been working within a single assembly for managing our extensibility parts.  While this works well if we want to use MEF more as an IoC container, it really limits the extensibility of our application.  In the single assembly model that we've been using, every update would require a new build of the application.  To overcome this limitation, we'll create a new example project that builds on what we've covered and branch it out into multiple assemblies.

Building a Console's Help Information

In the new example, we'll be creating a new console application whose sole purpose is to output help documentation on various fake commands.  We aren't going to be implementing any of the actual command functionality; however, each extension will represent a new command and is responsible for its own help documentation.  Given our scenario, it isn't very practical for an application to do nothing without any extensions.  Because of this, our example will be importing in the various parts from both inside of its own assembly (like we've done previously) as well as assemblies located in a Plug-Ins directory.

Defining Our Project Structure

Given the scenario that we are going to be working on, there are a couple of different ways we can structure our solution.  In an attempt to make this a bit more practical, we'll split out our extensibility contract into a separate assembly.  This assembly we'll reference into our hosting application as well as any external plug-in assemblies so that they aren't directly referencing our executable. While this extra assembly that contains our extensibility contract will be very simple, it also provides a point to branch out with other core functionality in a real world scenario.

For the sake of this example, let's start a new Windows Console application project named MEFExample4. Once the solution was created, we need to add a Class Library Project named MEFExample4.Core.  Below is an image of the possible solution explorer window inside of Visual Studio.


Creating Our Extensibility Contract

Now that we have the project structure established, let's go ahead and add a new Interface to the MEFExample4.Core project named IHelp.  This interface will be used by our various "commands" and define a CommandName and HelpText property.  Below is the code for IHelp.

   1:  namespace MEFExample4.Core
   2:  {
   3:      public interface IHelp
   4:      {
   5:          string CommandName { get; }
   6:          string HelpText { get; }
   7:      }
   8:  }

Building the Hosting Application

Now that we have our contract by means of the MEFExample4.Core.IHelp type, let's create our extensible application that imports multiple instances of IHelp types.  In this first iteration, we'll setup the application in the same way as our initial, simple example from earlier posts. We'll use the AssemblyCatalog which is used to instantiate types of a given assembly, and like our previous examples, will use Reflection to get the currently executed assembly. 

In addition to the catalog we're currently choosing, we need to make sure that we have our collection property to hold our imported parts and decorate it with the ImportMany(typeof(IHelp)) attribute. Once that's in place, the only thing left is to setup our console output for displaying the CommandName and HelpText properties of our imports.  Below is first iteration of our Program.cs file.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.ComponentModel.Composition;
   4:  using System.ComponentModel.Composition.Hosting;
   5:  using System.Reflection;
   6:  using System.Text;
   7:  using MEFExample4.Core;
   9:  namespace MEFExample4
  10:  {
  11:      class Program
  12:      {
  13:          static void Main(string[] args)
  14:          {
  15:              Program p = new Program();
  16:              p.Run();
  17:          }
  19:          [ImportMany("command", typeof(IHelp))]
  20:          public IEnumerable<IHelp> Commands { get; set; }
  22:          void Run()
  23:          {
  24:              Compose();
  26:              foreach(IHelp cmd in Commands)
  27:              {
  28:                  Console.WriteLine(FormatCommandOutput(cmd));
  29:              }
  31:              Console.ReadKey();
  32:          }
  34:          void Compose()
  35:          {
  36:              var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
  37:              var container = new CompositionContainer(catalog);
  38:              container.ComposeParts(this);
  39:          }
  41:          string FormatCommandOutput(IHelp command)
  42:          {
  43:              StringBuilder sb = new StringBuilder();
  44:              sb.AppendLine(command.CommandName);
  45:              sb.AppendLine(string.Empty); //empty line
  47:              List<string> splitText = SplitText(command.HelpText);
  49:              splitText.ForEach(t => sb.AppendLine(t));
  51:              return sb.ToString();
  52:          }
  54:          List<string> SplitText(string text)
  55:          {
  56:              const int LINE_LENGTH = 55;
  57:              int length = text.Length;
  58:              List<string> output = new List<string>();
  60:              for (int i = 0; i * LINE_LENGTH < length; i++)
  61:              {
  62:                  if (i*LINE_LENGTH + LINE_LENGTH <= length)
  63:                  {
  64:                      output.Add(text.Substring(i * LINE_LENGTH, LINE_LENGTH).PadLeft(60, ' '));
  65:                  }
  66:                  else
  67:                  {
  68:                      output.Add(text.Substring(i * LINE_LENGTH).PadLeft(55, ' '));
  69:                  }
  70:              }
  72:              return output;
  73:          }
  74:      }
  75:  }

Writing the First Command

Now that we have our contract and hosting application defined, we need to create our first "command".  We started our application to look inside of it's own assembly for our various types that will be imported.  Because of this, we need to add a new class called TestCommand and have it implement our IHelp interface.  Let's set the CommandName property to "Test" and the HelpText property to some random text.  Once we decorate the class with our Export(typeof(IHelp)) attribute, we can compile and run the application and see our text.


Preparing the Application For External Plug-ins

We've tested our application in the same fashion as the previous posts in this series and we're almost ready to work with external assemblies.  Before we do that, we need to add a new folder to our console application project named Plug-Ins.  This folder will be our holding area for the external assemblies that house additional "commands". 

The second thing we need to do is change our AssemblyCatalog to a DirectoryCatalog object.  Unlike an AssemblyCatalog which accepts an assembly as a constructor parameter, the DirectoryCatalog accepts a file path (relative or absolute) to the location where any number of the assemblies are found.  In addition to that difference, the DirectoryCatalog will review all assemblies located in the supplied directory.  This means that once we point our DirectoryCatalog to our Plug-Ins folder, we can just start dropping assemblies into there without having to recompile the console app. Note: Since the project uses the default build location of /bin/Debug, the path we supply will traverse up the directory tree once again. In practice, the Plug-Ins directory should probably be in the same directory as the executable application or the path configurable via an App.config file.  Below is the updated Program.Compose() method.

   1:  void Compose()
   2:  {
   3:      var catalog = new DirectoryCatalog(@"..\..\Plug-ins\");
   4:      var container = new CompositionContainer(catalog);
   5:      container.ComposeParts(this);
   6:  }

Adding Another Assembly

With our folder/repository ready to be used by our DirectoryCatalog, it's time for us to create a new "command" that implements IHelp from a new/separate assembly. Let's add a new class library project called MEFExample4.Commands.  Before we write any code for our new command, let's update the project properties a little bit.  Since we are working on our Commands assembly in the same solution as the application, let's add a Post-Build Event script that will copy our built Commands assembly to the Plug-Ins directory we made in the previous section.  To do this, we need to open the Project's Property window and click on the Build Events tab.  In the Post-Build Event Command Line textbox, we need to add the following:

copy $(TargetPath) $(SolutionDir)\MEFExample4\Plug-ins\$(TargetFileName)

If you are not familiar with Pre/Post-Build events and the macros, feel free to check out this link since they can be used to do some pretty interesting things.  As for the above script, it tells the build manager to copy the TargetPath (the built .dll file) and copy it to the application's Plug-ins directory using the same name.

Now that we have the project setup, we can write the code for our next "command".  We are going to rename Class1.cs to ExampleCommand.cs and add the below code into it.

   1:  using System.ComponentModel.Composition;
   2:  using MEFExample4.Core;
   4:  namespace MEFExample4.Commands
   5:  {
   6:      [Export("command", typeof(IHelp))]
   7:      public class ExampleCommand : IHelp
   8:      {
   9:          private string _helpText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
  10:                                          "Nulla molestie erat rhoncus sapien volutpat rhoncus sed sit " +
  11:                                          "amet dolor. Aliquam rhoncus sem et neque elementum lacinia ut " +
  12:                                          "vel est. Vestibulum et urna sit amet nibh feugiat imperdiet ac " +
  13:                                          "id tellus. Fusce lectus risus, congue et.";
  15:          public string CommandName
  16:          {
  17:              get { return "Example1"; }
  18:          }
  20:          public string HelpText
  21:          {
  22:              get { return _helpText; }
  23:          }
  24:      }
  25:  }

After that, let's build the solution and test everything out.  When we run the application we should now see Example1 and the Ipsum text that we designated for the HelpText property like the image below.


In addition, if we take a look in the Plug-Ins directory, we can also see our MEFExample4.Commands.dll is listed.

Working with Internal and External Assemblies

Now that we've seen how to work with the currently executing assembly as well a directory containing multiple different assemblies, there's one last thing for this post.  What if we wanted to look both internally and externally for our Commands?  An application like this without any native commands is just a shell and provides little initial value.  Sure we can create all of our commands in a separate assembly so we never have to touch the application; however, this allows for our initial commands to be deleted.  In order to cultivate both internal commands and external commands, we need to use an AggregateCatalog object.

An AggregateCatalog differs from an AssemblyCatalog or a DirectoryCatalog in that it's, in essence, a catalog of catalogs.  Looking at the intellisense, we're shown that the AssemblyCatalog constructor accepts either an IEnumerable<ComposablePartCatalog> object or a parameter array of ComposablePartCatalogs.  For the example, we'll use the parameter array version of the constructor to add new instances of an AssemblyCatalog and DirectoryCatalog into our new AggregateCatalog as shown by the updated code below.

   1:  void Compose()
   2:  {
   3:      var catalog = new AggregateCatalog(new DirectoryCatalog(@"..\..\Plug-ins\"),
   4:                                         new AssemblyCatalog(Assembly.GetExecutingAssembly()));
   5:      var container = new CompositionContainer(catalog);
   6:      container.ComposeParts(this);
   7:  }

Once we do this, we can test the application and see that the output has changed to the following:



In this post we really looked at starting a more applicable console application using MEF.  We looked at how you can use a couple different catalogs in order to obtain the assemblies that contain parts for our application.  There are additional catalogs available that allow other ways to import assemblies and types and can also be used with an AggregateCatalog.  In the next post for this series, we'll dive into how MEF handles Circular References real quick and then further expand our example.


kick it on DotNetKicks.comShout it

No comments:

Post a Comment