Accessing the AutoCAD objects referred to by fields using .NET

Thanks to Wolfgang Ruthensteiner for suggesting this excellent topic a comment to this previous post. Here's Wonfgang's question:

How do I read back the field code with C# (from an attribute e.g.)?

I am linking room-label blocks with polylines, using fields inside an attribute to display the polyline's area property.

Later I want to find out programatically, which polyline a certain block is linked to by evaluating the field in the attribute (extracting the objectId).

This was actually quite tricky, and one I needed the help of our old friend, ArxDbg, to solve (see here for some information on this very useful ObjectARX sample). I should say up-front that there may well be a simpler way to access the information - the below technique is to some degree relying on the database structure (which might be considered an implementation detail). I may be missing a higher-level API providing a simpler way to access the information, but there you have it.

The full text of the field expression is stored in an AcDbField object (which is accesible through the Autodesk.AutoCAD.DatabaseServices.Field) which exists inside a field dictionary in the text object's (or attribute's) extension dictionary. So here's what needs to happen:

  • Select the MText object (I chose to use MText in the below code, as it was a bit more work to allow attribute selection within a block - left as an exercise for the reader 🙂
  • Open the MText object's extension dictionary
  • Open the nested field dictionary
  • Access the field object stored therein

At this stage you have your text string with all the uninterpreted field codes. For those of you that are interested, I remember an important decision at the time we implemented fields in AutoCAD: that we should maintain the existing protocol and not return uninterpreted field codes from the standard text access properties/methods. This was largely to avoid migration issues for applications that depended on the data to be returned in its evaluated form. But it clearly means a bit more work if you want to get at the underlying codes.

So once we have our codes, we then want to get back to the "referred" object(s). I implemented a simple function that parses a string for the following sub-string:

%<\_ObjId XXX>%

... where XXX is a string representing the ObjectId. The code then uses a conversion function to get an integer from the string, and create an ObjectId from the integer. We return the ID to the calling function, where we can then open it and find out more about it.

So that's the description - here's the C# code implementing it:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using System;

namespace FieldExtraction

{

  public class Commands

  {

    [CommandMethod("GFL")]

    static public void GetFieldLink()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;

      // Ask the user to select an attribute or an mtext

      PromptEntityOptions opt =

        new PromptEntityOptions(

          "\nSelect an MText object containing field(s): "

        );

      opt.SetRejectMessage(

        "\nObject must be MText."

      );

      opt.AddAllowedClass(typeof(MText), false);

      PromptEntityResult res =

        ed.GetEntity(opt);

      if (res.Status == PromptStatus.OK)

      {

        Transaction tr =

          doc.TransactionManager.StartTransaction();

        using (tr)

        {

          // Check the entity is an MText object

          DBObject obj =

            tr.GetObject(

              res.ObjectId,

              OpenMode.ForRead

            );

          MText mt = obj as MText;

          if (mt != null)

          {

            if (!mt.HasFields)

            {

              ed.WriteMessage(

                "\nMText object does not contain fields."

              );

            }

            else

            {

              // Open the extension dictionary

              DBDictionary extDict =

                (DBDictionary)tr.GetObject(

                  mt.ExtensionDictionary,

                  OpenMode.ForRead

                );

              const string fldDictName = "ACAD_FIELD";

              const string fldEntryName = "TEXT";

              // Get the field dictionary

              if (extDict.Contains(fldDictName))

              {

                ObjectId fldDictId =

                  extDict.GetAt(fldDictName);

                if (fldDictId != ObjectId.Null)

                {

                  DBDictionary fldDict =

                    (DBDictionary)tr.GetObject(

                      fldDictId,

                      OpenMode.ForRead

                    );

                  // Get the field itself

                  if (fldDict.Contains(fldEntryName))

                  {

                    ObjectId fldId =

                      fldDict.GetAt(fldEntryName);

                    if (fldId != ObjectId.Null)

                    {

                      obj =

                        tr.GetObject(

                          fldId,

                          OpenMode.ForRead

                        );

                      Field fld = obj as Field;

                      if (fld != null)

                      {

                        // And finally get the string

                        // including the field codes

                        string fldCode = fld.GetFieldCode();

                        ed.WriteMessage(

                          "\nField code: "

                          + fldCode

                        );

                        // Loop, using our helper function

                        // to find the object references

                        do

                        {

                          ObjectId objId;

                          fldCode =

                            FindObjectId(

                              fldCode,

                              out objId

                            );

                          if (fldCode != "")

                          {

                            // Print the ObjectId

                            ed.WriteMessage(

                              "\nFound Object ID: "

                              + objId.ToString()

                            );

                            obj =

                              tr.GetObject(

                                objId,

                                OpenMode.ForRead

                              );

                            // ... and the type of the object

                            ed.WriteMessage(

                              ", which is an object of type "

                              + obj.GetType().ToString()

                            );

                          }

                        } while (fldCode != "");                         

                      }

                    }

                  }

                }

              }

            }

          }

        }

      }

    }

    // Extract an ObjectId from a field string

    // and return the remainder of the string

    //

    static public string FindObjectId(

      string text,

      out ObjectId objId

    )

    {

      const string prefix = "%<\\_ObjId ";

      const string suffix = ">%";

      // Find the location of the prefix string

      int preLoc = text.IndexOf(prefix);

      if (preLoc > 0)

      {

        // Find the location of the ID itself

        int idLoc = preLoc + prefix.Length;

        // Get the remaining string

        string remains = text.Substring(idLoc);

        // Find the location of the suffix

        int sufLoc = remains.IndexOf(suffix);

        // Extract the ID string and get the ObjectId

        string id = remains.Remove(sufLoc);

        objId = new ObjectId(Convert.ToInt32(id));

        // Return the remainder, to allow extraction

        // of any remaining IDs

        return remains.Substring(sufLoc + suffix.Length);

      }

      else

      {

        objId = ObjectId.Null;

        return "";

      }

    }

  }

}

Here's what happens when we run the code. Firstly I went and created a simple, closed polyline and a circle. I then created a single MText object with field codes accessing the other two objects' areas:

Fields_4

I then run the GFL command and select the MText object:

Command: GFL

Select an MText object containing field(s):

Field code: Area of the circle: \AcObjProp Object(%<\_ObjId

2130239616>%).Area\P\PArea of the polyline: \AcObjProp Object(%<\_ObjId

2130239624>%).Area

Found Object ID: (2130239616), which is an object of type

Autodesk.AutoCAD.DatabaseServices.Circle

Found Object ID: (2130239624), which is an object of type

Autodesk.AutoCAD.DatabaseServices.Polyline

As you can see, we've been able to find and extract information from the objects referred to by fields in an MText object.

4 responses to “Accessing the AutoCAD objects referred to by fields using .NET”

  1. Wolfgang Ruthensteiner Avatar
    Wolfgang Ruthensteiner

    Hi Kean!

    Thank you very much for your answer - that was fast!!!!!

    I'm working with AutoCAD 2006 and there is no Field Object available in the Autodesk.AutoCAD.DatabaseServices Namespace. I figured out quite a lot today and already considered writing a managed wrapper for the AcDbField Object. Here is what i came up with so far:

    /// <summary>
    /// a starting point for an AcDbFieldWrapper
    /// i guess it has to be implemented with C++ in a separate wrapper dll
    /// HOW?
    /// </summary>
    class AcDbFieldWrapper
    {
    IntPtr intPtr;
    ObjectId fieldId;

    public AcDbFieldWrapper(ObjectId fieldId)
    {
    this.fieldId = fieldId;
    TransactionManager tm = fieldId.Database.TransactionManager;
    using (DBObject field = (DBObject)tm.GetObject(fieldId, OpenMode.ForRead, true,true))
    {
    this.dBObjectType = field.GetType(); //Autodesk.AutoCAD.DatabaseServices.ImpDBObject ???
    this.intPtr = field.UnmanagedObject;
    }
    }

    private Type dBObjectType;

    public Type DBObjectType
    {
    get { return dBObjectType; }
    }

    public string GetFieldCode()
    {
    throw new System.Exception("AcDbFieldWrapper.GetFieldCode() not implemented");
    }

    /// <summary>
    /// static method to extract a fieldId from an attributeId
    /// if the attribute contains a field
    /// </summary>
    /// <param name="attId">ObjectId of an attribute reference</param>
    /// <returns>ObjectId of a DBObject of type Autodesk.AutoCAD.DatabaseServices.ImpDBObject that probably is an AcDbField</returns>
    public static ObjectId GetFieldFromAttribute(ObjectId attId)
    {
    const string ACAD_FIELD = "ACAD_FIELD";
    const string TEXT = "TEXT";
    TransactionManager tm = attId.Database.TransactionManager;
    using (AttributeReference attr = tm.GetObject(attId, OpenMode.ForRead, true, true) as AttributeReference)
    {
    if ((attr != null) && attr.ExtensionDictionary.IsValid)
    {
    using (DBDictionary dic = (DBDictionary)tm.GetObject(attr.ExtensionDictionary, OpenMode.ForRead, true, true))
    {
    if (dic.Contains(ACAD_FIELD))
    {
    using (DBDictionary fdic = (DBDictionary)tm.GetObject(dic.GetAt(ACAD_FIELD), OpenMode.ForRead, true, true))
    {
    if (fdic.Contains(TEXT))
    {
    return fdic.GetAt(TEXT); //this probably returns an AcDbField's ObjectId
    }
    }
    }
    }
    }
    }
    return ObjectId.Null;
    }
    }

    public void TestAttributes(string fileName)
    {
    using (Database db = new Database(false, true))
    {

    db.ReadDwgFile(fileName, System.IO.FileShare.ReadWrite, true, null);
    TransactionManager tm = db.TransactionManager;
    using (Transaction t = tm.StartTransaction())
    {
    BlockTable bt = (BlockTable)tm.GetObject(db.BlockTableId, OpenMode.ForRead, false);
    BlockTableRecord modelSpace = (BlockTableRecord)tm.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead, false);

    foreach (ObjectId id in modelSpace)
    {
    BlockReference br = tm.GetObject(id, OpenMode.ForRead) as BlockReference;
    if (br != null)
    {
    foreach (ObjectId attID in br.AttributeCollection)
    {
    ObjectId fieldId = AcDbFieldWrapper.GetFieldFromAttribute(attID);
    if (fieldId != ObjectId.Null)
    {
    AcDbFieldWrapper acDbField = new AcDbFieldWrapper(fieldId);
    Debug.WriteLine("Attribute contains a field, Type: " + acDbField.DBObjectType.ToString());
    }
    }
    }
    }
    }

    }
    }

    Do I have to upgrade my AutoCAD or is there another way?

    W

  2. Kean Walmsley Avatar

    Hi Wolfgang,

    I'm not an expert at exposing managed wrappers, I'm afraid. Members of my team do it regularly, so asking via ADN would get you an answer. If you do happen to be an ADN member then there's also a fair amount of info on the ADN website that should be of interest, for example:

    Tutorial to create managed wrappers for custom ARX functions and objects

    If you're not an ADN member then let me know and I'll send you this particular document by email.

    That said, upgrading could prove to be more cost effective if you don't have many seats and you're not proficient with managed C++ (which I will freely admit I'm not).

    Regards,

    Kean

  3. Wolfgang Ruthensteiner Avatar
    Wolfgang Ruthensteiner

    Hi Kean!

    Thank you very much for all your help! I want to give it a try and create my own wrapper. I've done it before, but not for an AutoCAD project and I'm not a C++ expert either. I'm not an ADN Member, so I would highly appreciate if you could send me the document. I just found out that my e-mail adress posted with my last messages was wrong. Now it's corrected...

    Thank you again!

    W.

  4. Roland Feletic Avatar

    Hi Kean,
    thank you for this code. I had really big problems to get the field code of attributes. Now it should be no problem anymore.

    Roland

  5. Roland Feletic Avatar

    Hi Kean,
    with you're code I can get the field code of attributes, but can you explain how to write some field code into attributes or texts. It seems to work a little bit different with tables as for texts.

    I would need it for a program which inserts blocks and its attributes. The attributes includes a field eg. with the position of the block. Now when I insert the attributes the fieldcode is not correct because it does not know the Id of the block. Therefore I have to read the field code and change it.

    Regards
    Roland

  6. Kean Walmsley Avatar

    Hi Roland,

    I'm not sure why this works differently - can you email me some code that demonstrates the problem you're hitting?

    Regards,

    Kean

  7. Roland Feletic Avatar

    Hi Kean,
    here is the code I'm testing.
    But the attribute only shows the code of the field.

    using Autodesk.AutoCAD.ApplicationServices;
    using Autodesk.AutoCAD.DatabaseServices;
    using Autodesk.AutoCAD.EditorInput;
    using Autodesk.AutoCAD.Runtime;
    using Autodesk.AutoCAD.Geometry;
    using Autodesk.AutoCAD.Internal;

    [assembly: CommandClass(typeof(RSNNAcadApp.Test.InsertBlock))]
    namespace RSNNAcadApp.Test
    {
    public class InsertBlock
    {
    //Inserts a blockreference at Point 0,0,0
    [CommandMethod("InsertTest")]
    static public void InsertBlockTest()
    {
    ObjectId tmpBlockId;

    Document doc = Application.DocumentManager.MdiActiveDocument;
    Database db = doc.Database;
    Editor ed = doc.Editor;
    Transaction tr = doc.TransactionManager.StartTransaction();
    try
    {
    using (tr)
    {
    //Get Blockname and Id
    PromptStringOptions BlockNameOption = new PromptStringOptions("Blockname:");
    BlockNameOption.AllowSpaces = false;
    PromptResult BlockNameResult = ed.GetString(BlockNameOption);
    BlockTable bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead, false);

    if (BlockNameResult.Status == PromptStatus.OK)
    {
    string tmpBlockName = BlockNameResult.StringResult;

    tmpBlockId = bt[tmpBlockName];

    BlockTableRecord btr = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);

    Point3d ObjPunkt = Point3d.Origin;
    BlockReference BlockRef = new BlockReference(ObjPunkt, tmpBlockId);
    ObjectId BlObj = btr.AppendEntity(BlockRef);
    tr.AddNewlyCreatedDBObject(BlockRef, true);

    BlockTableRecord btAttRec = (BlockTableRecord)tr.GetObject(tmpBlockId, OpenMode.ForRead);

    if (btAttRec.Annotative)
    {
    //Attach Current Annotation-Scale.
    //If you don't add the content the block and the following attribute will not inserted correct.
    ObjectContextManager ocm = db.ObjectContextManager;
    ObjectContextCollection occ = ocm.GetContextCollection("ACDB_ANNOTATIONSCALES");

    DBObject obj = tr.GetObject(BlObj, OpenMode.ForRead);
    if (obj != null)
    {
    //ObjectContexts.AddContext(obj, occ.GetContext("1:1"));
    ObjectContexts.AddContext(obj, occ.CurrentContext);
    }
    }

    ed.WriteMessage(string.Format("\nBlockID: {0}", BlObj.ToString()));
    //Add the attributes
    foreach (ObjectId idAtt in btAttRec)
    {
    Entity ent = (Entity)tr.GetObject(idAtt, OpenMode.ForRead);
    if (ent is AttributeDefinition)
    {
    AttributeDefinition attDef = (AttributeDefinition)ent;
    AttributeReference attRef = new AttributeReference();
    attRef.SetAttributeFromBlock(attDef, BlockRef.BlockTransform);
    ObjectId AttObj = BlockRef.AttributeCollection.AppendAttribute(attRef);

    string FieldText = "\\AcVar Login \\f \"%tc1\"";
    attRef.TextString = FieldText;

    tr.AddNewlyCreatedDBObject(attRef, true);
    }
    }
    }

    tr.Commit();
    }
    }
    catch (System.Exception ex)
    {
    ed.WriteMessage(ex.ToString());
    }
    finally
    {
    tr.Dispose();
    }

    }

    }
    }

    Regards,
    Roland

  8. Hi Roland,

    You're missing the field delimiter - try this instead:

    string FieldText = "%<\\AcVar Login \\f \"%tc1\">%";

    The field values will probably come up as "####" until they're regened, so adding this after tr.Commit() will fix that:

    ed.Regen();

    Regards,

    Kean

  9. Roland Feletic Avatar
    Roland Feletic

    Sometimes it is so simple. It takes me hours of testing and it was just such a little thing.

    Thak you, Kean.

  10. Roland Feletic Avatar
    Roland Feletic

    Now just another question.
    If there is a field code like the following in the attributedeffinition, is it possible to insert the attributereference with the correct field code.

    The code from the attribute deffinition is:
    \AcObjProp.16.2 Object(?BlockRefId,1).InsertionPoint \f
    "%lu2%pt2%pr3"

    after inserting the attribute reference the field code of it is:
    %<\AcObjProp \f "%lu2%pt2%pr3">%

    I understand that it doesn't know the ObjectId of the BlockReference. But is there a simple way to insert the correct code?
    Or do I have to read the field code of the attribute deffinition and need to change "\AcObjProp.16.2 Object(?BlockRefId,1)" to e.g. "AcObjProp Object(%<\_ObjId 2130518272>%)" manually?

    Regards
    Roland

  11. Kean Walmsley Avatar

    Hi Roland,

    I haven't seen the syntax you've used here (Object(?BlockRefId,1)) - I'm only familiar with the _ObjId field code. Is this pseudo-code, that you'd like to have work, or is it from an actual object? I wasn't aware (or don't believe) it's possible without going via the ObjectId (although the process to set it can be automated - it doesn't need to be manual).

    Regards,

    Kean

  12. Roland Feletic Avatar

    Thank you, Kean.
    The code above is from a block placeholder (I hope it is the right name in english) for the insertion point of the block.
    After inserting the block with the command INSERT the field code changes and the correct ObjId is filled in and the y-Value will be seen. If I do it with the above code the result is something without any ObjectId and you can just see "InsertionPoint" as the field value. But now it is no problem anymore to search and replace the code from the attdef and fill in the right code into the attref, I just thought that there is something which is doing it automatically.

    Regards,
    Roland

  13. Roland Feletic Avatar

    Hi Kean,
    just another question. Is it possible to get the field code with the field delimiter?
    When you look at your example it is not really easy to see where the field code begins and where it ends. Therefore it is difficult to change something in the text without loosing some field code.

    Regards,
    Roland

  14. Hi Roland,

    I chose to make FindObjectId() as flexible as possible (for my needs), by defining a prefix and a suffix to search for. You should feel free to reimplement this to be more suitable to your own needs... 🙂

    Regards,

    Kean

  15. Thanks answering an old topic,

    On the Autodesk dicussion group, as suggested, I've found a solution for what I was exactly trying to do.

    discussion.autodesk.com/forums/thread.jspa?messageID=6070934&#6070934

    So far, the piece of code From Roland Feletic works great

    Alain

  16. Hi Alain,

    Sorry - I don't know. I suggest submitting your question to the ADN team, if you're a member, or otherwise the AutoCAD .NET Discussion Group.

    Regards,

    Kean

  17. Thanks answering an old topic,

    On the Autodesk dicussion group, as suggested, I've found a solution for what I was exactly trying to do.

    discussion.autodesk.com/forums/thread.jspa?messageID=6070934&#6070934

    So far, the piece of code From Roland Feletic works great

    Alain

  18. Hi Gents,

    I was wondering if anyone could either post or email the lisp to me?
    I have tried a few times to build it from here but it isn't working!

    Regards,

    John

  19. Sorry here is the email

    john.panda@nharch.net

    Thanks in advance

    J

  20. Kean Walmsley Avatar

    Hi John,

    I'm sorry: if this is a request for a LISP version of the above code, then you're going to be disappointed. I have neither a LISP version nor the time to create one for you.

    I suggest starting with this post to see how to build a .NET module for AutoCAD.

    Regards,

    Kean

  21. Hi, Kean.
    I was trying to find a way to insert a MText with custom field to drawing. I copied your code to my DWG to test something and it's very strange - when I manually choose from the menu Insert->Field and then MY_FIELD (from custom drawing properties), it inserts OK. When I insert it from my code - just put MText object with .Contents="%<\AcVar CustomDP.MY_FIELD >%", it apears as a textfield with "####". But when I use your code and click those fields (the manually inserted - correct displaying and programatically inserted - displaying ####), I get the same message - that this is a MText object with a value of \AcVar CustomDP.MY_FIELD. So where's a difference?
    How to properly insert MText with custom property, which would refresh every time a drawing is opened? I was trying to change and use your code from "Embedding fields in an AutoCAD table using .NET", but I failed - object was not found in BlockTable.

  22. I'm afraid I don't know what the problem is. I haven't looked at this area in four years - what's in the post is pretty much the sum of my knowledge on the subject.

    I suggest posting your question via ADN or to the AutoCAD .NET Discussion Group.

    Kean

  23. I admit to being a newbie here. But, nonetheless, could you point me in the right direction. I want to access in Plant 3D the Field/DrawingProperty: Area. It shows up in the FIELD dialog under the category Project, and is called CurrentDwgArea with the Field Expression: %<\PnID DrawingProperties.General.Area>%. Broader request, would be to access each Field Category and each Field within the category. I have read thru your other posts, and can't seem to find the right combination. Any help would be appreciated. And to note, I am teaching myself VB.NET.

    1. Kean Walmsley Avatar

      I'm afraid I don't know anything about Plant3D property information. I suggest posting to the relevant Autodesk discussion group or contacting ADN (if you're a member).

      Kean

  24. Camilo Andres Nemocon Farfan Avatar
    Camilo Andres Nemocon Farfan

    Hi, How can access to the object data for a Polyline?

    1. Hello,

      Please post your support questions to the appropriate Autodesk forum.

      Thank you,

      Kean

  25. Hi
    I used below code to put line bearing in a cell of table.

    lines_table.SetTextString(i + 2, 1, "%<AcObjProp Object(%<\_ObjId " + line_table_items[i].Split('|')[5].Trim(new char[] { '(', ')' }) + ">%).Angle f "%au5">%");

    the result is :
    uploads.disquscdn.c...

    What changes in my code can remove spaces and replace ° instead of d in bearing value ? (like image below , probably some changes needed in green codes)

    uploads.disquscdn.c...

    Thank you

    1. Kean Walmsley Avatar

      Please post your technical support questions to the Autodesk forums.

      Thank you,

      Kean

Leave a Reply to dennis Cancel reply

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