This question came in recently by email from Michael Fichter of Superstructures Engineers and Architects:
Could you suggest an approach that would enable me to drive a .NET function (via COM) that could return a value from .NET back to COM? I have used SendCommand in certain instances where return values were not needed.
Michael’s referring to a technique used in this previous post, which shows how to launch AutoCAD from a .NET executable via COM and then launch a command which can then safely interface with AutoCAD in-process via its managed API.
And yes, this technique is fine if you don’t want to return results, but has limitations if you do. You could populate AutoCAD user variables or create a file for the calling application to read but such approaches are cumbersome.
So… in spite of my initial doubtful reaction I decided to give it a try. Here are the steps I used to get this working…
First we create a Class Library for our in-process component with references to the usual acmgd.dll and acdbmgd.dll assemblies, adding the following C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using System.Runtime.InteropServices;
namespace LoadableComponent
{
[ProgId("LoadableComponent.Commands")]
public class Commands
{
// A simple test command, just to see that commands
// are loaded properly from the assembly
[CommandMethod("MYCOMMAND")]
public void MyCommand()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
ed.WriteMessage("\nTest command executed.");
}
// A function to add two numbers and create a
// circle of that radius. It returns a string
// withthe result of the addition, just to use
// a different return type
public string AddNumbers(int arg1, double arg2)
{
// During tests it proved unreliable to rely
// on DocumentManager.MdiActiveDocument
// (which was null) so we will go from the
// HostApplicationServices' WorkingDatabase
Database db =
HostApplicationServices.WorkingDatabase;
Document doc =
Application.DocumentManager.GetDocument(db);
// Perform our addition
double res = arg1 + arg2;
// Lock the document before we access it
DocumentLock loc = doc.LockDocument();
using (loc)
{
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
// Create our circle
Circle cir =
new Circle(
new Point3d(0, 0, 0),
new Vector3d(0, 0, 1),
res
);
cir.SetDatabaseDefaults(db);
// Add it to the current space
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
db.CurrentSpaceId,
OpenMode.ForWrite
);
btr.AppendEntity(cir);
tr.AddNewlyCreatedDBObject(cir, true);
// Commit the transaction
tr.Commit();
}
}
// Return our string result
return res.ToString();
}
}
}
You’ll see we mark our Commands class as having the ProgId of “LoadableComponent.Commands” (this doesn’t have to follow the namespace.class-name convention, if you’d rather use something else). Be sure to edit the AssemblyInfo.cs file to make sure the ComVisible assembly attribute is set to true (the default is false), otherwise no classes will be exposed via COM.
The code is mostly pretty simple… it includes a command just to make sure commands are registered when the assembly loads. The AddNumbers() function uses a slightly different technique to get the working database and its document, mainly because I found MdiActiveDocument to be null when I needed it. I suspect this is simply a timing issue, and that if AutoCAD had the time to fully initialize we wouldn’t have to code this defensively. There may well be a clean way to wait for this to happen (comments, anyone?).
Once we’ve built the assembly it needs to be registered via COM. The way I tend to do this is via a “Visual Studio Command Prompt” (which has the path set nicely to call the VS development tools). I browse to the location of my assembly and then run “regasm LoadableComponent.dll” (you can specify the optional /reg parameter if you’d rather create a .reg file rather than modifying the Registry directly).
Now we can create an executable project to drive this component with COM references to the AutoCAD Type Library (I’m using the one for AutoCAD 2010) and the AutoCAD/ObjectDBX Common Type Library (AutoCAD 2010’s is version 18.0), as well as a reference to our .NET assembly (which I have called LoadableComponent.dll).
Inside the default form created with the executable project we can add a button behind which we copy the code in the post referred to earlier, adding some logic to load our component and dynamically execute its AddNumbers() function:
using Autodesk.AutoCAD.Interop;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Reflection;
using System;
using LoadableComponent;
namespace DrivingAutoCAD
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
const string progID = "AutoCAD.Application.18";
AcadApplication acApp = null;
try
{
acApp =
(AcadApplication)Marshal.GetActiveObject(progID);
}
catch
{
try
{
Type acType =
Type.GetTypeFromProgID(progID);
acApp =
(AcadApplication)Activator.CreateInstance(
acType,
true
);
}
catch
{
MessageBox.Show(
"Cannot create object of type \"" +
progID + "\""
);
}
}
if (acApp != null)
{
try
{
// By the time this is reached AutoCAD is fully
// functional and can be interacted with through code
acApp.Visible = true;
object app =
acApp.GetInterfaceObject("LoadableComponent.Commands");
if (app != null)
{
// Let's generate the arguments to pass in:
// an integer and a double
object[] args = { 5, 6.3 };
// Now let's call our method dynamically
object res =
app.GetType().InvokeMember(
"AddNumbers",
BindingFlags.InvokeMethod,
null,
app,
args
);
acApp.ZoomAll();
MessageBox.Show(
this,
"AddNumbers returned: " + res.ToString()
);
}
}
catch (Exception ex)
{
MessageBox.Show(
this,
"Problem executing component: " +
ex.Message
);
}
}
}
}
}
I decided to try Application.GetInterfaceObject() – the classic way to load an old VB6 ActiveX DLL into AutoCAD from VBA or Visual LISP – to see whether it worked for .NET assemblies that have a ProgId assigned. It not only worked, but the commands contained within the module were registered properly. A nice surprise! :-)
I started by defining an interface in the Class Library to be used in the Executable, but ended up going with a more dynamic approach, using InvokeMember() on the class of the object returned by GetInterfaceObject(). This avoids having to define and cast to the interface but adds a little uncertainty to the operation (as I’ve mentioned a number of times in recent weeks when we start getting dynamic we lose most of the compiler crutches we’ve all become used to :-). I also hit a problem if the command function was declared as static, but presumably that can be resolved with the right arguments to InvokeMember().
When we run this code and select the button, we see a circle get created with the radius of 11.3, the result of adding the integer (5) and double (6.3) we passed to the AddNumbers() function:
The combination of COM for out-of-process control with .NET for in-process power and performance will hopefully be a useful technique for many of you needing to automate AutoCAD from an external executable. Be sure to post comments if any of you have things to share on this topic. Thanks for the question, Michael! :-)
Update:
A much-improved implementation of this application can be found in this post.