Megosztás a következőn keresztül:


Python scripting in MEF-based applications

Extensible applications typically allow developers to extend the application’s functionality by implementing new components. Scriptable applications typically allow power users to either extend or automate the application’s functionality by writing scripts that can be executed in the context of the application. Examples include Visual Studio, which supports VB Script; 3ds Max, which supports Max Script; Maya, which supports Python; Photoshop, which supports Java Script; etc. Application automation via scripts can provide standard procedures within an application, such as exporting a 3D model with the particular parameters required for a particular game, or can automate a specialized but repetitive task, such as inserting a standard comment header at the top of a code file.

Scriptable applications require careful design that programmatically exposes components of the application to scripts. MEF (Managed Extensibility Framework), introduced in .NET 4.0 (under System.ComponentModel.Composition), promotes such a design. Typically, MEF-based applications are compositions of highly cohesive, loosely coupled components. In the approach I describe here, the entire CompositionContainer is exposed to the scripts, and therefore all components of the application are exposed. Scripts are treated as transient entities and query the CompsoitionContainer directly. These days, there are many .NET compatible scripting languages, but in this post I’ll specifically be using Iron Python since hosting it in an application is trivial.

The first thing needed is an interface for components that run scripts:

public interface IScriptingService
{
object RunScript(string script);
}

Eventually, some UI in the application might consume this interface to allow the user to run scripts saved on disk, or perhaps would expose an interactive console within the application. Following is an implementation of this interface that supports Python scripting:

[Export(typeof(IScriptingService))]
internal class PythonScriptingService : IScriptingService
{
[Import]
private CompositionContainer container;

public object RunScript(string script)
{
ScriptEngine pythonEngine = Python.CreateEngine();
ScriptScope scriptScope = pythonEngine.CreateScope();
scriptScope.SetVariable("container", this.container);

ScriptSource scriptSource = pythonEngine.CreateScriptSourceFromString(script);
return scriptSource.Execute(scriptScope);
}
}

Notice that the implementation imports the CompositionContainer. For this to work, the CompositionContainer basically needs to export itself. This can be done imperatively as follows:

var batch = new CompositionBatch();
batch.AddExportedValue<CompositionContainer>(container);
container.Compose(batch);

The PythonScriptingService exposes the CompositionContainer to scripts as a variable named container. Scripts can query the container for exports and use them to extend or automate the application’s functionality. As a simple example, a Python script might look something like this:

import clr
clr.AddReference("SampleApplication")
from SampleApplication import IStatusService
statusService = container.GetExportedValue[IStatusService]()
statusService.ReportStatus("Hello from python!")

If you have multiple implementations of IScriptingService for different scripting languages, it might make sense to add ExportMetadata to each implementation that describes the scripting language supported by the implementation and perhaps the file extension used for that scripting language. This will of course depend on the specific application implementing and exposing these services.

Also worth noting, it might be possible to use a custom MEF programming model to inject components directly into the scripts as opposed to the scripts relying on direct access to the CompositionContainer. This would certainly be more complex and require more code, but it’d be interesting to explore regardless.  If anyone has done something like this or has ideas around this, I’d love to hear about it!

I’ve made some sample code available that demonstrates this approach. The focus is definitely on the Iron Python scripting, so the rest of the application is very simple and rather poorly designed. Feedback is welcome.