February 05, 2007

Using .NET reflection with AutoCAD to change object properties - Part 1

I ended up having a fun weekend, in spite of the circumstances. My wife and eldest son were both sick with a cold (which, inevitably enough, I now feel I’m coming down with myself), so we mostly stayed around at home. So I got to spend a little time working on an idea that came to me after my last post.

While the code I provided last time does pretty much exactly what was asked of it (allowing the person who requested it to change the colour of every entity in the modelspace – whether in nested blocks or not – for them to be coloured “by block”), it occurred to me that to be a really useful tool we should go one step further and enable two things:

a) Allow the user to specify the kind of object to apply the change tob) Allow the user to select the specific property that should be changed

What we'd actually end up with, doing this, is a CHPROP on steroids - a command-line interface to provide deep object property modification (down through nested blocks), on any kind of object (as long as it - or one of its ancestor classes - provides a .NET interface). This is cool functionality for people needing to go and massage data, though clearly is potentially quite a scary tool in the wrong hands (thank goodness for the undo mechanism!).

The specific programming problem we need to solve comes down to runtime querying/execution of code and is quite interesting: one that’s easy to solve in LISP (thanks to the combination of the (read) and (eval) functions) and in COM (using type information to call through to IDispatch-declared functions exposed via the v-table), but is almost impossible in C++. With ObjectARX you can use class factories to create instances of objects that were not compiled in (we do this when we load a drawing – we find out the class name of each object being read in, call its static class factory method available in the class’ corresponding AcRxClass object, and pass the information we read in to the dwgInFields() function to resurrect a valid instance of the object). But it’s much harder to query at runtime for a particular method to be called – you could hardcode it for the built-in protocol, but any new objects would cause a problem.

But anyway – all this to say that the way to do it in .NET is to use our old friend Reflection (I love it when a couple of recent topics converge like this, although I wish I could say it was all part of some grand plan… :-)

So, there are four things we actually need to use reflection for in this sample:

That's basically all there is to it, but I'm going to drag this out over a couple of posts, as the code does get quite involved.

This first post is going to focus on the user-input aspect of the code - querying the user for the various bits of information we need, and getting the type & property information using reflection. So it really only looks at the first two items on the above list.

Here's the C# code itself - the main user-input function is called SelectClassPropertyAndValue(), and I've defined a simple TEST command to simply call it and return the results.

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Colors;

using System.Reflection;

namespace PropertyChanger

{

publicclassPropertyChangerCmds

{

[CommandMethod("TEST")]

publicvoid TestInput()

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Editor ed = doc.Editor;

System.Type objType;

string propName;

object newPropValue;

bool recurse;

if (SelectClassPropertyAndValue(

out objType,

out propName,

out newPropValue,

out recurse

)

)

{

ed.WriteMessage(

"\nType selected: " + objType.Name +

"\nProperty selected: " + propName +

"\nValue selected: " + newPropValue +

"\nRecurse chosen: " + recurse

);

}

else

{

ed.WriteMessage(

"\nFunction returned false."

);

}

}

privatebool SelectClassPropertyAndValue(

out System.Type objType,

outstring propName,

outobject newPropValue,

outbool recurse)

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Editor ed = doc.Editor;

objType = null;

propName = "";

newPropValue = null;

recurse = true;

// Let's first get the class to query for

PromptResult ps =

ed.GetString(

"\nEnter type of objects to look for: "

);

if (ps.Status == PromptStatus.OK)

{

string typeName = ps.StringResult;

// Use reflection to get the type from the string

objType =

System.Type.GetType(

typeName,

false, // Do not throw an exception

true// Case-insensitive search

);

// If we didn't find it, try prefixing with

// "Autodesk.AutoCAD.DatabaseServices."

if (objType == null)

{

objType =

System.Type.GetType(

"Autodesk.AutoCAD.DatabaseServices." +

typeName + ", acdbmgd",

false, // Do not throw an exception

true// Case-insensitive search

);

}

if (objType == null)

{

ed.WriteMessage(

"\nType " + typeName + " not found."

);

}

else

{

// If we have a valid type then let's

// first list its writable properties

ListProperties(objType);

// Prompt for a property

ps = ed.GetString(

"\nEnter property to modify: "

);

if (ps.Status == PromptStatus.OK)

{

propName = ps.StringResult;

// Make sure the property exists...

System.Reflection.PropertyInfo propInfo =

objType.GetProperty(propName);

if (propInfo == null)

{

ed.WriteMessage(

"\nProperty " +

propName +

" for type " +

typeName +

" not found."

);

}

else

{

if (!propInfo.CanWrite)

{

ed.WriteMessage(

"\nProperty " +

propName +

" of type " +

typeName +

" is not writable."

);

}

else

{

// If the property is writable...

// ask for the new value

System.Type propType = propInfo.PropertyType;

string prompt =

"\nEnter new value of " +

propName +

" property for all objects of type " +

typeName +

": ";

// Only certain property types are currently

// supported: Int32, Double, String, Boolean

switch (propType.ToString())

{

case"System.Int32":

PromptIntegerResult pir =

ed.GetInteger(prompt);

if (pir.Status == PromptStatus.OK)

newPropValue = pir.Value;

break;

case"System.Double":

PromptDoubleResult pdr =

ed.GetDouble(prompt);

if (pdr.Status == PromptStatus.OK)

newPropValue = pdr.Value;

break;

case"System.String":

PromptResult psr =

ed.GetString(prompt);

if (psr.Status == PromptStatus.OK)

newPropValue = psr.StringResult;

break;

case"System.Boolean":

PromptKeywordOptions pko =

newPromptKeywordOptions(

prompt);

pko.Keywords.Add("True");

pko.Keywords.Add("False");

PromptResult pkr =

ed.GetKeywords(pko);

if (pkr.Status == PromptStatus.OK)

{

if (pkr.StringResult == "True")

newPropValue = true;

else

newPropValue = false;

}

break;

default:

ed.WriteMessage(

"\nProperties of type " +

propType.ToString() +

" are not currently supported."

);

break;

}

if (newPropValue != null)

{

PromptKeywordOptions pko =

newPromptKeywordOptions(

"\nChange properties in nested blocks: "

);

pko.AllowNone = true;

pko.Keywords.Add("Yes");

pko.Keywords.Add("No");

pko.Keywords.Default = "Yes";

PromptResult pkr =

ed.GetKeywords(pko);

if (pkr.Status == PromptStatus.None |

pkr.Status == PromptStatus.OK)

{

if (pkr.Status == PromptStatus.None |

pkr.StringResult == "Yes")

recurse = true;

else

recurse = false;

returntrue;

}

}

}

}

}

}

}

returnfalse;

}

privatevoid ListProperties(System.Type objType)

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Editor ed = doc.Editor;

ed.WriteMessage(

"\nWritable properties for " +

objType.Name +

": "

);

PropertyInfo[] propInfos =

objType.GetProperties();

foreach (PropertyInfo propInfo in propInfos)

{

if (propInfo.CanWrite)

{

ed.WriteMessage(

"\n " +

propInfo.Name +

" : " +

propInfo.PropertyType

);

}

}

ed.WriteMessage("\n");

}

}

}

There are a few points I should make about this code:

GetType() needs an assembly qualified name. The above code makes two calls to this function, one without the "Autodesk.AutoCAD.DatabaseServices." prefix and the ", acdbmgd" suffix, in case we want to get another type of class, but if that fails then the prefix/suffix get added.

To make it easier for the user to select a writable property, I've implemented a separate ListProperties() function that iterates through the available properties and provides the name and type for each one that's writable.

Only properties of these datatypes are currently supported: System.Int32, System.Double, System.String, System.Boolean. It should be simple enough to support other datatypes (Vector3d, Point3d etc.), if you have the time and the inclination.

That's about it for the UI portion of the code... let's take a look at what happens when this code runs: