shortcuts to the canned python scripts defined in RevitPythonShell.xml to the
Revit RibbonPanel. I'd like to share some of the fancy magic that went into r65.
RevitPythonShell lets you define a list of named scripts in the
RevitPythonShell.xml file. An exerpt from my personal file looks like this:
<Commands>
<!-- a list of preconfigured commands -->
<Command name="dumpdpvfolder" src="C:\RevitPythonShell\Commands\dpvdumpfolder.py"/>
<Command name="read model" src="C:\RevitPythonShell\Commands\readmodel.py"/>
<Command name="Report" src="C:\RevitPythonShell\Commands\report.py"/>
<Command name="View Model" src="C:\RevitPythonShell\Commands\viewmodel.py"/>
</Commands>
These are commands I use all the time during development of the
DesignPerformanceViewer (DPV). I use these commands to test parts of the DPV, run
automated tests on lots of input projects etc.
So far, these commands where only displayed in a toolbar above the interactive
shell. On the RibbonPanel provided by Autodesk Revit Architecture 2010 only a
single button was displayed: Open Python Shell. The main reason being: To add
an item to the RibbonPanel, you have to provide a path to an assembly and the
name of a type inside that assembly implementing IExternalCommand.
In the case of RevitPythonShell, we can't specify such an assembly. There is none.
And we can't use a generic type as the IExternalCommand and load the python script
in the Execute() method either, since the arguments to Execute don't contain any hints as
to which button was clicked!
This is where dynamic assemblies and Reflecion.Emit enters the stage: When RevitPythonShell
starts up (IExternalCommand.OnStartup), the list of canned commands is read and a dynamic assembly
is created to hold a dynamically created class for each canned command defined in RevitPythonShell.xml.
This is done with Reflection.Emit in the method RevitPythonShellApplication.CreateCommandLoaderAssembly():
/// <summary>
/// Creates a dynamic assembly that contains types for starting the canned commands.
/// </summary>
private static void CreateCommandLoaderAssembly()
{
var assemblyName = new AssemblyName {Name = "CommandLoaderAssembly", Version = new Version(1, 0, 0, 0)};
var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("CommandLoaderModule", "CommandLoaderAssembly.dll");
foreach (var command in GetCommands())
{
var typebuilder = moduleBuilder.DefineType("Command" + command.Index,
TypeAttributes.Class | TypeAttributes.Public,
typeof(CommandLoaderBase));
var ci = typeof(CommandLoaderBase).GetConstructor(new[] { typeof(string) });
var constructorBuilder = typebuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[0]);
var gen = constructorBuilder.GetILGenerator();
gen.Emit(OpCodes.Ldarg_0); // Load "this" onto eval stack
gen.Emit(OpCodes.Ldstr, command.Source); // Load the path to the command as a string onto stack
gen.Emit(OpCodes.Call, ci); // call base constructor (consumes "this" and the string)
gen.Emit(OpCodes.Nop); // Fill some space - this is how it is generated for equivalent C# code
gen.Emit(OpCodes.Nop);
gen.Emit(OpCodes.Nop);
gen.Emit(OpCodes.Ret); // return from constructor
typebuilder.CreateType();
}
assemblyBuilder.Save("CommandLoaderAssembly.dll");
}
Now, this is the first time I have ever emitted IL in my life. Naturally, I wanted to get as much done in C# as possible, so the types
generated all inherit CommandLoaderBase:
/// <summary>
/// Starts up a ScriptOutput window for a given canned command.
///
/// It is expected that this will be inherited by dynamic types that have the field
/// _scriptSource set to point to a python file that will be executed in the constructor.
/// </summary>
public abstract class CommandLoaderBase: IExternalCommand
{
protected string _scriptSource = "";
public CommandLoaderBase(string scriptSource)
{
_scriptSource = scriptSource;
}
public IExternalCommand.Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
var executor = new ScriptExecutor(commandData, message, elements);
string source;
using (var reader = File.OpenText(_scriptSource))
{
source = reader.ReadToEnd();
}
var result = executor.ExecuteScript(source);
message = executor.Message;
switch (result)
{
case (int)IExternalCommand.Result.Succeeded:
return IExternalCommand.Result.Succeeded;
case (int)IExternalCommand.Result.Cancelled:
return IExternalCommand.Result.Cancelled;
case (int)IExternalCommand.Result.Failed:
return IExternalCommand.Result.Failed;
default:
return IExternalCommand.Result.Succeeded;
}
}
}
This base class has a constructor that accepts the path to a python script and
creates a ScriptExecutor (check RevitPythonShell source code) to execute that script in its implementation of
IExternalCommand.Execute().
And now the Reflection/IL stuff starts to make sense: Create a type inheriting from CommandLoaderBase, adding a parameterless constructor (called by
Revit) which in turn calls the base constructor with the path to the script. The resulting IL looks something like this:
.class public auto ansi Command0
extends [RevitPythonShell]RevitPythonShell.CommandLoaderBase
{
.method public specialname rtspecialname instance void .ctor() cil managed
{
.maxstack 2
L_0000: ldarg.0
L_0001: ldstr "C:\\RevitPythonShell\\Commands\\dpvdumpfolder.py"
L_0006: call instance void [RevitPythonShell]RevitPythonShell.CommandLoaderBase::.ctor(string)
L_000b: nop
L_000c: nop
L_000d: nop
L_000e: ret
}
}
With "Command0" being the name of the type to instantiate for the first canned script in RevitPythonShell.xml. The C# equivalent looks like this:
public class Command0 : CommandLoaderBase
{
// Methods
public Command0() : base(@"C:\RevitPythonShell\Commands\dpvdumpfolder.py")
{
}
}
I used Red Gate's .NET Reflector to disassemble the resulting assembly and also as a hint on how to write the Reflection.Emit stuff (created a dummy subclass of CommandLoaderBase with the desired constructor, compiled and then disassembled in Reflector).
Now I need to add a nice interface to manage the canned commands - any ideas?
No comments:
Post a Comment