.NET

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

By Gigi and Saar Sayfan, June 03, 2011

Using .NET 4.0's MEF to facilitate a plugin architecture turns out to be fairly easy. So is embedding IronPython. Doing both has its challenges

Graphr is a Windows Presentation Foundation-based GUI program implemented in C# that allows you to generate a dataset based on a rule (expression) such as "(x-3) * (x-5)" for a given range of values. Graphr then displays a graph on the screen. Graphr is interesting because it is a polyglot application. It is a C# application that hosts an embedded IronPython engine. It allows you to write your rules in Python and even use any function from Python's math library, such as sin() or cos(). The best part is that you don't need to parse and evaluate the expression. Python will do this for you. The other interesting aspect of Graphr is its architecture. Graphr is a plugin-based system. It utilizes the Managed Extensibility Framework (MEF) to load graph plugins dynamically and use them in a totally decoupled fashion. You can add additional graphs and Graphr will be happy to automatically use them without touching Graphr itself. This is a useful practice that can be applied in many situations.

In this article, we will demonstrate Graphr, explain in detail how to embed IronPython in any application, and explore MEF and its use in a plug­in architecture. In a separate article, which you can access immediately here, we've done a walkthrough of Graphr's architecture. Finally, we'll talk a little about the future of Graphr.

Graphr in Action

Before we delve into the code, let's have some fun playing with Graphr. You need Visual Studio 2010 and IronPython 2.7. Both are free (Visual Studio 2010 has non-free versions, too, but they are not needed to build Graphr).

When you launch Graphr, you see a window divided into two panes. The left pane contains the rule and range text boxes and a "Visualize" button on top, and a graph selector and graph properties on the bottom. The right pane (empty initially) contains the graph display area. Click the "Visualize" button and a yellow line graph is displayed (Figure 1).

[Click image to view at full size]

Figure 1.

The graph properties area allows you to customize the appearance of the graph. The LineColor text box contains four double-digit hex numbers (A, R, G, B). The first two digits control transparency (00 fully transparent, FF fully opaque), the other digits control the red, green, and blue values of the color. The LineThickness text box controls the thickness of the line.

You can also change the rule and the range. When you change the rule and/or the range, you must click the "Visualize" button again. The graph selector dropdown box allows you to select a different graph type. Figure 2 shows a bar graph that uses a different rule and a range that includes negative numbers. Note that each graph type has its own graph properties.
The bar graph uses a gradient brush to paint the bars, so the bar color changes gradually from the selected color at the top to white at the bottom of the bar.

[Click image to view at full size]

Figure 2.

Graphr is also fully resizable. You can resize the entire window and the graph will adjust and fit into the new size. You can also change the ratio between the left and right pane by dragging the splitter left and right.

Graphr also supports a pie graph, which displays slices that are proportionate to the values of the rule. The color of each slice is selected randomly, and you can only control the border color. If you play with the pie graph, it is recommended that you use a range of up to 50 values because it takes longer to render (especially if you resize the window).

Embedding IronPython

Graphr is able to parse and evaluate complicated expressions. Implementing a custom parser and evaluator in C# is possible, but would take a long time. Instead, we opted to take advantage of Python, which provides out of the box dynamic evaluation capability.

IronPython is an open source implementation of Python for the .NET platform. IronPython 2.7, which is what we used in Graphr can be downloaded here.

Dynamic Evaluation with IronPython

IronPython makes it easy to dynamically evaluate any valid Python expression and, in particular, mathematical expressions. The key is the built-in eval() function, which takes a Python expression as a string, executes it dynamically, and returns the resulting Python object. Here is a short interactive session that demonstrates it:

Graphr needs to evaluate an expression that contains an xvariable multiple times, where x is assigned different values from the range. The Evaluatr.py Python module provides this functionality. It imports all the functions in Python's math library so they can be evaluated as part of the expression. It contains two functions: evaluateSingleValue() and evaluate(). The evaluateSingleValue() function gets an expression (rule) and an x value. It substitutes all the x's in the expression with the value of x and evaluates it, as shown here:

The evaluate() function is used directly by Graphr. It accepts an expression and a list of values. It iterates over the values and evaluates each one using evaluateSingleValue(). Note that evaluate() is a Python generator, which means that values are computed only as they are needed. The yield keyword returns control to the caller, which allows efficient streaming in case of large ranges that don't need to be fully evaluated (such as a graph that displays a window of 100 values regardless of the entire range), as shown next:

def evaluate(expr, values):
"""Evaluate an expression with an X variable over a range of X values
expr - a string that represents a Python expression with a variable
values - a list of floating point values
return a generator that yields pairs of (x, eval(expr))
"""
expr = expr.lower()
for x in values:
try:
yield (x, evaluateSingleValue(expr, x))
except:
yield (None, None)

Embedding IronPython in a C# Program

Now, that we have a good Python evaluator that can dynamically evaluate Python expressions, it's time to embed it in your C# program. IronPython is built on top of the DLR (Dynamic Language Runtime). The DLR exposes hosting APIs designed to support this use case. We isolated all embedding code into a single C# class called IronPythonEvaluator. The IronPythonEvaluator relies on the Evaluatr.py module to perform the actual computation. This raises the question of how to deploy the Evaluatr.py module with the Graphr app. The most straightforward approach is just to provide it as an additional file, but we chose a more integrated approach where the source code for the module is embedded as a resource in the Graphr.exe assembly (Figure 3).

[Click image to view at full size]

Figure 3.

When the IronPythonEvaluator is initialized, it loads the resource and saves it to a file. This file is used later by the IronPython runtime engine. Before we dive into the IronPythonCalculator, here are the assemblies it uses:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
// The following assemblies are needed to host IronPython
using IronPython.Hosting;
using Microsoft.Scripting.Hosting;
using Graphr.Properties;
using System.IO;

The IronPythonEvaluator has a constructor and two overloaded evaluate() methods. The constructor creates an IronPython engine with a "Debug" option. This is useful because it allows stepping from C# into IronPython code when debugging in VisualStudio. Also, the debugger will present you with a unified call stack from C# to Python if an exception is raised in the Python code (Figure 4). This polyglot debugging is a unique feature of the .NET platform.

[Click image to view at full size]

Figure 4.

The constructor then adds the IronPython Lib directory to the engine's search path. That allows IronPython to import any standard library module. The next step is to write the embedded Evaluatr.py module to a temporary file, then load this file into the engine's runtime and assign the resulting Python module into the evaluatr dynamic member. Once the file has been loaded into the runtime, it can be deleted. Note, that this code is not industrial strength and doesn't handle exceptions so the temporary file will not be deleted if an exception is thrown.

It accepts a mathematical expression as a string and a list of doubles (the X values). It returns a list of pairs of doubles (the X,Y pairs).
The evaluate() method starts by declaring an empty result list and calling the evaluate method of the embedded IronPython evaluatr module:

The return value r is a dynamic object, which actually contains an IronPython.Runtime.PythonGenerator object. This is not the type of object we want to expose to the rest of the program (the idea is to hide the implementation). So, we iterate over the generator and push every pair to the result list.

If the expression is invalid, then an IronPython exception is raised (in Python, you raise exceptions instead of throwing them), and we translate that to a C# exception, which we then throw, as shown next:

Managed Extension Framework (MEF)

MEF was released as part of the .NET framework 4 and Silverlight 4. It provides a standard way for applications to discover and load extensions and to expose application services to extensions. In addition, the application extensions may depend on each other.

The main parts of MEF are the catalog and the composition container. The catalog is responsible for locating composable parts, and the container is responsible for creating parts and their dependencies (which are other parts). Parts have import and export contracts and they interact with other parts using these contracts.
MEF provides an attribute-based programming model, which is very easy to use and yet provides good error messages when things go wrong. For further MEF information, we recommend http://mef.codeplex.com/documentation.

Plugin-based Applications

We prefer the term "plugin" instead of "application extension," so that's what we'll use from now on. My definition of a plugin is a piece of code that has an interface and is loaded by some application dynamically at runtime; then, the application interacts with the plugin through the interface. The application may provide certain services to the plugin though its interface.

The application and the plugins are loosely coupled and interact through interfaces. The application discovers and loads the plugin through some standard mechanism that (often) can be reused by other applications. We usually prefer to have a well-known directory that contains plugins. Developers and/or administrators may add or remove plugins from this directory. The benefit of such a programming model is that it is very easy to evolve an application and develop major parts of its functionality in isolated plugins that are easy to test and deploy and don't require modifying the application code itself. It also makes it very easy to create custom apps with different functionality (just pick a custom set of plugins). Finally, application startup time can be dramatically reduced if plugins are loaded only when they are needed.

Graphr is a plugin-based application. It has a MainWindow class that's responsible for interacting with the user, the embedded IronPython evaluator, and configuring the active graph properties. It has a GraphManager class that's responsible for loading the graph plugins and managing the active graph. And it has graph plugins that are responsible for displaying the current graph data.

All the components interact through interfaces and promote decoupling. The interfaces are defined in their own assembly (Graphr.Contract) and are referenced by Graphr itself and by each plugin. Graphr and the plugins don't reference each other. Their main interface is IGraph. There is an additional interface, IGraphMetadata, that is used by MEF.

The IGraph interface

The IGraph interface has three methods: PopulateCanvas(), DoGraphLayout(), and ConfigSpec.

The PopulateCanvas() method is called whenever the data is modified. It accepts a Canvas object (where the graph is displayed), the data as a list of x,y values, and a config object, which is a dictionary of graph specific items like color and stroke thickness. The graph plugin that implements the interface should construct the visual elements that represent the current data and the graph properties. For example, a bar graph will create a bar object for each x,y pair,and set its border color and fill color based on the graph properties.

The DoGraphLayout() method is called every time the graph needs to be redrawn. This happens whenever the canvas is resized (when the whole window is resized or the splitter between the left and right pane is moved). It accepts the canvas and the data. You could argue that the plugin could store the canvas and the data when PopulateCanvas() is called  then the DoGraphLayout() method could be simpler with no arguments. This is true, but since the MainWindow that calls DoGraphLayout() must have the canvas and data anyway, it is better not to store the same information in each plugin, too.

The ConfigSpec property is a dictionary that maps names to type and object. Each graph plugin supports a different set of named configuration items of different types. A dictionary of typed objects is a generic way to represent this set. The ConfigSpec contains the initial values of these items (e.g., StrokeThickness = 2). The graph properties pane displays these initial values and the user may modify them and change the appearance of the displayed graph.
Here is the entire IGraph interface:

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.

Video

This month's Dr. Dobb's Journal

This month,
Dr. Dobb's Journal is devoted to mobile programming. We introduce you to Apple's new Swift programming language, discuss the perils of being the third-most-popular mobile platform, revisit SQLite on Android
, and much more!