Stripping XData from offset AutoCAD entities using .NET

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.Com
mandEnded += 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.

5 responses to “Stripping XData from offset AutoCAD entities using .NET”

  1. Hi Kean,

    instead of
    "// Check the entity has our XData using LINQ"

    you can use:

    if (dbo.GetXDataForApplication("KEAN") != null)
    {
    // If so, remove it
    ...
    }

    Regards,
    Chris

  2. Hi Chris,

    Thanks! Not sure how I managed to forget to use the direct method (it's fun to use LINQ, but not always necessary ;-).

    I've gone ahead and modified the code in the post.

    Cheers,

    Kean

  3. "it's fun to use LINQ, but not always necessary ;-)"

    Using LINQ for the sake of using LINQ isn't fun, it can be extremely wasteful and adversely impact performance, not to mention serving as a bad influence on those who aspire to learn good sound coding habits.

    For example, when you have an array and you want to do something differently with all but the first element (sound familiar? 8-)), you just start your for() loop at 1 instead of 0, and skip the pointless and wasteful overhead of having to send everything through IEnumerable, and then at the end, have to allocate memory to hold a copy of the original array elements sans the first element.

  4. I used LINQ in this post because I'd forgotten there was a better way to do it. It happens.

    Nice to see you're back to using your real name, Tony.

    Kean

  5. "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."

    Well, I'm certainly looking forward to your more generic solution. As I mentioned on another forum, Overrules were designed to solve a broad range of problems that previously required the approach taken in your post. Overrules are a very powerful abstraction, but please heed this advice: If not implemented correctly, overrules (especially ObjectOverrule) can severely impact the performance of just about anything the user does with AutoCAD.

Leave a Reply to Tony Tanzillo Cancel reply

Your email address will not be published. Required fields are marked *