Channels ▼
RSS

.NET

Graphr: A Plugin-Based Graphical App in C# Using MEF and Embedded IronPython


The IGraphMetadata Interface

This is a very simple interface that just has a single property called Name:

  public interface IGraphMetadata
  {
    string Name { get; }
  }

The IGraphMetadata Interface

This is a very simple interface that just has a single property called Name:

  public interface IGraphMetadata
  {
    string Name { get; }
  }
  

This is an interface used by MEF to annotate plugins with metadata that can be queried without instantiating the plugin. You will see later how this metadata is used by Graphr.

GraphManager and MEF

GraphManager is a class that encapsulates the plugins and the interaction with MEF and also keeps track of the current active graph plugin. It begins by loading the graph plugins, which are implemented as MEF parts. The GraphManager has the following data member:

[ImportMany]
public IEnumerable<Lazy<IGraph, IGraphMetadata>> Helpers { get; set; }

The Helpers collection is decorated with MEF's [ImportMany] attribute. It is an enumerable collection of Lazy pairs of IGraph and IGraphMetadata. The Lazy<T, Metadata> is an MEF extension of the .NET 4 Lazy<T> template. It allows accessing the metadata without instantiating the plugin itself. In order for a graph plugin to be discoverable this way, it must comply with the following requirements:

  1. Implement IGraph
  2. Have an [Export] attribute with a type of IGraph
  3. Have an [ExportMetadata] attribute with a "Name" property that matches the IGraphMetadata interface.

The following code is a snippet from the LineGraph plugin:

  [ExportMetadata("Name", "Line")]
  [Export(typeof(IGraph))]
  class LineGraph : IGraph
  {
    ...
    

MEF can discover and load all compliant plugins into the Helpers collection. To locate the plugins, we use MEF's DirectoryCatalog. Because we want to scan both the current directory and a "plugins" directory, we use an AggregateCatalog and add both the current directory and the "plugins" directory (if we can find it). Once the catalog is ready, we create a CompositionContainer with the catalog and call its compose­Parts() method. That tells MEF to do its magic: Scan the directories in the catalog, scan each assembly in these directories for types that comply with the graph plugin requirements, and load them (without instantiation) into the Helpers collection.

    public GraphManager(Canvas c, Grid g)
    {
      this.canvas = c;
      this.propertyGrid = g;
 // Discover and load graph plugins via MEF magic. All the plugins
 // will automagically populate the Helpers collection

      var catalog = new AggregateCatalog();
      catalog.Catalogs.Add(new DirectoryCatalog
         (Directory.GetCurrentDirectory()));

      // Find the plugins directory
      var cd = Directory.GetCurrentDirectory();
      var d = cd;
      var pluginsDir = Path.Combine(d, "plugins");
      var root = Directory.GetDirectoryRoot(d);
      while (d != root)
      {
        d = Path.GetDirectoryName(d);
        pluginsDir = Path.Combine(d, "plugins");
        if (Directory.Exists(pluginsDir) && pluginsDir != cd)
        {
          catalog.Catalogs.Add(new DirectoryCatalog(pluginsDir));
          break;
        }
      }
      
      var container = new CompositionContainer(catalog);
      container.ComposeParts(this);
    }

When the user selects a new graph type in the graph selector dropdown box (or when initially loading the default graph type), the GraphManager is responsible for the switch. The SwitchGraphHelper() method is called, which selects from the Helpers collection the graph plugin whose Metadata.Name property (defined in the [ExportMetadata] attribute) matches the name argument. Then it populates the graph properties pane using the current helper's ConfigSpec and repopulates the canvas if there is data, as shown in this code:

    public void SwitchGraphHelper(string name)
    {
      this.helper = 
          Helpers.Single(h => h.Metadata.Name == name).Value;

      PopulateGraphProperties(helper.ConfigSpec);
      if (this.data != null)
      {
        PopulateCanvas(this.data);
      }
    }
    

The PopulateGraphProperties() method populates the graph properties grid in the left pane dynamically based on ConfigSpec:

public void PopulateGraphProperties(IDictionary<string,
             Tuple<Type, object>> configSpec)
    {
      config = new Dictionary<string, object>();
      foreach (var kv in configSpec)
      {
        config.Add(kv.Key, kv.Value.Item2);
      }

      var g = this.propertyGrid;

      g.ColumnDefinitions.Clear();
      g.RowDefinitions.Clear();
      g.Children.Clear();

      // Define the Columns
      ColumnDefinition colDef1 = new ColumnDefinition();
      ColumnDefinition colDef2 = new ColumnDefinition();
      g.ColumnDefinitions.Add(colDef1);
      g.ColumnDefinitions.Add(colDef2);

      int row = 0;
      foreach (var pair in configSpec)
      {
        var s = pair.Value;
        RowDefinition rd = new RowDefinition();
        rd.Height = new GridLength(25);
        g.RowDefinitions.Add(rd);

        var label = new Label();
        label.Content = pair.Key;
        g.Children.Add(label);
        var editor = _createEditor(pair.Key, s.Item1, s.Item2);
        g.Children.Add(editor);

        Grid.SetRow(label, row);
        Grid.SetColumn(label, 0);

        Grid.SetRow(editor, row);
        Grid.SetColumn(editor, 1);

        ++row;
      }
    }

The _createEditor() method creates an editor for the particular item type. The supported types are: string, Color, Int32, and Double. In the current implementation, it is always a TextBox, but in general it can be any UIEllement. Different item types are handled differently. The _createEditor() method attaches a type-specific handler for each item type:


    private UIElement _createEditor(string name, Type t, object v)
    {
      var e = new TextBox() { Name = name, Text = v.ToString() };
      if (t.Name == "string")
       e.TextChanged += 
                 new TextChangedEventHandler(_onTextChanged);
      else if (t.Name == "Color")
       e.TextChanged += 
                 new TextChangedEventHandler(_onColorChanged);
      else if (t.Name == "Int32")
       e.TextChanged += new TextChangedEventHandler(_onIntChanged);
      else if (t.Name == "Double")
     e.TextChanged += 
                 new TextChangedEventHandler(_onDoubleChanged);
      else
      {
        throw new Exception("Unknown type");
      }
      
      return e;
    }
    

Details about Graphr code (unrelated to MEF or IronPython, but including descriptive details about the plugins) are available here. Complete code and build files for the project are available at http://is.gd/blplMd.

Conclusion

Graphr is a fun project that demonstrates the power and ease of use of WPF and explores the polyglot programming world by embedding IronPython in C#. It also shows how simple it is to add plugins to an application with MEF. If you like it, you may find it interesting to further extend it.

Related Links

Using IronRuby in .NET Programs


— Gigi and Saar Sayfan are regular contributors to Dr. Dobb's, most recently authoring a two-part exploration of the PolyArea Project; see (http://drdobbs.com/windows/226700093 and http://drdobbs.com/open-source/227700264).


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video