Purging registered application names from a folder of AutoCAD drawings using .NET

In the last post we looked at some code to programmatically purge Registered Application names from the drawing currently active in AutoCAD. In this post we take the "batching" code first used in this previous post and apply it to this problem.

What we end up with is an additional command called PF which asks the user to specify a folder and then purges the RegApps from the DWGs in that folder, saving those files that end up being modified with the "_purged" suffix.

One point to note is the use of the Database.RetainOriginalThumbnailBitmap property: as we're not making any graphical changes it's fairly safe to set this to true, which retains the pervious thumbnail bitmap, rather than it being blank in the new drawing. If you were to set it to true after graphical changes nothing especially serious would happen, but it could be confusing for users if the preview differed substantially from the DWG contents.

Here's the C# code with the additional lines in red:

    1 using Autodesk.AutoCAD.ApplicationServices;

    2 using Autodesk.AutoCAD.DatabaseServices;

    3 using Autodesk.AutoCAD.EditorInput;

    4 using Autodesk.AutoCAD.Runtime;

    5 using System.IO;

    6 using System;

    7

    8 namespace Purger

    9 {

   10   public class Commands

   11   {

   12     [CommandMethod("PF")]

   13     public void PurgeFiles()

   14     {

   15       Document doc =

   16         Application.DocumentManager.MdiActiveDocument;

   17       Editor ed = doc.Editor;

   18

   19       PromptResult pr =

   20         ed.GetString(

   21           "\nEnter folder containing DWGs to process: "

   22         );

   23       if (pr.Status != PromptStatus.OK)

   24         return;

   25       string pathName = pr.StringResult;

   26

   27       string[] fileNames =

   28         Directory.GetFiles(pathName, "*.dwg");

   29

   30       // We'll use some counters to keep track

   31       // of how the processing is going

   32

   33       int processed = 0, saved = 0, problem = 0;

   34

   35       foreach (string fileName in fileNames)

   36       {

   37         if (fileName.EndsWith(

   38               ".dwg",

   39               StringComparison.CurrentCultureIgnoreCase

   40             )

   41         )

   42         {

   43           string outputName =

   44             fileName.Substring(

   45               0,

   46               fileName.Length - 4) +

   47             "_purged.dwg";

   48           Database db = new Database(false, true);

   49           using (db)

   50           {

   51             try

   52             {

   53               ed.WriteMessage(

   54                 "\n\nProcessing file: " + fileName

   55               );

   56

   57               db.ReadDwgFile(

   58                 fileName,

   59                 FileShare.ReadWrite,

   60                 false,

   61                 ""

   62               );

   63

   64               db.RetainOriginalThumbnailBitmap = true;

   65

   66               int objectsPurged =

   67                 PurgeDatabase(db);

   68

   69               // Display the results

   70

   71               ed.WriteMessage(

   72                 "\nPurged {0} object{1}",

   73                 objectsPurged,

   74                 objectsPurged == 1 ? "" : "s"

   75               );

   76

   77               // Only save if we changed something

   78

   79               if (objectsPurged > 0)

   80               {

   81                 ed.WriteMessage(

   82                   "\nSaving to file: {0}", outputName

   83                 );

   84

   85                 db.SaveAs(

   86                   outputName,

   87                   DwgVersion.Current

   88                 );

   89                 saved++;

   90               }

   91               processed++;

   92             }

   93             catch (System.Exception ex)

   94             {

   95               ed.WriteMessage(

   96                 "\nProblem processing file: {0} - \"{1}\"",

   97                 fileName,

   98                 ex.Message

   99               );

  100               problem++;

  101             }

  102           }

  103         }

  104       }

  105       ed.WriteMessage(

  106         "\n\nSuccessfully processed {0} files," +

  107         " of which {1} had objects to purge" +

  108         " and an additional {2} had errors " +

  109         "during reading/processing.",

  110         processed,

  111         saved,

  112         problem

  113       );

  114     }

  115

  116     [CommandMethod("PC")]

  117     public void PurgeCurrentDocument()

  118     {

  119       Document doc =

  120         Application.DocumentManager.MdiActiveDocument;

  121       Database db = doc.Database;

  122       Editor ed = doc.Editor;

  123

  124       int count =

  125         PurgeDatabase(db);

  126

  127       ed.WriteMessage(

  128         "\nPurged {0} object{1} from " +

  129         "the current database.",

  130         count,

  131         count == 1 ? "" : "s"

  132       );

  133     }

  134

  135     private static int PurgeDatabase(Database db)

  136     {

  137       int idCount = 0;

  138

  139       Transaction tr =

  140         db.TransactionManager.StartTransaction();

  141       using (tr)

  142       {

  143         // Create the list of objects to "purge"

  144

  145         ObjectIdCollection idsToPurge =

  146           new ObjectIdCollection();

  147

  148         // Add all the Registered Application names

  149

  150         RegAppTable rat =

  151           (RegAppTable)tr.GetObject(

  152             db.RegAppTableId,

  153             OpenMode.ForRead

  154         );

  155

  156         foreach (ObjectId raId in rat)

  157         {

  158           if (raId.IsValid)

  159           {

  160             idsToPurge.Add(raId);

  161           }

  162         }

  163

  164         // Call the Purge function to filter the list

  165

  166         db.Purge(idsToPurge);

  167

  168         Document doc =

  169           Application.DocumentManager.MdiActiveDocument;

  170         Editor ed = doc.Editor;

  171

  172         ed.WriteMessage(

  173           "\nRegistered applications being purged: "

  174         );

  175

  176         // Erase each of the objects we've been

  177         // allowed to

  178

  179         foreach (ObjectId id in idsToPurge)

  180         {

  181           DBObject obj =

  182             tr.GetObject(id, OpenMode.ForWrite);

  183

  184           // Let's just add to me "debug" code

  185           // to list the registered applications

  186           // we're erasing

  187

  188           RegAppTableRecord ratr =

  189             obj as RegAppTableRecord;

  190           if (ratr != null)

  191           {

  192             ed.WriteMessage(

  193               "\"{0}\" ",

  194               ratr.Name

  195             );

  196           }

  197

  198           obj.Erase();

  199         }

  200

  201         // Return the number of objects erased

  202         // (i.e. purged)

  203

  204         idCount = idsToPurge.Count;

  205         tr.Commit();

  206       }

  207       return idCount;

  208     }

  209   }

  210 }

You can download the source file from here.

20 responses to “Purging registered application names from a folder of AutoCAD drawings using .NET”

  1. Hi Kean,
    Is there any chance that you could show how to open drawings from a form to execute custom routines on them using .NET. I have been trying to find a solution for some time without any success. Could you create a simple example that would at least show how to switch the contexts, application & document? There have been a few others posts on the user group, but not a good solution. May be something that for you could be so simple, will help a lot of people.

  2. Fernando Malard Avatar

    Hi,

    The best approach is to separate the drawing walkthrough routine from the database operation itself.

    You may structure some kind of custom action that will be performed at each drawing. Then from a simple interface you can enable those custom actions and then perform those ones inside each drawing inside the loop.

    Regards.

  3. Hi Kean.

    It might be helpful to inform your readers that batch operations on DWG files also has the possibly-unintended side-effect of 'upgrading' those files from whatever release version they were saved in, to the current release version.

    Some may not realize that doing this with drawings produced saved by older versions of vertical products like ADT or Civil3D, can lead to utter catastrophy if they haven't backed up everything first.

  4. Hi Tony,

    In the examples I've shown I've saved to a different filename, but yes - if you need to maintain the original file version then a little more code is required (I'll look at posting something about this).

    Thanks,

    Kean

  5. The change seems to be simpler than I expected...

    db.SaveAs(
    outputName,
    db.LastSavedAsVersion // instead of
    // DwgVersion.Current
    );

    Kean

  6. Dear Kean,
    thank you for this sample code.
    Do you know if it is possible to get the variable "ANNOTATIVEDWG" for each of the db with your code? I just want to make drawings in a directory annotative but could not find anything in Database which can set it annotative.

    Regards,
    Roland

  7. Dear Roland,

    It looks as Database doesn't have this property exposed in AutoCAD 2008, so until it's fixed (hopefully in the next release) you'll need to set it using another approach.

    If you're in AutoCAD (i.e. not RealDWG) then you should be able to just call Application.SetSystemVariable() to set it.

    You'll need to make sure you set HostApplicationServices.WorkingDatabase = db;
    and then set it back to the previous value before the SaveAs (at least that's what I noticed when I just tried this out).

    Regards,

    Kean

    Oh, and I also noticed that I did need a little more code for the version maintenance, after all:

    DwgVersion ver =
    (db.LastSavedAsVersion == DwgVersion.MC0To0 ?
    DwgVersion.Current :
    db.LastSavedAsVersion
    );
    db.SaveAs(
    outputName,
    ver
    );

  8. Thank you, Kean.
    I hope this will be fixed in the next version.

    Regards,
    Roland

  9. Hi Kean.

    Thanks for giving that some attention.

    I'm not sure about this because I haven't tested it, but I'm pretty sure that AutoCAD verticals upgrade custom objects to the current release when the drawing saved in the earlier release is opened.

    I'm not sure that saving down to the previous AutoCAD DWG version will address that particular problem. I suppose that's because custom objects implemented by the various verticals do not seem to support saving themsleves to older versions, in the same way that AutoCAD does for native database objects.

  10. Hi Tony,

    It's quite possible that one or more of our AutoCAD-based verticals do this (I haven't looked into it, myself).

    Ultimately it comes down to a number of implementation decisions... (which I’m going to talk through more for the benefit of people implementing their own custom objects, not to make excuses for what we do in our products.)

    Providers of custom objects could choose to link the versions of their own objects to a specific DWG version (by querying AcDbDwgFiler::dwgVersion() in their dwgOutFields() implementation), but that can be very limiting as one may want to up the version in between format changes, which do not happen every release. It also raises problems of how best to dumb down objects to previous versions. With recently-upgraded (and unchanged) objects it's OK, but you'd still have to find a way to tag the data that originally defined the object on drawing load (and make sure any changes made to the object were handled gracefully when stored back to the old format).

    Then there's round-tripping: how to store the additional data with the old versions of the objects, allowing them to resurrect their behaviour when they come back into the newer release of the product. These problems are also there when custom object implementers support saveAs(), of course.

    An alternative would be to provide object enablers for previous versions that support newer versions of the objects, but that might also be a substantial effort and blur the line of what functionality is delivered in which release (creating strange behaviour as old products get partially upgraded feature sets).

    Anyway – all this to say that yes, I can understand how this happens from an implementation perspective. I don’t have any magic solutions, however – it’s an inherently difficult problem.

    Cheers,

    Kean

  11. Hi Kean,

    I was wondering if there's an easy way to modify the objects to purge. For example, if a particular text style was included in the drawing that I did not want to be purged. Can this easily be done?

    Thanks Mike

  12. Kean Walmsley Avatar

    Hi Mike,

    There are a couple of ways:

    You can maintain your own list of objects "to keep" and remove any items that are on this list from idsToPurge before you erase them.

    Or you can create an object that's owned at some level by the Database (an Xrecord placed inside the Named Objects Dictionary should do it) that contains "hard" references to the objects you wish to keep (which means using DxfCode.HardPointerId - or one of the indeces just following it - when creating your TypedValues). This has the advantage of also stopping the standard PURGE command from removing those objects.

    Cheers,

    Kean

  13. Hello Kean,

    I have a question regarding the PurgeFiles function. From within the using(db) statement I want to call another method which needs the same database/drawing. Is this possible? Can I, within the foreach loop, pass the database to another function, do some stuff (only read actions, no writing needed) and return to the mainfunction? I tried this, but keep getting fatal errors. Perhaps you could show me how this might work (if possible)?

    Thanks,

    Frank

  14. Hi Frank,

    If you pass the db to the function then this should work, although clearly the devil's in the detail. 🙂

    You must make sure you don't do anything inappropriate to the Database (Disposing of it would be the worst crime, modifying it isn't necessarily a problem, but again, it depends on what you're doing).

    Maybe you can email or post your code?

    Kean

  15. Hello Kean, thank you for your response. Let me explain what I am trying to do and what I have in my code.

    I have a directory with drawings in it. These drawings have to be checked on a couple of points. To do certain checks it is necessary to open the drawing.

    In this project there is a separation in classes. I have a class that concerns everything that has to do with layouts, there is a class for attributes, a class for layers and so on.

    For the checking I created a form for selecting the directory and a button to start the checking of the drawings. The clicking of the button start the CheckDrawings function. In this function there is a for each loop to go through all drawings in the given directory. In this loop there is an new function called for each check I want to do. So each check has it's own function. All these checking function are in a separate class as well and return a true or false. From within these checking classes, depending on the check, I use the functions from the specific classes for layout, attribute, layer etc.
    In the end a file is created with all "errors" found per drawing.

    A few examples off checks I do:
    - version of the file (no need for a database)
    - can the drawing be opened in the working acad version?
    - is there a specific titleblock in the drawing (name based checking of the titleblock)
    - on what layer is the titleblock
    - each attribute in the titleblock is checked as well, concerning textstyle, heigt, color, layer and value

    I hope it is a bit clear to you what I am trying to do. At the moment I am creating the database at the last moment, so in the functions in the specific classes. Until that moment I pass a string containing the full pathname to the drawing.

    If I run the program a couple of times quickly after eachother autocad locks/is ended. I can't seem to find out why.

    Below a few parts from my code:

    Button: click
    private void cmdBatchCheck_Click(object sender, EventArgs e)
    {
    BatchChecker.CheckDrawings(this);
    }

    Function: CheckDrawings
    public void CheckDrawings(BatchCheckForm CheckForm)
    {
    foreach (string path in filePaths)
    {
    try
    {
    // Check dwg version
    MnAlvCheckerHelper.DwgVersionChecker(fData.AlvVersion, path);

    // Try opening existing dwg
    if (!MnAlvCheckerHelper.FileOpenChecker(path))
    {
    // File could not be opened: continue to next dwg
    ErrorWriter.WriteLine(FileName, LogType.ContinueCheckingError, "Checking of this dwg has been terminated");
    continue;
    }

    // Check layout count
    if (!MnAlvCheckerHelper.LayoutCountChecker(path))
    {
    // Found too many layouts: continue to next dwg
    ErrorWriter.WriteLine(FileName, LogType.ContinueCheckingError, "Checking of this dwg has been terminated");
    continue;
    }

    //... more checks
    }
    }
    }

    Specific Function concerning layouts: GetLayoutArray
    public ArrayList GetLayoutArray(string path)
    {
    DBDictionary layoutDict = null;
    Layout lay = null;
    ArrayList la = new ArrayList();
    Database dbArraylist = new Database(false, true);
    dbArraylist.ReadDwgFile(path, FileShare.Read, true, "");
    Editor ed = Acadapp.DocumentManager.GetDocument(dbArraylist).Editor;

    DocumentLock docLock = ed.Document.LockDocument();

    using (Transaction tr = dbArraylist.TransactionManager.StartTransaction())
    {
    try
    {
    // Layouts don't have their own tables but are stored in a dictonary
    layoutDict = (DBDictionary)tr.GetObject(dbArraylist.LayoutDictionaryId, OpenMode.ForRead, false);

    //create enumarator
    DbDictionaryEnumerator dictEnum = layoutDict.GetEnumerator();
    while (dictEnum.MoveNext() && (_layoutId == ObjectId.Null))
    {
    DBDictionaryEntry dbDictEnt = dictEnum.Current;
    lay = (Layout)tr.GetObject(dbDictEnt.Value, OpenMode.ForRead, false);
    if (lay.LayoutName != "Model")
    {
    la.Add(lay.LayoutName);
    }
    }
    }
    catch
    {
    Acadapp.ShowAlertDialog("Couldn't retrieve layout from DB correctly");
    }
    finally
    {
    docLock.Dispose();
    dbArraylist.Dispose();
    }
    }
    return la;
    }

  16. Between the CheckDrawings function and the specific function there is the the call to the check function which returns a bool.

    Function: LayoutCountChecker
    public static bool LayoutCountChecker(string path)
    {
    bool retVal = false;
    MnLayoutManager layMan = new MnLayoutManager();
    string FileName = path.Substring(path.LastIndexOf("\\") + 1);
    ArrayList LayoutCount = null;

    try
    {
    LayoutCount = layMan.GetLayoutArray(path);

    // Check layout count
    if (LayoutCount.Count > 1)
    {
    // If more then 1, create entries in log
    ErrorWriter.WriteLine(FileName, LogType.LayoutCountError,
    "More then 1 layout was found: " + LayoutCount.Count);
    }
    else
    {
    // 1 layout was found, set return value to true
    retVal = true;
    }
    }
    catch (Exception)
    {
    // An unexpected error occured, create entries in log
    ErrorWriter.WriteLine(FileName, LogType.UnknownError, "An unknown error has occured");
    return false;
    }
    finally
    {
    }
    return retVal;
    }

  17. A couple of comments:

    You don't need to lock a DWG that you read into a Database using RealDwgFile() (the assumption being it isn't loaded into the editor: if it is, then you shouldn't need RealDwgFile() in the first place). So you can drop the code to get the editor and lock/unlock the document.

    Also you can use foreach() to iterate through the contents of the LayoutDictionary, which is much cleaner than direct use of the iterator (which is always prone to error).

    I hope this helps resolve your problem,

    Kean

  18. Thank you Kean, the problem was the iterator. Now it all works.

  19. Kean,

    I've tried to get this to work, but I can't seem to. I'll preface all of this and say I'm COMPLETELY NEW to all of this, and have no idea how any of this works, so I'm sure I'm messing it up, but I don't know how to fix it.

    First, when I downloaded the source file, it was a ".cs", which didn't show up in my "appload" window when I went to load it into AutoCAD. So, I changed the file extension to ".lsp", which loaded successfully, but then, on the command line (after loading), it reads:

    APPLOAD purge-files.lsp successfully loaded.
    Command: ; error: bad function: "PF"

    So... I'm guessing I messed it up when I changed the ".cs" to ".lsp", but I don't know what to do to get the .cs to load.

    Also, we want the file names to remain the same after the purge (i.e., not have the "_purge" after the file name, but maintain the existing file name), how do we make that change? I see the code says:

    string outputName =
    fileName.Substring(
    0,
    fileName.Length - 4) +
    "_purged.dwg";
    Database db = new Database(false, true);
    using (db)

    but I don't know where to start and stop the change (i.e., if I just take out the "_purged.dwg"; line, will that work or do I need the ";", do I need the "+" on the previous line, etc). We don't so much care about keeping the version of the file the same, but we need the file names to remain, and in the same location.

    Also, so you know, to make that change, I can't edit the .cs as it's not a known file type on my computer... but changing it to a .lsp allows me to open Notepad.

    Finally, any chance to getting a .lsp file with all these changes (same version [per previous comments], and same name [per this request])? If not, I understand... it would just make all these changes a bit easier for a newbie like me.

    Again, thanks for all you do! Any help would be greatly appreciated!

  20. Kevin,

    Although a bit old, this post should be of help:

    keanw.com/2006/07/getting_started.html

    You should be able to save back by change outputName on line 86 to fileName.

    Regards,

    Kean

Leave a Reply to Frank Cancel reply

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