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 to
b) 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:
1) Get a System.Type from a string:
System.Type objType = System.Type.GetType(typeName);
2) Using the Type object get a PropertyInfo from a string:
System.Reflection.PropertyInfo propInfo = objType.GetProperty(propName);
3) Check whether an entity is of a particular type:
res = objType.IsInstanceOfType(ent);
4) Access the property on a particular entity:
Get...
val = objType.InvokeMember(propName, BindingFlags.GetProperty, null, ent, args);
Set...
objType.InvokeMember(propName, BindingFlags.SetProperty, null, ent, args);
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
{
public class PropertyChangerCmds
{
[CommandMethod("TEST")]
public void 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."
);
}
}
private bool SelectClassPropertyAndValue(
out System.Type objType,
out string propName,
out object newPropValue,
out bool 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 =
new PromptKeywordOptions(
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 =
new PromptKeywordOptions(
"\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;
return true;
}
}
}
}
}
}
}
return false;
}
private void 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:
Command: TEST
Enter type of objects to look for: Circle
Writable properties for Circle:
Normal : Autodesk.AutoCAD.Geometry.Vector3d
Thickness : System.Double
Radius : System.Double
Center : Autodesk.AutoCAD.Geometry.Point3d
EndPoint : Autodesk.AutoCAD.Geometry.Point3d
StartPoint : Autodesk.AutoCAD.Geometry.Point3d
MaterialMapper : Autodesk.AutoCAD.GraphicsInterface.Mapper
MaterialId : Autodesk.AutoCAD.DatabaseServices.ObjectId
Material : System.String
ReceiveShadows : System.Boolean
CastShadows : System.Boolean
LineWeight : Autodesk.AutoCAD.DatabaseServices.LineWeight
Visible : System.Boolean
LinetypeScale : System.Double
LinetypeId : Autodesk.AutoCAD.DatabaseServices.ObjectId
Linetype : System.String
LayerId : Autodesk.AutoCAD.DatabaseServices.ObjectId
Layer : System.String
PlotStyleNameId : Autodesk.AutoCAD.DatabaseServices.PlotStyleDescriptor
PlotStyleName : System.String
Transparency : Autodesk.AutoCAD.Colors.Transparency
ColorIndex : System.Int32
Color : Autodesk.AutoCAD.Colors.Color
HasSaveVersionOverride : System.Boolean
XData : Autodesk.AutoCAD.DatabaseServices.ResultBuffer
MergeStyle : Autodesk.AutoCAD.DatabaseServices.DuplicateRecordCloning
OwnerId : Autodesk.AutoCAD.DatabaseServices.ObjectId
AutoDelete : System.Boolean
Enter property to modify: Radius
Enter new value of Radius property for all objects of type Circle: 3.14159
Change properties in nested blocks [Yes/No] <Yes>: Y
Type selected: Circle
Property selected: Radius
Value selected: 3.14159
Recurse chosen: True
In the next post we'll hook this up with some code to actually call the properties on various objects, recursing through block definitions, as needed.