As a follow-on from this recent post, I decided to take a stab at a more generic solution for managing XData that should remain unique – i.e. attached to a single object – inside an AutoCAD drawing. This was prompted by an internal discussion that included a long-time colleague, Randy Kintzley, who suggested the approach taken in this post. (Thanks also to Tekno Tandean and Davis Augustine for adding valuable comments/feedback.)
Randy’s suggestion was to avoid per-command event handling completely by adding an additional piece of XData – the handle – to objects that need to be managed in this way. The beauty of this approach is that you don’t need to check on the various ways that objects can be duplicated/copied/cloned inside AutoCAD – you simply compare the handle in the XData with that of the owning object and if they’re the same then the data is valid. If they’re different then you can take action either to make the data valid or to remove it from what’s basically a copy of the original object.
As for when you perform this integrity check: that’s ultimately up to you – the least intrusive approach is probably to perform it “just in time”, when the objects are selected by the user and/or processed programmatically. You could make sure it happens “just in case” (i.e. on an ongoing basis), but that would add avoidable execution overhead.
There are some points to note regarding this:
- The handle needs to be stored as something other than a handle (e.g. using group-code 1000 – a string – rather than 1005), to make sure it doesn’t get translated automatically when the original object is copied. I’ve gone and added a prefix to this string, to make sure it doesn’t get confused with other data (and it also allows me not to rely on the item of XData existing in a specific location in the list – something you may choose to rely upon in your own application).
- Handles will get reassigned – and the XData invalidated – when objects get wblocked (or inserted) into a drawing. Which means the data will (always?) become invalid on wblock – and that’s probably OK, you can strip it when it’s next handled by your app – but you clearly don’t want to have a “false positive” where the object happens to have the same handle as the original. You could mitigate for this by also adding the fingerprint GUID of the originating drawing in the XData, but there’s memory and storage overhead associated with that, of course. This is left for the reader to implement based on their specific needs.
Here’s some C# code that implements this basic approach:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
namespace ExtendedEntityData
{
public class Commands
{
const string appName = "KEAN";
const string handPref = "HND:";
[CommandMethod("SELXD")]
public void SelectWithXData()
{
var doc = Application.DocumentManager.MdiActiveDocument;
var ed = doc.Editor;
// We'll filter our selection to only include entities with
// our XData attached
var tv = new TypedValue(1001, appName);
var sf = new SelectionFilter(new TypedValue[] { tv });
// Ask the user to select (filtered) entities
var res = ed.GetSelection(sf);
if (res.Status != PromptStatus.OK)
return;
// We'll collect our valid and invalid IDs in two collections
var valid = new ObjectIdCollection();
var invalid = new ObjectIdCollection();
using (var tr = doc.TransactionManager.StartTransaction())
{
FindValidStripInvalid(
tr, tv, res.Value.GetObjectIds(), valid, invalid
);
tr.Commit();
}
ed.WriteMessage(
"\nFound {0} objects with valid XData, " +
"stripped {1} objects of invalid XData.",
valid.Count,
invalid.Count
);
}
private void FindValidStripInvalid(
Transaction tr,
TypedValue root,
ObjectId[] ids,
ObjectIdCollection valid,
ObjectIdCollection invalid,
bool strip = true
)
{
foreach (var id in ids)
{
// Look for the "HND:" value anywhere in our app's
// XData (this could be changed to look at a specific
// location)
bool found = false;
// Start by opening each object for read and get the XData
// we care about
var obj = tr.GetObject(id, OpenMode.ForRead);
using (
var rb = obj.GetXDataForApplication((string)root.Value)
)
{
// Check just in case something got passed in that doesn't
// have our XData
if (rb != null)
{
foreach (TypedValue tv in rb)
{
// If we have a string value...
if (tv.TypeCode == 1000)
{
var val = tv.Value.ToString();
// That starts with our prefix...
if (val.StartsWith(handPref))
{
// And matches the object's handle...
if (val == handPref + obj.Handle.ToString())
{
// ... then it's a valid object
valid.Add(id);
found = true;
}
else
break; // Handle prefix found with bad handle
}
}
}
}
if (!found)
{
// We have an invalid handle reference (or none at all).
// Optionally strip the XData from this object
invalid.Add(id);
if (strip)
{
obj.UpgradeOpen();
obj.XData = new ResultBuffer(root);
}
}
}
}
}
[CommandMethod("GXD")]
public void GetXData()
{
var doc = Application.DocumentManager.MdiActiveDocument;
var ed = doc.Editor;
// Ask the user to select an entity
// for which to retrieve XData
var opt = new PromptEntityOptions("\nSelect entity");
var res = ed.GetEntity(opt);
if (res.Status == PromptStatus.OK)
{
using (var tr = doc.TransactionManager.StartTransaction())
{
var obj = tr.GetObject(res.ObjectId, OpenMode.ForRead);
using (var rb = obj.XData)
{
if (rb == null)
{
ed.WriteMessage(
"\nEntity does not have XData attached."
);
}
else
{
int n = 0;
foreach (TypedValue tv in rb)
{
ed.WriteMessage(
"\nTypedValue {0} - type: {1}, value: {2}",
n++,
tv.TypeCode,
tv.Value
);
}
}
}
}
}
}
[CommandMethod("SXD")]
public void SetXData()
{
var doc = Application.DocumentManager.MdiActiveDocument;
var ed = doc.Editor;
// Ask the user to select an entity
// for which to set XData
var opt = new PromptEntityOptions("\nSelect entity");
var res = ed.GetEntity(opt);
if (res.Status == PromptStatus.OK)
{
using (var tr = doc.TransactionManager.StartTransaction())
{
var obj = tr.GetObject(res.ObjectId, OpenMode.ForWrite);
AddRegAppTableRecord(tr, doc.Database, appName);
var rb =
new ResultBuffer(
new TypedValue(1001, appName),
new TypedValue(1000, "This is a test string"),
new TypedValue(1000, handPref + obj.Handle.ToString())
);
using (rb)
{
obj.XData = rb;
}
tr.Commit();
}
}
}
private void AddRegAppTableRecord(
Transaction tr, Database db, string name
)
{
var rat =
(RegAppTable)tr.GetObject(
db.RegAppTableId,
OpenMode.ForRead
);
if (!rat.Has(name))
{
rat.UpgradeOpen();
var ratr = new RegAppTableRecord();
ratr.Name = name;
rat.Add(ratr);
tr.AddNewlyCreatedDBObject(ratr, true);
}
}
}
}
I’ve tried to keep the code fairly bare-bones (which no doubt means there’s plenty of room for improvement :-).
It implements a helper function – FindValidStripInvalid() – to filter valid from invalid objects (optionally stripping the XData from the invalid ones). The SELXD command asks the user to select objects – filtering on those with our XData attached – and then passes them into this helper function.
This means, of course, that the selection process may report more objects have been selected than will later prove to be valid, but if that’s undesirable then a different approach is probably warranted (whether to filter them out “just in case” or to choose a completely different mechanism). And yes, this also means that your app’s “selection” process can no longer necessarily be considered a read-only operation: we’re introducing a database side-effect that might otherwise be at odds with your application’s behaviour. Again, the choice is yours: cleaning up at another time is certainly an option.
To see the code in action, try using the SXD command to add XData to a few entities inside an AutoCAD drawing. COPY or OFFSET these entities (multiple times, if you wish) and then use the GXD command to see what data is attached to a few of the objects. Running the SELXD and selecting everything will report the number of objects selected with valid XData and clean up the XData attached to any “invalid”, cloned objects.
As mentioned, this is a fairly simple implementation of the concept. There may well be further cases that you have to code for, depending on the behaviour you need in your application, but feel free to kick the tyres and let me know your feedback.
Update:
It turns out I've suggested an alternative approach to solving this problem, in the past. That approach uses an API introduced in AutoCAD 2010 and so is clearly only valid for the releases since then, in case that's a determining factor for your application. Otherwise both approaches remain valid, with pros and cons discussed in this post's comments thread.