Maintaining per-object XData in AutoCAD using .NET

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.

2 responses to “Maintaining per-object XData in AutoCAD using .NET”

  1. The problem with Xdata is that if it is attached to a block ATTSYNCing the block will erase it. This is mentioned in AutoCAD help library at docs.autodesk.com/ACD/2010/ENU/AutoCAD%202010%20User%20Documentation/index.html?url=WS1a9193826455f5ffa23ce210c4a30acaf-512c.htm,topicNumber=d0e199454 so it's not a totally robust way of storing extra data if it can be unwittingly erased.

  2. Thanks, Ewen.

    This post isn't really to discuss the relative merits of XData (vs. other options such as XRecords in extension dictionaries), but it's good that people are aware of limitations such as this.

    Many apps continue to rely on XData despite this particular issue, of course.

    Regards,

    Kean

  3. "// If we have a string value...
    // That starts with our prefix...
    // And matches the object's handle...
    // ... then it's a valid object

    ummm... Sorry, that's not true.

    It is not necessarily a 'valid object'.

    There is something that you and your colleages seem to have overlooked.

  4. stoptheinhumanity@yaho.com Avatar
    stoptheinhumanity@yaho.com

    Appears to me Tony you are missing the point.

  5. It's almost certainly me who overlooked whatever it is. My colleagues contributed to the discussion but weren't sent the code before it was published. I thought I was very clear in this post that there were probably missing caveats. If there's something more serious amiss, it would be good for everyone to know.

    Feel free to come back and post a comment that's genuinely helpful, whenever you're ready. In the meantime, I'm going to get on with something productive (no doubt fatally flawed, but hey - we all do what we can).

    Kean

  6. The integrity of your solution relies entirely on an assumption that an object that is created by cloning an object from a different database, cannot have the same handle as the source object.

    I re-read your comments and am still not sure if your vague reference to a 'false-positive' is this, or not, so in your comments you may have not overlooked it, but the code and the comments in in it sure do.

    As far as being helpful, I've already done that by simply making it clear that the code has a fatal flaw that if not understood, could cost someone who decides to use the approach it takes real money, as opposed to Monopoly money.

  7. IMO the information in my post was clear enough: I'm sorry if you didn't understand it.

    The post even includes a proposal for one possible solution. Because I didn't like the overhead - and acknowledged that apps have different requirements (which I feel also implies may have other ways of solving the problem) - I left that for "the reader to implement based on their specific needs". But one approach was stated very clearly.

    As far as being helpful: your comment (it turns out) alluded to a problem that had already been acknowledged in the post. And so, no, I don't find that helpful. It would have been more so if you'd made the effort to clarify your statement from the beginning, even if you were mistaken.

  8. "As far as being helpful: your comment (it turns out) alluded to a problem that had already been acknowledged in the post."

    Sorry, the code doesn't work as posted. The comments in the code contradict what you refer to as an acknowledgement in the post, and the acknowledgement itself was so brief and vague that it was missed the first time it was read. I'm referring to the acknowledgement itself, not the proposal for how the problem could be solved. Perhaps after writing the code it was clear to you, but I don't agree that it was very clear to someone who never saw the code before, and the comments in the code are contradictory.

    Going further with the approach which you propose as being a way to solve the problem of ensuring distinct values in xdata, the fact that routine operations like COPY can result in invalidating xdata means that it must be dealt with as soon as the application uses the xdata for anything. In order to resolve the invalidated xdata, all values that must be distinct must be accessed and used to produce a new unique value for the invalidated xdata, while the user waits.....

    So the solution becomes a bottleneck during routine editing operations, because it prevents the use of the xdata without first validating and resolving it if invalid, and that must be done on-demand.

    In a case where the UI and/or objects in a drawing have a visual dependence on the distinct xdata values, then the solution can actually create more problems than it solves, because in those cases, invalidated xdata would have to be resolved immediately, just as would be required if the data involved were visible block attribute values rather than non-visual data.

    Assuming the fatal flaw was addressed, a solution like that can be useful in cases where a drawing file gets sent out to others for editing without being under control of the extension that manages the xdata, and resolved when the file returns, but as a way of solving the more transient case within the context of an AutoCAD session with the managing plug-in or extension present and running, that type of solution can creates more problems than it solves, IMO.

    There are several AutoCAD APIs that were designed to address the class of problems like the one this post deals with, including the deep-clone and wblock-clone events of the Document, and ObjectOverrules. Any of those can be used to solve the problem in more-determinant ways that eliminates any potential for invalid xdata to exist in the first place, and with that, the need resolve it after-the-fact, while the user waits.

  9. I see. So your concern with the approach goes beyond what you'd previously stated as the "fatal flaw"? Interesting that you should raise it now.

    As I've said in the post, you can certainly choose to eradicate invalid data sooner should that be important to you. If you have UI elements that somehow can't detect whether the XData is valid or not before displaying it, then you could certainly choose to clean it up at a different time.

    Ultimately there are lots of ways to skin a cat. If this implementation - which is simple and valid for lots of different scenarios - also happens to lead to you creating and sharing something that works for other people in other situations, then everyone wins. There's certainly no need to turn this into a contest.

  10. Hi Kean. Initially I didn't see any point to looking at the solution more closely, because I felt that it had that flaw that would rule out its use for anything other than for tracking what happened to files that were sent and edited outside the control of the extension that manages the xdata.

    The point was, that you pursued the solution with the acknowledged flaw intact, apparently because you felt that it wasn't a major issue or 'show-stopper', giving only brief mention to the problem and your proposed way to deal with it.

    The key to coming up with robust solutions is being familiar with the various APIs available, and knowing them well enough to be able to use them to solve problems, Like for example, knowing about the Curve API that reverses curves. If we aren't familiar with the vast number of APIs available to us, then we obviously can't leverage them to solve problems, and end up resorting to kludges that in many cases, are ineffective, and in some cases, downright embarrasing.

  11. "There's certainly no need to turn this into a contest."

    Feel free to label it any way you choose, but I would prefer to simply call it what it is, peer review. I've always invited and welcomed constructive scrutiny of the work I've shared, because that helps improve it

  12. OK, fair enough - that's a valid enough explanation for the follow-on. The fact you’d made snide comments prior to even seeing the post made me assume you were always going to pick holes in this one, anyway. But if you say that's not what this is about, I'll give you the benefit of the doubt.

    Yes, it's true that it's important to be familiar with the various APIs you might use to solve a particular problem. But what remains is the fact that there are alternatives available to you: the solution proposed by Randy remains absolutely valid, and in many ways very elegant. At least one approach for dealing with data from different places has been highlighted clearly. And if it wasn't clear before, the fact this ridiculous comment thread has raised it again will also help people.

    I've been working with various AutoCAD APIs on and off for around 20 years. I do other things, too, but that's been a constant. But the set of APIs is broad and ever-increasing and - while you may not do this - I have the occasional brain-fart and forget that an API exists even if I've used it effectively in the past. I *never* feel bad about someone telling me there's a better way of doing something, as long as that feedback is given politely and maturely. At the end of the day, the most important thing is that people get information on how to solve their problems. Whether that happens from the initial post, an update to the post, or feedback from the comments, then that’s great.

    So I don’t really care whether you feel I should be embarrassed by the quality of my output. Feel free to keep making snide comments about what I do: I’m going to keep on doing it, because I believe that not starting the dialog would be the biggest shame of all.

  13. I couldn't agree more. Constructive scrutiny, politely given, is my personal preference but I'll take what I can get.

    Any efforts you make to keep things civil are, as ever, greatly appreciated.

  14. I have dutifully refrained from making any snide comments about grabbing popcorn (oh sorry, there I went and did it anyway), but I do have to say one of my gripes about using a blog as the medium for this kind of material is that it's not designed for a discussion like this. Kean, you mention the benefit to everyone of having a followup discussion, and I totally agree with your sentiment, but this blog comment format is difficult to follow and limits participation, so also makes it much less useful. Just an observation. Ok gentlemen, this is your 8th and final round, please touch gloves in the center of the ring.

  15. It's Friday night here in Europe, so I'm already at the 19th hole (just to mix sporting metaphors). One day I'll hopefully have the pleasure of meeting Tony in person for a beer - I suspect it'd be a lot more interesting (and fruitful) than these discussions.

    Point taken about the blog format, but it is what it is. Maybe if we shifted across to Google+ (for instance) it'd work better in that respect.

    Thanks for the valuable feedback, Owen!

    Kean

  16. And, to be helpful, here's something that I had also overlooked, regarding this solution: WBLOCK * is often used to as the quick and easy way to purge a file of all unreferenced/unused items, and it also reallocates handles.

  17. Like you, I prefer a cleaner solution, but I don't see the problems as "fatal" by any means. I mean, you could cache nothing and just recalculate everything on the fly every time it's needed -- but invalidating stale data that isn't really stale is still a huge improvement over not caching anything at all. I think you're envisioning a use case where false negatives are expensive, but I think it's perfectly reasonable to use a design like this when false negatives are inexpensive. Likewise, some use cases may consider false positives unacceptable, but I'll bet most would not. I think this is Kean's point, that the "correctness" of this design depends on the use case. I can't disagree with that.

  18. I notice you have started to declare var doc, and var ed etc instead of the actual data type (Document etc). Is there a reason, and what is considered best practice?

  19. >>I would prefer to simply call it what it is, peer review.

    Seriously? You really think anyone reading this believes that the way you started this comment thread was 'peer review'? ROFL.

    Kean - Keep up the good work. This is a great blog - don't let the occasional troll get you down. The silent majority appreciate what you're doing.

  20. Great question, Dale. There's apparently been some debate on the pros and cons of this.

    Here's what I'd consider to be the stated best practice:

    blogs.msdn.com/b/ericlippert/archive/2011/04/20/uses-and-misuses-of-implicit-typing.aspx

    I tend to follow the advice in this post and use implicit typing when it's clear what the underlying type is (hovering over the "var" in VS displays the resolved type in a tooltip, by the way), and as it reduces redundancy. Also, as my blog is space-limited (70 characters or less per line, with the formatting I use), I tend to get the benefit of being able to reduce the number of lines of code I create.

    Cheers,

    Kean

  21. I'm only just weaning myself off Hungarian notation, so this is perhaps a little too radical for now. Maybe when I'm feeling stronger...

  22. Owen, yes it certainly does depend on the specifics of each usage case. And, my opinion on what approach works best is based on my own experiences with application-managed data, and what constraints I had to deal with in most of them, where this approach would clearly not work.

    As I mentioned, if there is any kind of visual dependence on the data in question, it must be resolved immediately, and I don't believe there is any getting around that. Visual dependence includes the UI (e.g., displaing aspects of the currently-selected items) as well as graphical representations of entities containing or referencing the invalid data.

    In the majority of those cases where I've had to deal with that, an indeterminant approach would not work, simply because the objects that carry the data are in the drawing and directly available to the end user at any time, including immediately after they've been created by a COPY or other operation where there was cloning.

    In many cases, selecting one or more objects with the application-managed data attached required UIs to be updated. In other cases,
    actions that operated on the current/pickfirst selection could not proceed with objects containing invalid data selected.

    I've found that more often than not, one can't allow invalid data to persist.

  23. I'm interested by this, as it could clearly be a reason to clean up invalid data sooner (when you do so is obviously also a choice - you don't have to wait for object selection in a custom command).

    There isn't a direct UI capability inside AutoCAD that works with XData (at least not that I'm aware of). So your app needs to integrate in some way to extract the information for it to be displayed, and can therefore presumably choose to ignore objects that don't have valid data. I did something similar in an old post I just re-discovered (just to re-emphasize my ability to forget approaches I've used in the past: keanw.com/2009/05/using-an-autocad-2010-overrule-to-control-the-copying-of-xdata-using-net.html).

    Getting some specifics on the scenario(s) you've highlighted would certainly help people choose between the available approaches for solving this problem.

  24. Hi Kean,

    Is it a good practice to use "using" statement in c# for every autocad object that has implemented IDispose.Dispose() method?

    Thanks,

  25. Hans Benny Christoffersen Avatar
    Hans Benny Christoffersen

    hi. So i have a problem i am trying so hard to figure out.
    my work place have placed so much data on 3D models in Autocad as xData.
    I am trying to find out a good way or good workflow to get this data on the model in inventor.
    i am trying to make a program that can prepare the model for inventor import. But dont quite know how the best way would be. I get some success by making the 3D object to a block and just dump all the xData as attributes on the block. But its not something i get a success on every time. if i export the block as a iges i can get the solid as the "name" of the block. but no more info then that.

    im asking here because no matter what i search. i always seem to land on this page for something related. But not just quite what i am looking for.

    1. Hi Hans Benny,

      Have you tried asking your question on the AutoCAD .NET forum? Unfortunately I'm not able to help you on this.

      Best,

      Kean

Leave a Reply to Dave Adams Cancel reply

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