Thanks to all of your interest in this recent post, which looked at a way to interface an out-of-process .NET application with an assembly running in-process to AutoCAD. After some obvious functionality gaps were raised, Renze de Waal, one of our ADN members, pointed out a DevNote on the ADN website covering – and more completely addressing – this topic. Shame on me for not checking there before writing the post. Anyway, onwards and upwards…
The information in the DevNote highlights some of the problems I and other people had hit with my previous code, mostly related to the fact it wasn’t executed on the main AutoCAD thread (which meant we were effectively limited in the interactions we had with the AutoCAD application).
To fix this we can derive our application from System.EnterpriseServices.ServicedComponent (also adding an additional project reference to the System.EnterpriseServices .NET assembly). Here is the updated C# code for the LoadableComponent:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using System.Runtime.InteropServices;
using System.EnterpriseServices;
namespace LoadableComponent
{
[Guid("5B5B731C-B37A-4aa2-8E50-42192BD51B17")]
public interface INumberAddition
{
[DispId(1)]
string AddNumbers(int arg1, double arg2);
}
[ProgId("LoadableComponent.Commands"),
Guid("44D8782B-3F60-4cae-B14D-FA060E8A4D01"),
ClassInterface(ClassInterfaceType.None)]
public class Commands : ServicedComponent, INumberAddition
{
// A simple test command, just to see that commands
// are loaded properly from the assembly
[CommandMethod("MYCOMMAND")]
static 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
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
ed.WriteMessage(
"\nAdd numbers called with {0} and {1}.",
arg1, arg2
);
// 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();
}
}
}
Some points to note...
- We now use an interface to expose functionality from our component, which allows us more flexibility in the way we return data to the calling application.
- We're labeling our interface and component with specific GUIDs - generated by guidgen.exe - although we could probably skip this step.
- We're now able to use the MdiActiveDocument property safely, as well as being able to write messages via the editor.
When we build the component we can - as before - register it via the regasm.exe tool. Here's the .reg output if you specify the /regfile option:
REGEDIT4
[HKEY_CLASSES_ROOT\LoadableComponent.Commands]
@="LoadableComponent.Commands"
[HKEY_CLASSES_ROOT\LoadableComponent.Commands\CLSID]
@="{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}"
[HKEY_CLASSES_ROOT\CLSID\{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}]
@="LoadableComponent.Commands"
[HKEY_CLASSES_ROOT\CLSID\{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}\InprocServer32]
@="mscoree.dll"
"ThreadingModel"="Both"
"Class"="LoadableComponent.Commands"
"Assembly"="LoadableComponent, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
"RuntimeVersion"="v2.0.50727"
[HKEY_CLASSES_ROOT\CLSID\{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}\InprocServer32\1.0.0.0]
"Class"="LoadableComponent.Commands"
"Assembly"="LoadableComponent, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
"RuntimeVersion"="v2.0.50727"
[HKEY_CLASSES_ROOT\CLSID\{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}\ProgId]
@="LoadableComponent.Commands"
[HKEY_CLASSES_ROOT\CLSID\{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}\Implemented Categories\{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}]
One thing to mention - I found that the calling application was not able to cast the returned System.__COMObject to LoadableComponent.INumberAddition unless I updated the project settings to "Register from COM Interop" (near the bottom of the Build tab).
Now for our calling application… here’s the updated C# code:
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;
INumberAddition app =
(INumberAddition)acApp.GetInterfaceObject(
"LoadableComponent.Commands"
);
// Now let's call our method
string res = app.AddNumbers(5, 6.3);
acApp.ZoomAll();
MessageBox.Show(
this,
"AddNumbers returned: " + res
);
}
catch (Exception ex)
{
MessageBox.Show(
this,
"Problem executing component: " +
ex.Message
);
}
}
}
}
}
You should be able to see straightaway that it’s simpler – we cast the results of the GetInterfaceObject call to our interface and call the AddNumbers method on it.
And when we execute the code, we can see we’re now able to write to the command-line, as well as getting better results from our ZoomAll():