In the last post we looked at some code that combined user-input from the AutoCAD command-line with .NET reflection to determine an object type, a property belonging to that type, and the value to which we want to change that property on instances of that type (phew!). Here's where we look at hooking that up with some code to work recursively through the drawing database and make the changes to the actual objects.
Firstly, let's look at some C# code to check the type of an entity, and then - as appropriate - to query the property's value and to set it to a new value, if it doesn't match what it needs to be set to:
// Function to change an individual entity's property
private int ChangeSingleProperty(
Entity ent,
System.Type objType,
string propName,
object newValue)
{
int changedCount = 0;
// Are we dealing with an entity we care about?
if (objType.IsInstanceOfType(ent))
{
// Check the existing value
object res =
objType.InvokeMember(
propName,
BindingFlags.GetProperty,
null,
ent,
new object[0]
);
// If it is not the same then change it
if (!res.Equals(newValue))
{
// Entity is only open for read
ent.UpgradeOpen();
object[] args = new object[1];
args[0] = newValue;
res =
objType.InvokeMember(
propName,
BindingFlags.SetProperty,
null,
ent,
args
);
changedCount++;
ent.DowngradeOpen();
}
}
return changedCount;
}
The previous post mentioned the guts of this function as two uses we intended to make of reflection (items 3 & 4 in the list, if you remember), but I'd suggest looking at the MSDN documentation on Type.IsInstanceOfType() and on Type.InvokeMember() for more information on how these functions work.
The rest of the code is relatively close to what was shown in the last post but one. I've approached things a little differently here, though:
- Recursion is now optional - we use a flag that gets set by the user.
- We have two versions of ChangePropertyOfEntitiesOfType() - the main function to change the property on a set of objects: one takes the ID of a container block table record and opens it, passing the list of contained objects through to the other version, which simply takes a list of object IDs. I could have duplicated some of the code for performance purposes, but it seemed cleaner for now to take the performance hit and reduce code duplication/maintainance.
- There are three commands defined (and I've done what I can to share implementations across them):
- CHPS - CHange the Property on Selected entities
- CHPM - CHange the Property on the contents of the Modelspace
- CHPP - CHange the Property on the contents of the Paperspace
That's about all there is to it. Here's the full C# source:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System.Reflection;
namespace PropertyChanger
{
public class PropertyChangerCmds
{
[CommandMethod("CHPS",
CommandFlags.Modal |
CommandFlags.Redraw |
CommandFlags.UsePickSet)
]
public void ChangePropertyOnSelectedEntities()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
try
{
PromptSelectionResult psr =
ed.GetSelection();
if (psr.Status == PromptStatus.OK)
{
System.Type objType;
string propName;
object newPropValue;
bool recurse;
if (SelectClassPropertyAndValue(
out objType,
out propName,
out newPropValue,
out recurse
)
)
{
int count =
ChangePropertyOfEntitiesOfType(
psr.Value.GetObjectIds(),
objType,
propName,
newPropValue,
recurse);
// Update the display, and print the count
ed.Regen();
ed.WriteMessage(
"\nChanged " +
count + " object" +
(count == 1 ? "" : "s") +
" of type " +
objType.Name +
" to have a " +
propName + " of " +
newPropValue + "."
);
}
}
}
catch (System.Exception ex)
{
ed.WriteMessage(
"Exception: " + ex
);
}
}
[CommandMethod("CHPM")]
public void ChangePropertyOnModelSpaceContents()
{
ChangePropertyOnSpaceContents(
BlockTableRecord.ModelSpace
);
}
[CommandMethod("CHPP")]
public void ChangePropertyOnPaperSpaceContents()
{
ChangePropertyOnSpaceContents(
BlockTableRecord.PaperSpace
);
}
private void ChangePropertyOnSpaceContents(
string spaceName
)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
try
{
System.Type objType;
string propName;
object newPropValue;
bool recurse;
if (SelectClassPropertyAndValue(
out objType,
out propName,
out newPropValue,
out recurse
)
)
{
ObjectId spaceId;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
BlockTable bt =
(BlockTable)tr.GetObject(
doc.Database.BlockTableId,
OpenMode.ForRead
);
spaceId = bt[spaceName];
// Not needed, but quicker than aborting
tr.Commit();
}
// Call our recursive function to set the new
// value in our nested objects
int count =
ChangePropertyOfEntitiesOfType(
spaceId,
objType,
propName,
newPropValue,
recurse);
// Update the display, and print the count
ed.Regen();
ed.WriteMessage(
"\nChanged " +
count + " object" +
(count == 1 ? "" : "s") +
" of type " +
objType.Name +
" to have a " +
propName + " of " +
newPropValue + "."
);
}
}
catch (System.Exception ex)
{
ed.WriteMessage(
"Exception: " + ex
);
}
}
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");
}
// Version of the function that takes a container ID
private int ChangePropertyOfEntitiesOfType(
ObjectId btrId,
System.Type objType,
string propName,
object newValue,
bool recurse
)
{
// We simply open the container, extract the IDs
// and pass them to another version of the function...
// If efficiency is an issue, then this could be
// streamlined (i.e. duplicated, rather than factored)
ObjectIdCollection btrContents =
new ObjectIdCollection();
Document doc =
Application.DocumentManager.MdiActiveDocument;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
btrId,
OpenMode.ForRead
);
foreach (ObjectId entId in btr)
{
btrContents.Add(entId);
}
tr.Commit();
}
ObjectId[] ids = new ObjectId[btrContents.Count];
btrContents.CopyTo(ids, 0);
// Call the other version of this function
return ChangePropertyOfEntitiesOfType(
ids,
objType,
propName,
newValue,
recurse
);
}
// Version of the function that takes a list of ents
private int ChangePropertyOfEntitiesOfType(
ObjectId[] objIds,
System.Type objType,
string propName,
object newValue,
bool recurse
)
{
int changedCount = 0;
Document doc =
Application.DocumentManager.MdiActiveDocument;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
foreach (ObjectId entId in objIds)
{
Entity ent =
tr.GetObject(entId, OpenMode.ForRead)
as Entity;
// Change each entity, one by one
if (ent != null)
{
changedCount +=
ChangeSingleProperty(
ent,
objType,
propName,
newValue
);
}
// If we are to recurse and it's a blockref...
if (recurse)
{
BlockReference br = ent as BlockReference;
if (br != null)
{
// ...then recurse
changedCount +=
ChangePropertyOfEntitiesOfType(
br.BlockTableRecord,
objType,
propName,
newValue,
recurse
);
}
}
}
tr.Commit();
}
return changedCount;
}
// Function to change an individual entity's property
private int ChangeSingleProperty(
Entity ent,
System.Type objType,
string propName,
object newValue)
{
int changedCount = 0;
// Are we dealing with an entity we care about?
if (objType.IsInstanceOfType(ent))
{
// Check the existing value
object res =
objType.InvokeMember(
propName,
BindingFlags.GetProperty,
null,
ent,
new object[0]
);
// If it is not the same then change it
if (!res.Equals(newValue))
{
// Entity is only open for read
ent.UpgradeOpen();
object[] args = new object[1];
args[0] = newValue;
res =
objType.InvokeMember(
propName,
BindingFlags.SetProperty,
null,
ent,
args
);
changedCount++;
ent.DowngradeOpen();
}
}
return changedCount;
}
}
}
I'll leave it as an exercise for the reader to see what can be done with the code... a few parting tips/comments:
- If you want to change a subset of objects you can either select them using CHPS and then further by object type (e.g. "BlockReference"), or you can stick to the generic "Entity" type to change the entire selection.
- You can toggle the "Visible" property using this code, which can be a bit scary for users (which is my queue to reiterate this point: the tool is for people who understand something about the drawing database structure and AutoCAD's object model as exposed through .NET... I'm providing the code as a demonstration of the technique for people doing development work, not for people to build and use as a standard day-to-day tool to replace or complement CHPROP).
- OK - disclaimer over... enjoy! :-)