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:
- Implement IGraph
- Have an [Export] attribute with a type of IGraph
- 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 composeParts() 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).



