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:
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.


Subscribe via RSS
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:
///
/// a starting point for an AcDbFieldWrapper
/// i guess it has to be implemented with C++ in a separate wrapper dll
/// HOW?
///
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");
}
///
/// static method to extract a fieldId from an attributeId
/// if the attribute contains a field
///
/// ObjectId of an attribute reference
/// ObjectId of a DBObject of type Autodesk.AutoCAD.DatabaseServices.ImpDBObject that probably is an AcDbField
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
Posted by: Wolfgang Ruthensteiner | July 13, 2007 at 06:14 PM
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
Posted by: Kean | July 13, 2007 at 07:48 PM
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.
Posted by: Wolfgang Ruthensteiner | July 13, 2007 at 09:50 PM
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
Posted by: Roland Feletic | July 16, 2007 at 08:24 AM
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
Posted by: Roland Feletic | July 19, 2007 at 09:20 PM
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
Posted by: Kean | July 20, 2007 at 06:55 PM
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
Posted by: Roland Feletic | July 22, 2007 at 08:46 PM
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
Posted by: Kean | July 23, 2007 at 09:20 AM
Sometimes it is so simple. It takes me hours of testing and it was just such a little thing.
Thak you, Kean.
Posted by: Roland Feletic | July 23, 2007 at 05:42 PM
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
Posted by: Roland Feletic | July 23, 2007 at 05:55 PM
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
Posted by: Kean | July 23, 2007 at 06:01 PM
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
Posted by: Roland Feletic | July 23, 2007 at 08:29 PM
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
Posted by: Roland Feletic | July 24, 2007 at 10:26 AM
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
Posted by: Kean | July 24, 2007 at 10:38 AM