In the last post we looked at some code to search the current drawing for a particular attribute and update its value. In this post - as promised - we're going to look at how to extend this application to work on a folder of drawings, updating those that contain the attribute and saving them to a new filename.
Rather than implement a fancy, graphical user interface, I've stuck with my approach of using the command-line for input and output. If you wish to implement your own UI, please do - it's really easy using .NET. I get the occasional request to do this myself, but I prefer to keep my posts focused - there are other posts on this blog that focus specifically on UI-related issues.
I ended up changing the code somewhat - I had been printing the results of the batching process from within the UpdateAttributesInDatabase() function. As I get closer to extracting the functionality to a RealDWG application, I've started reducing the dependencies on the Editor (as this will not be available later on). The WriteMessage() calls are now in the commands themselves, instead of in the processing functions.
In addition to the previous UA (UpdateAttribute) command, I've added a new one called UAIF (UpdateAttributeInFiles). This one queries for some additional data (such as the path to the folder to process), and then uses some handy .NET Framework functionality to iterate through the drawings stored in a particular location. Rather than overwrite the originals, the updated files get saved to a new name (with "-updated" added to the filename). It is left as an exercise to put these in a separate folder or to save back to the original location (I'd rather not get blamed for overwriting valuable data :-).
Here's the C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System.IO;
using System;
namespace AttributeUpdater
{
public class Commands
{
[CommandMethod("UAIF")]
public void UpdateAttributeInFiles()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
// Have the user choose the block and attribute
// names, and the new attribute value
PromptResult pr =
ed.GetString(
"\nEnter folder containing DWGs to process: "
);
if (pr.Status != PromptStatus.OK)
return;
string pathName = pr.StringResult;
pr =
ed.GetString(
"\nEnter name of block to search for: "
);
if (pr.Status != PromptStatus.OK)
return;
string blockName = pr.StringResult.ToUpper();
pr =
ed.GetString(
"\nEnter tag of attribute to update: "
);
if (pr.Status != PromptStatus.OK)
return;
string attbName = pr.StringResult.ToUpper();
pr =
ed.GetString(
"\nEnter new value for attribute: "
);
if (pr.Status != PromptStatus.OK)
return;
string attbValue = pr.StringResult;
string[] fileNames =
Directory.GetFiles(pathName,"*.dwg");
// We'll use some counters to keep track
// of how the processing is going
int processed = 0, saved = 0, problem = 0;
foreach (string fileName in fileNames)
{
if (fileName.EndsWith(
".dwg",
StringComparison.CurrentCultureIgnoreCase
)
)
{
string outputName =
fileName.Substring(
0,
fileName.Length - 4) +
"_updated.dwg";
Database db = new Database(false, false);
using (db)
{
try
{
ed.WriteMessage(
"\n\nProcessing file: " + fileName
);
db.ReadDwgFile(
fileName,
FileShare.ReadWrite,
false,
""
);
int attributesChanged =
UpdateAttributesInDatabase(
db,
blockName,
attbName,
attbValue
);
// Display the results
ed.WriteMessage(
"\nUpdated {0} instance{1} of " +
"attribute {2}.",
attributesChanged,
attributesChanged == 1 ? "" : "s",
attbName
);
// Only save if we changed something
if (attributesChanged > 0)
{
ed.WriteMessage(
"\nSaving to file: {0}", outputName
);
db.SaveAs(
outputName,
DwgVersion.Current
);
saved++;
}
processed++;
}
catch (System.Exception ex)
{
ed.WriteMessage(
"\nProblem processing file: {0} - \"{1}\"",
fileName,
ex.Message
);
problem++;
}
}
}
}
ed.WriteMessage(
"\n\nSuccessfully processed {0} files, of which {1} had " +
"attributes to update and an additional {2} had errors " +
"during reading/processing.",
processed,
saved,
problem
);
}
[CommandMethod("UA")]
public void UpdateAttribute()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
// Have the user choose the block and attribute
// names, and the new attribute value
PromptResult pr =
ed.GetString(
"\nEnter name of block to search for: "
);
if (pr.Status != PromptStatus.OK)
return;
string blockName = pr.StringResult.ToUpper();
pr =
ed.GetString(
"\nEnter tag of attribute to update: "
);
if (pr.Status != PromptStatus.OK)
return;
string attbName = pr.StringResult.ToUpper();
pr =
ed.GetString(
"\nEnter new value for attribute: "
);
if (pr.Status != PromptStatus.OK)
return;
string attbValue = pr.StringResult;
ed.WriteMessage(
"\nProcessing file: " + db.Filename
);
int count =
UpdateAttributesInDatabase(
db,
blockName,
attbName,
attbValue
);
ed.Regen();
// Display the results
ed.WriteMessage(
"\nUpdated {0} instance{1} of " +
"attribute {2}.",
count,
count == 1 ? "" : "s",
attbName
);
}
private int UpdateAttributesInDatabase(
Database db,
string blockName,
string attbName,
string attbValue
)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
// Get the IDs of the spaces we want to process
// and simply call a function to process each
ObjectId msId, psId;
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
msId =
bt[BlockTableRecord.ModelSpace];
psId =
bt[BlockTableRecord.PaperSpace];
// Not needed, but quicker than aborting
tr.Commit();
}
int msCount =
UpdateAttributesInBlock(
msId,
blockName,
attbName,
attbValue
);
int psCount =
UpdateAttributesInBlock(
psId,
blockName,
attbName,
attbValue
);
return msCount + psCount;
}
private int UpdateAttributesInBlock(
ObjectId btrId,
string blockName,
string attbName,
string attbValue
)
{
// Will return the number of attributes modified
int changedCount = 0;
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
btrId,
OpenMode.ForRead
);
// Test each entity in the container...
foreach (ObjectId entId in btr)
{
Entity ent =
tr.GetObject(entId, OpenMode.ForRead)
as Entity;
if (ent != null)
{
BlockReference br = ent as BlockReference;
if (br != null)
{
BlockTableRecord bd =
(BlockTableRecord)tr.GetObject(
br.BlockTableRecord,
OpenMode.ForRead
);
// ... to see whether it's a block with
// the name we're after
if (bd.Name.ToUpper() == blockName)
{
// Check each of the attributes...
foreach (
ObjectId arId in br.AttributeCollection
)
{
DBObject obj =
tr.GetObject(
arId,
OpenMode.ForRead
);
AttributeReference ar =
obj as AttributeReference;
if (ar != null)
{
// ... to see whether it has
// the tag we're after
if (ar.Tag.ToUpper() == attbName)
{
// If so, update the value
// and increment the counter
ar.UpgradeOpen();
ar.TextString = attbValue;
ar.DowngradeOpen();
changedCount++;
}
}
}
}
// Recurse for nested blocks
changedCount +=
UpdateAttributesInBlock(
br.BlockTableRecord,
blockName,
attbName,
attbValue
);
}
}
}
tr.Commit();
}
return changedCount;
}
}
}
Here's what happens when I run it on my temp folder (which is full of crud, and that's putting it politely):
Command: UAIF
Enter folder containing DWGs to process: c:\temp
Enter name of block to search for: TEST
Enter tag of attribute to update: ONE
Enter new value for attribute: 1234
Processing file: c:\temp\-old_recover.dwg
Problem processing file: c:\temp\-old_recover.dwg - "eBadDwgHeader"
Processing file: c:\temp\4076612-2.DWG
Updated 0 instances of attribute ONE.
Processing file: c:\temp\attributes.dwg
Updated 5 instances of attribute ONE.
Saving to file: c:\temp\attributes_updated.dwg
[Deleted a lot of uninteresting reports]
Successfully processed 42 files, of which 1 had attributes to update and an
additional 3 had errors during reading/processing.
You'll see that some DWGs have failed to load: hopefully because they're in need of recovery rather than the workings of this code. The above technique should handle this gracefully, allowing you to go back and fix the problematic ones.
Update 1:
Norman Yuan pointed out a mistake in this code - I've been using the TransactionManager from the current document, rather than from the db. This isn't dramatically bad, but would have become more of a problem as we move to RealDWG. What I should really have done sooner is remove the doc and ed variables from the UpdateAttributesIn...() functions, so that I would not have been in a position to use them by mistake. I now pass in the Database as a parameter, which allows us to use it to get the appropriate TransactionManager. One other benefit this code enables is the choice of constructing the database without an associated document:
Database db = new Database(false, true);
This makes the code work more efficiently (as well as being cleaner).
Here are the updated functions - not the entire code:
private int UpdateAttributesInDatabase(
Database db,
string blockName,
string attbName,
string attbValue
)
{
// Get the IDs of the spaces we want to process
// and simply call a function to process each
ObjectId msId, psId;
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
msId =
bt[BlockTableRecord.ModelSpace];
psId =
bt[BlockTableRecord.PaperSpace];
// Not needed, but quicker than aborting
tr.Commit();
}
int msCount =
UpdateAttributesInBlock(
db,
msId,
blockName,
attbName,
attbValue
);
int psCount =
UpdateAttributesInBlock(
db,
psId,
blockName,
attbName,
attbValue
);
return msCount + psCount;
}
private int UpdateAttributesInBlock(
Database db,
ObjectId btrId,
string blockName,
string attbName,
string attbValue
)
{
// Will return the number of attributes modified
int changedCount = 0;
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
btrId,
OpenMode.ForRead
);
// Test each entity in the container...
foreach (ObjectId entId in btr)
{
Entity ent =
tr.GetObject(entId, OpenMode.ForRead)
as Entity;
if (ent != null)
{
BlockReference br = ent as BlockReference;
if (br != null)
{
BlockTableRecord bd =
(BlockTableRecord)tr.GetObject(
br.BlockTableRecord,
OpenMode.ForRead
);
// ... to see whether it's a block with
// the name we're after
if (bd.Name.ToUpper() == blockName)
{
// Check each of the attributes...
foreach (
ObjectId arId in br.AttributeCollection
)
{
DBObject obj =
tr.GetObject(
arId,
OpenMode.ForRead
);
AttributeReference ar =
obj as AttributeReference;
if (ar != null)
{
// ... to see whether it has
// the tag we're after
if (ar.Tag.ToUpper() == attbName)
{
// If so, update the value
// and increment the counter
ar.UpgradeOpen();
ar.TextString = attbValue;
ar.DowngradeOpen();
changedCount++;
}
}
}
}
// Recurse for nested blocks
changedCount +=
UpdateAttributesInBlock(
db,
br.BlockTableRecord,
blockName,
attbName,
attbValue
);
}
}
}
tr.Commit();
}
return changedCount;
}
}
Update 2:
The above code does not realign attributes after editing their values: if your attributes are anything other than left-justified, you will need to make a call to AdjustAlignment on the attribute reference after editing it.
There's a trick to this: you need to make sure the working database is set to the drawing you're working on, as well as passing it as an argument to the function.
You could set the working database early in the code, or insert this code to do it locally (the choice is yours):
ar.TextString = attbValue;
// Begin alignment code
Database wdb = HostApplicationServices.WorkingDatabase;
HostApplicationServices.WorkingDatabase = db;
ar.AdjustAlignment(db);
HostApplicationServices.WorkingDatabase = wdb;
// End alignment code
ar.DowngradeOpen();
I've left the line before and the line after in the above snippet, so it should be clear where the code needs inserting.

Subscribe via RSS
Kean,
In VBA, autocad object document has "name" and "path" property, but in Autocad.Net, I only find "name" but not "path" for document,(Application.DocumentManager.MdiActiveDocument.Name)is that true? I also searched autodesk forum, but did not find an anwser.
If that's true, is there any other way to get the path of current activated drawing?
Thanks.
Posted by: Limin | July 25, 2007 at 06:38 PM
Kean,
Your example was great, but was not what I was hoping for. I need to be able to display a userform and be able to select a group of drawing files, perhaps from different locations and execute different routines in each one of them. What has me stuck is the "Execution Context". I know this is not a personal answering service, but if you ever write something that will show how to do this, I will very thankful.
Posted by: HJohn | July 25, 2007 at 10:24 PM
Yes - this is not a complete solution for someone wanting to run an arbitrary command against a list of files/folders. This isn't necessarily hard to build, but in any case there are out-of-the-box solutions you can use for just this - take a look at ScriptPro, for instance.
My posts are more about helping people develop code to solve their own specific development issues, not providing a generic tool to solve them all myself.
Regards,
Kean
Posted by: Kean | July 26, 2007 at 09:56 AM
Limin,
The Name property on the Document object exposes AcApDocument::fileName(), which contains both the filename and the path:
"Returns the full path and file name of the database being used by this AcApDocument."
Kean
Posted by: Kean | July 26, 2007 at 10:52 AM
Kean,
funny enough, I did something nearly identical to this not more than about a couple weeks ago. I tried to incorporate the progress meter as you illustrated a while ago, and found that the (if I recall correctly) db.SaveAs() method seems to put its progress on the progress meter, and when the save is complete, my progress meter does not return. So, i get a meter in place for the first drawing processed, and then it just reverts to the default. Any insight on a way to maintain the progress meter through the entire process?
Posted by: M Schumacher | July 26, 2007 at 02:59 PM
Thanks. Kean.
And I found for a drawing not saved yet, it only returns drawing name not path name, (even at that time it has a temparary file path). but for drawing that is saved, it returns both file name and path.
Posted by: Limin | July 26, 2007 at 03:32 PM
Kean,
I appreciate your response, but what I was look for is for something that AutoCAD users have been able to do since ever and it is being able to customized the application to suit our requirements. I have been able to create customizations that make our daily work much efficient. With the new .NET managed classes, the potential to create more sophisticated solutions has greatly expanded. Right now all our customizations (none of them commercially available) have been developed in LISP and VBA, both of which have been great in their times, excellent resources. I work for the precast industry and most of our jobs generate lots of drawings, hence the need to process batch of drawings. If I am not able to find a way of opening a list of drawings from a user form and solve the “Execution Context” problem, my future in .NET managed classes is dead. There is no information on the topic or I have been not able to find it.
Posted by: HJohn | July 26, 2007 at 04:16 PM
HJohn,
I'd suggest submitting your question - with sample code to reproduce - via the ADN website, if you're a member, or otherwise posting it to the .NET Customization Discussion Group.
Assuming you're talking about processing a number of open documents - which this post does not - then you will need to be careful. ObjectARX and .NET are inherently document-centric, unless you work to make sure they are working in the session/application context. There are various places you could be falling over - I now see you've posted to the discussion group, but haven't provided code. Unless you provide a minimal sample showing the problem you're having, it's hard for people to determine exactly where your problem is occurring.
Regards,
Kean
Posted by: Kean | July 26, 2007 at 04:43 PM
M Schumacher,
Yes - I see the problem. It also occurs if your drawing requires Autodesk Shape Manager to be loaded (the message "Loading Modeler DLLs" clears the progress meter in this case).
The only way I can see around it would be to display your own dialog - it could be located wherever you like - with a progress meter to demonstrate overall progress through the operation.
It would clearly have been nice to have used AutoCAD's, but this isn't really an option in this case, it seems.
Regards,
Kean
Posted by: Kean | July 26, 2007 at 04:53 PM
Kean, thank you for your example. It demonstrates the idea very well.
For the future:
You mentioned "context" many times, will you give a review of them, their limitations, peculiarities, obligatory switching between them etc.? Now I know 3 contexts: application, document and (new) object.
Posted by: Nikolay Poleshchuk | July 27, 2007 at 06:03 PM
Hi Nikolay,
Only two of these "contexts" are related: document and session/application. This previous post covers these fairly well (I hope).
Object contexts are a separate concept and are currently only used for the annotation scaling feature. They're unrelated to the application execution contexts mentioned above.
Then, of course, there are context menus, which are unrelated to both these areas. :-)
Regards,
Kean
Posted by: Kean | July 30, 2007 at 10:52 AM
Kean,
Thanks for pointing me to this great article! It really gave me what I needed. Is there any docmentation on the the BlockTable properties that are being exposed? I want to iterate thru more than 1 BlockTableRecord.PaperSpace object in each drawing file that I open and read. Is it possible to do that?
Thanks again,
John
Posted by: John | August 17, 2007 at 03:51 PM
John,
The documentation (such that it is) is currently included in the ObjectARX SDK (which you can download from http://www.autodesk.com/objectarx).
You can certainly iterate through multiple layouts. Instead of just getting psId, you should be able to use foreach() on the BlockTable, taking each ObjectId and passing it to the UpdateAttributesInBlock() function. If you want to be slightly more efficient, you might want to open the block table record and check its IsLayout property, to skip the block definitions that are not layouts (this property is also true for the modelspace layout).
Or you could just run the command on every block in the block table, and remove the recursion - that should also work.
Cheers.
Kean
Posted by: Kean | August 17, 2007 at 04:37 PM
Kean,
I appreciate your quick response and all these articles. They've been more informative and helpful than any "documentation" I've found so far (including the documentation downloaded with ObjectARX SDK). I'm assuming you're talking about in the UpdateAttributesInDatabase function. How do you type the foreach to iterate thru all ids in the blocktable and pass them into the UpdateAttributesInBlock function? That's giving me the most fits. It seems no matter how I type the variable for the foreach, it gives one error or another in the build.
Thanks,
John
Posted by: John | August 17, 2007 at 05:03 PM
John,
Try this (it compiles, but I haven't tested it, yet):
private int UpdateAttributesInDatabase( Database db, string blockName, string attbName, string attbValue ) { // Get the IDs of the spaces we want to process // and simply call a function to process eachObjectIdCollection layoutIds =
new ObjectIdCollection();
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
foreach (ObjectId id in bt)
{
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
id,
OpenMode.ForRead
);
if (btr.IsLayout)
layoutIds.Add(id);
}
// Not needed, but quicker than aborting
tr.Commit();
}
int updateCount = 0;
foreach (ObjectId id in layoutIds)
{
updateCount +=
UpdateAttributesInBlock(
db,
id,
blockName,
attbName,
attbValue
);
}
return updateCount;
}
Cheers,
Kean
Posted by: Kean | August 17, 2007 at 05:13 PM
Kean,
You are the man!! That worked perfectly! Thanks so much for your time, help, and guidance!
John
Posted by: John | August 17, 2007 at 08:58 PM
Really interesting post!
Never stop iterating and don’t fear failure. Choose well-understood conventions where they will do to the most good , shortcuts you might take will cost you more to fix later than to try to get right up-front today.
Thanks , Zoli Juhasz
Posted by: Agence de casting pour enfants | January 28, 2008 at 10:19 PM
Hi, Kean
I'm luis again
I take you code abut "Updating a specific attribute", Now when I to open file updated the attribute is not sichronized, so need to aply command "AttSync" but I can´t. how I do it in code VB, can you give me a tittle example to do it plase.
Posted by: Luis Rey | January 23, 2009 at 08:27 PM
Hi Luis,
Have you tried SendStringToExecute, as in this post?
Kean
Posted by: Kean Walmsley | January 25, 2009 at 06:59 PM
Yes kean, I tired that, but the problem is because I was opening the file by code:
Dim doc As Document = Application.DocumentManager.MdiActiveDocument
Dim ed As Editor = doc.Editor
Dim db As New Database(False, True)
Dim cadena As String = ruta
Dim blk As BlockReference
Try
db.ReadDwgFile(cadena, System.IO.FileShare.ReadWrite, False, "")
Catch ex As Exception
MsgBox(ex.Message)
Exit Sub
End Try
If I try use "SendStringToExecute" I need a drawing open (activedocumet), I just want to opening his database file but not open the file. I try this:
Dim doc As Document = Application.DocumentManager.MdiActiveDocument
Dim ed As Editor = doc.Editor
Dim db As New Database(True, False)
Dim cadena As String = RUTA
Try
db.ReadDwgFile(cadena, System.IO.FileShare.ReadWrite, False, "")
Catch ex As Exception
MsgBox(ex.Message)
Exit Sub
End Try
Dim trn As Transaction = db.TransactionManager.StartTransaction
Dim bt As BlockTable = trn.GetObject(db.BlockTableId, OpenMode.ForWrite)
If bt.Has("PIE DE PLANO-" & ID_CLIENTE) Then
doc.SendStringToExecute("attsync" & Chr(10) & "n" & Chr(10) & "PIE DE PLANO-597" & Chr(13), False, False, False)
Else
Exit Sub
End If
trn.Commit()
Try
db.SaveAs(RUTA, DwgVersion.Current)
db.Dispose()
db = Nothing
Catch ex As Exception
MsgBox("en archivo esta abierto" & ex.Message)
ed.WriteMessage(vbCr + "\nError al Actualizar Archivo: " + db.Filename)
End Try
But it has an error (Buffertoosmal).How a do this (sichronize the atributtes) just open the database file:
thank´s a lot kean. but I'm started to programing and a has so much errors.
Posted by: Luis Rey | January 26, 2009 at 08:09 PM
hi kean,I was tired but I'canto do this Help me please.
Posted by: Luis Rey | February 13, 2009 at 10:59 PM
Hi Luis,
I'm sorry - I really don't have time to provide individual support (unless there's a problem with one of my posts).
If you're an ADN member, please post it there - otherwise someone on the AutoCAD .NET Discussion Group may be able to help.
Kean
Posted by: Kean Walmsley | February 16, 2009 at 04:21 PM
I'm sorry kean it wasn't my intesion. I'm started to programing and the best guide to do is your blog. Every day on the morning or on the bus in cel phone when I go to work I read an new example on you blog (you have many post).
I hope you help if I will on problems abut someone your post.
Posted by: Luis Rey | February 17, 2009 at 05:11 PM
Many thanks for Update 2, that alignment problem plagued me for years in VBA and I'm glad to see there is a workaround on .NET.
Keep up the good work, your a true hero to those of us learning .NET!
Posted by: Terry W. Dotson | June 04, 2009 at 12:53 AM