This question came in day or two ago:
“I attach an XData to some AutoCAD entities. When the AutoCAD entity is offset by OFFSET command, the XData is cloned in the offset entity. What’s the way to control(stop) the cloning of my XData in OFFSET command?”
This is an interesting one. Many applications rely on External Entity Data (XData) providing unique references from AutoCAD objects to other locations, so when objects with XData attached get copied, it either needs to be removed or updated to refer to something different (an identifier to a new record in an external database table, for instance). So it certainly seemed to be a topic worth covering here.
Responding to the question, a colleague of mine suggested using the Copied event on the objects of interest, and then using ObjectClosed to see when the copy gets added to the database (storing its ID for later processing).
I gave this a try, but wasn’t able to get it working properly: I suspect the problem was related to watching for ObjectClosed on objects that hadn’t previously been added to the database (from .NET, anyway – my colleague was talking about ObjectARX).
After hitting my head against it for some time, I ended up choosing a simpler path: during the OFFSET command, watch for objects being appended to the database. If any get added that are entities and have XData we care about attached, then we go ahead and process them once the command is over. Which in our case means we strip rather than update the XData, but other applications may want to treat it differently.
As long as we’re careful about the entities we modify – i.e. we only look for our specific XData, as there may be XData that other applications are interested in and rely upon being copied – then this simpler approach should be just fine.
Here’s the C# code that implements this, building on the commands in this previous post (which you can use to add XData to the entities you want to offset and test the existence of the XData once the OFFSET command has completed):
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
[assembly: ExtensionApplication(typeof(ExtendedEntityData.Commands))]
[assembly: CommandClass(typeof(ExtendedEntityData.Commands))]
namespace ExtendedEntityData
{
public class Commands : IExtensionApplication
{
private ObjectIdCollection _added;
public void Initialize()
{
_added = new ObjectIdCollection();
var dm = Application.DocumentManager;
// When a document is created, make sure we handle the
// important events it fires
var start = new CommandEventHandler(OnCommandWillStart);
var finish = new CommandEventHandler(OnCommandFinished);
dm.DocumentCreated +=
(s, e) =>
{
e.Document.CommandWillStart += start;
e.Document.CommandEnded += finish;
e.Document.CommandCancelled += finish;
e.Document.CommandFailed += finish;
};
// Do the same for any documents existing on application
// initialization
foreach (Document doc in dm)
{
doc.CommandWillStart += start;
doc.CommandEnded += finish;
doc.CommandCancelled += finish;
doc.CommandFailed += finish;
}
}
public void Terminate()
{
var start = new CommandEventHandler(OnCommandWillStart);
var finish = new CommandEventHandler(OnCommandFinished);
foreach (Document doc in Application.DocumentManager)
{
doc.CommandWillStart -= start;
doc.CommandEnded -= finish;
doc.CommandCancelled -= finish;
doc.CommandFailed -= finish;
}
}
// When the OFFSET command starts, let's add our database
// monitoring event-handler
private void OnCommandWillStart(object s, CommandEventArgs e)
{
var doc = (Document)s;
if (e.GlobalCommandName == "OFFSET")
{
doc.Database.ObjectAppended +=
new ObjectEventHandler(OnObjectAppended);
}
}
// And when the command ends, remove it and call the function
// to process the information collected
private void OnCommandFinished(object s, CommandEventArgs e)
{
if (e.GlobalCommandName == "OFFSET")
{
var doc = (Document)s;
doc.Database.ObjectAppended -=
new ObjectEventHandler(OnObjectAppended);
// We're hard-coding our application name, so that we only
// attempt to strip off certain XData. This could be
// extended to be more general - or to query the XData
// at runtime from the original objects - but that comes
// with the risk of stomping on important information
// (important to someone else, that is :-)
StripXData(doc, "KEAN");
}
}
void OnObjectAppended(object sender, ObjectEventArgs e)
{
// When an object gets added to the database, simply add its
// ObjectId to our list
_added.Add(e.DBObject.ObjectId);
}
private void StripXData(Document doc, string appName)
{
if (_added.Count > 0)
{
using (
var tr = doc.TransactionManager.StartOpenCloseTransaction()
)
{
// Make sure the application name is in the database
// (this is almost certainly the case, but anyway)
AddRegAppTableRecord(doc, appName);
// We'll use this TypedValue to remove the XData, if it
// exists
var tv = new TypedValue(1001, appName);
foreach (ObjectId id in _added)
{
// Ignore anything that isn't an unerased entity with
// XData
var obj = tr.GetObject(id, OpenMode.ForRead, true);
if (!obj.IsErased && obj is Entity && obj.XData != null)
{
// See if we have our XData in the list
// [previous version checked this using LINQ:
// obj.XData.Cast<TypedValue>().
// Contains<TypedValue>(tv)
// ]
var xd = obj.GetXDataForApplication(appName);
if (xd != null)
{
// If so, remove it
obj.UpgradeOpen();
using (var rb = new ResultBuffer(tv))
{
obj.XData = rb;
}
}
}
}
tr.Commit();
}
_added.Clear();
}
}
[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(doc, "KEAN");
var rb =
new ResultBuffer(
new TypedValue(1001, "KEAN"),
new TypedValue(1000, "This is a test string")
);
using (rb)
{
obj.XData = rb;
}
tr.Commit();
}
}
}
private void AddRegAppTableRecord(Document doc, string name)
{
using (
var tr = doc.TransactionManager.StartOpenCloseTransaction()
)
{
var rat =
(RegAppTable)tr.GetObject(
doc.Database.RegAppTableId,
OpenMode.ForRead,
false
);
if (!rat.Has(name))
{
rat.UpgradeOpen();
var ratr = new RegAppTableRecord();
ratr.Name = name;
rat.Add(ratr);
tr.AddNewlyCreatedDBObject(ratr, true);
}
tr.Commit();
}
}
}
}
Update:
After some internal discussions, I’m working on a more generic solution to the issue of XData being copied with objects by certain AutoCAD commands. I’ll try to post something next week.