An automatic numbering system for AutoCAD blocks using .NET - Part 4

In the original post in this series, we introduced a basic application to number AutoCAD objects, specifically blocks with attributes. In the second post we extended this to make use of a generic numbering system for drawing-resident AutoCAD objects, and in the third post we implemented additional commands to take advantage of this new "kernel".

In this post we're going to extend the application in a few ways: firstly we're going to support duplicates, so that the LNS command which parses the current drawing to understand its numbers will support automatic and semi-automatic renumbering of objects with duplicate numbers. In addition there are a number of new event handlers that have been introduced to automatically renumber objects on creation/insertion/copy, and also to clear the numbering system when a user undoes any action in the drawing (just to be safe :-).

While introducing these event handlers I decide to switch the approach for associating data with a drawing: rather than declaring the variables at a class level and assuming they would be duplicated instantiated appropriately per-document, as shown in this previous post, I decided to encapsulate the variables in a class and specifically instantiate that class and store it per-document, as shown in this previous post.

Here's the updated C# code, with the changed & new lines in red, and here is the complete source file to save you having to strip line numbers:

    1 using Autodesk.AutoCAD.ApplicationServices;

    2 using Autodesk.AutoCAD.Runtime;

    3 using Autodesk.AutoCAD.DatabaseServices;

    4 using Autodesk.AutoCAD.EditorInput;

    5 using Autodesk.AutoCAD.Geometry;

    6 using System.Collections.Generic;

    7 using System.Collections;

    8

    9 namespace AutoNumberedBubbles

   10 {

   11   public class Commands : IExtensionApplication

   12   {

   13     // Strings identifying the block

   14     // and the attribute name to use

   15

   16     const string blockName = "BUBBLE";

   17     const string attbName = "NUMBER";

   18

   19     // A string to identify our application's

   20     // data in per-document UserData

   21

   22     const string dataKey = "TTIFBubbles";

   23

   24     // Define a class for our custom data

   25

   26     public class BubbleData

   27     {

   28       // A separate object to manage our numbering

   29

   30       private NumberedObjectManager m_nom;

   31       public NumberedObjectManager Nom

   32       {

   33         get { return m_nom; }

   34       }

   35

   36       // A "base" index (for the start of the list)

   37

   38       private int m_baseNumber;

   39       public int BaseNumber

   40       {

   41         get { return m_baseNumber; }

   42         set { m_baseNumber = value; }

   43       }

   44

   45       // A list of blocks added to the database

   46       // which we will then renumber

   47

   48       private List<ObjectId> m_blocksAdded;

   49       public List<ObjectId> BlocksToRenumber

   50       {

   51         get { return m_blocksAdded; }

   52       }

   53

   54       // Constructor

   55

   56       public BubbleData()

   57       {

   58         m_baseNumber = 0;

   59         m_nom = new NumberedObjectManager();

   60         m_blocksAdded = new List<ObjectId>();

   61       }

   62

   63       // Method to clear the contents

   64

   65       public void Reset()

   66       {

   67         m_baseNumber = 0;

   68         m_nom.Clear();

   69         m_blocksAdded.Clear();

   70       }

   71     }

   72

   73     // Constructor

   74

   75     public Commands()

   76     {

   77     }

   78

   79     // Functions called on initialization & termination

   80

   81     public void Initialize()

   82     {

   83       try

   84       {

   85         DocumentCollection dm =

   86           Application.DocumentManager;

   87         Document doc = dm.MdiActiveDocument;

   88         Database db = doc.Database;

   89         Editor ed = doc.Editor;

   90

   91         ed.WriteMessage(

   92           "\nLNS  Load numbering settings by analyzing the current drawing" +

   93           "\nDMP  Print internal numbering information" +

   94           "\nBAP  Create bubbles at points" +

   95           "\nBIC  Create bubbles at the center of circles" +

   96           "\nMB  Move a bubble in the list" +

   97           "\nDB  Delete a bubble" +

   98           "\nRBS  Reorder the bubbles, to close gaps caused by deletion" +

   99           "\nHLB  Highlight a particular bubble"

  100         );

  101

  102         // Hook into some events, to detect and renumber

  103         // blocks added to the database

  104

  105         db.ObjectAppended +=

  106           new ObjectEventHandler(

  107             db_ObjectAppended

  108           );

  109         dm.DocumentCreated +=

  110           new DocumentCollectionEventHandler(

  111             dm_DocumentCreated

  112           );

  113         dm.DocumentLockModeWillChange +=

  114           new DocumentLockModeWillChangeEventHandler(

  115             dm_DocumentLockModeWillChange

  116           );

  117

  118         doc.CommandEnded +=

  119           delegate(object sender, CommandEventArgs e)

  120           {

  121             if (e.GlobalCommandName == "UNDO" ||

  122                 e.GlobalCommandName == "U")

  123             {

  124               ed.WriteMessage(

  125                 "\nUndo invalidates bubble numbering: call" +

  126                 " LNS to reload the numbers for this drawing"

  127               );

  128               GetBubbleData((Document)sender).Reset();

  129             }

  130           };

  131       }

  132       catch

  133       { }

  134     }

  135

  136     public void Terminate()

  137     {

  138     }

  139

  140     // Method to retrieve (or create) the

  141     // BubbleData object for a particular

  142     // document

  143

  144     private BubbleData GetBubbleData(Document doc)

  145     {

  146       Hashtable ud = doc.UserData;

  147       BubbleData bd =

  148         ud[dataKey] as BubbleData;

  149

  150       if (bd == null)

  151       {

  152         object obj = ud[dataKey];

  153         if (obj == null)

  154         {

  155           // Nothing there

  156

  157           bd = new BubbleData();

  158           ud.Add(dataKey, bd);

  159         }

  160         else

  161         {

  162           // Found something different instead

  163

  164           Editor ed = doc.Editor;

  165           ed.WriteMessage(

  166             "Found an object of type \"" +

  167             obj.GetType().ToString() +

  168             "\" instead of BubbleData.");

  169         }

  170       }

  171       return bd;

  172     }

  173

  174     // Do the same for a particular database

  175

  176     private BubbleData GetBubbleData(Database db)

  177     {

  178       DocumentCollection dm =

  179         Application.DocumentManager;

  180       Document doc =

  181         dm.GetDocument(db);

  182       return GetBubbleData(doc);

  183     }

  184

  185     // When a new document is created, attach our

  186     // ObjectAppended event handler to the new

  187     // database

  188

  189     void dm_DocumentCreated(

  190       object sender,

  191       DocumentCollectionEventArgs e

  192     )

  193     {

  194       e.Document.Database.ObjectAppended +=

  195         new ObjectEventHandler(

  196           db_ObjectAppended

  197         );

  198     }

  199

  200     // When an object is appended to a database,

  201     // add it to a list we care about if it's a

  202     // BlockReference

  203

  204     void db_ObjectAppended(

  205       object sender,

  206       ObjectEventArgs e

  207     )

  208     {

  209       BlockReference br =

  210         e.DBObject as BlockReference;

  211       if (br != null)

  212       {

  213         BubbleData bd =

  214           GetBubbleData(e.DBObject.Database);

  215         bd.BlocksToRenumber.Add(br.ObjectId);

  216       }

  217     }

  218

  219     // When the command (or action) is over,

  220     // take the list of blocks to renumber and

  221     // go through them, renumbering each one

  222

  223     void dm_DocumentLockModeWillChange(

  224       object sender,

  225       DocumentLockModeWillChangeEventArgs e

  226     )

  227     {

  228       Document doc = e.Document;

  229       BubbleData bd =

  230         GetBubbleData(doc);

  231

  232       if (bd.BlocksToRenumber.Count > 0)

  233       {

  234         Database db = doc.Database;

  235         Transaction tr =

  236           db.TransactionManager.StartTransaction();

  237         using (tr)

  238         {

  239           foreach (ObjectId bid in bd.BlocksToRenumber)

  240           {

  241             try

  242             {

  243               BlockReference br =

  244                 tr.GetObject(bid, OpenMode.ForRead)

  245                 as BlockReference;

  246               if (br != null)

  247               {

  248                 BlockTableRecord btr =

  249                   (BlockTableRecord)tr.GetObject(

  250                     br.BlockTableRecord,

  251                     OpenMode.ForRead

  252                 );

  253                 if (btr.Name == blockName)

  254                 {

  255                   AttributeCollection ac =

  256                     br.AttributeCollection;

  257

  258                   foreach (ObjectId aid in ac)

  259                   {

  260                     DBObject obj =

  261                       tr.GetObject(aid, OpenMode.ForRead);

  262                     AttributeReference ar =

  263                       obj as AttributeReference;

  264

  265                     if (ar.Tag == attbName)

  266                     {

  267                       // Change the one we care about

  268

  269                       ar.UpgradeOpen();

  270

  271                       int bubbleNumber =

  272                         bd.BaseNumber +

  273                         bd.Nom.NextObjectNumber(bid);

  274                       ar.TextString =

  275                         bubbleNumber.ToString();

  276

  277                       break;

  278                     }

  279                   }

  280                 }

  281               }

  282             }

  283             catch { }

  284           }

  285           tr.Commit();

  286           bd.BlocksToRenumber.Clear();

  287         }

  288       }

  289     }

  290

  291     // Command to extract and display information

  292     // about the internal numbering

  293

  294     [CommandMethod("DMP")]

  295     public void DumpNumberingInformation()

  296     {

  297       Document doc =

  298         Application.DocumentManager.MdiActiveDocument;

  299       Editor ed = doc.Editor;

  300       BubbleData bd =

  301         GetBubbleData(doc);

  302       bd.Nom.DumpInfo(ed);

  303     }

  304

  305     // Command to analyze the current document and

  306     // understand which indeces have been used and

  307     // which are currently free

  308

  309     [CommandMethod("LNS")]

  310     public void LoadNumberingSettings()

  311     {

  312       Document doc =

  313         Application.DocumentManager.MdiActiveDocument;

  314       Database db = doc.Database;

  315       Editor ed = doc.Editor;

  316       BubbleData bd =

  317         GetBubbleData(doc);

  318

  319       // We need to clear any internal state

  320       // already collected

  321

  322       bd.Reset();

  323

  324       // Select all the blocks in the current drawing

  325

  326       TypedValue[] tvs =

  327         new TypedValue[1] {

  328             new TypedValue(

  329               (int)DxfCode.Start,

  330               "INSERT"

  331             )

  332           };

  333       SelectionFilter sf =

  334         new SelectionFilter(tvs);

  335

  336       PromptSelectionResult psr =

  337         ed.SelectAll(sf);

  338

  339       // If it succeeded and we have some blocks...

  340

  341       if (psr.Status == PromptStatus.OK &&

  342           psr.Value.Count > 0)

  343       {

  344         Transaction tr =

  345           db.TransactionManager.StartTransaction();

  346         using (tr)

  347         {

  348           // First get the modelspace and the ID

  349           // of the block for which we're searching

  350

  351           BlockTableRecord ms;

  352           ObjectId blockId;

  353

  354           if (GetBlock(

  355                 db, tr, out ms, out blockId

  356             ))

  357           {

  358             // For each block reference in the drawing...

  359

  360             foreach (SelectedObject o in psr.Value)

  361             {

  362               DBObject obj =

  363                 tr.GetObject(o.ObjectId, OpenMode.ForRead);

  364               BlockReference br = obj as BlockReference;

  365               if (br != null)

  366               {

  367                 // If it's the one we care about...

  368

  369                 if (br.BlockTableRecord == blockId)

  370                 {

  371                   // Check its attribute references...

  372

  373                   int pos = -1;

  374                   AttributeCollection ac =

  375                     br.AttributeCollection;

  376

  377                   foreach (ObjectId id in ac)

  378                   {

  379                     DBObject obj2 =

  380                       tr.GetObject(id, OpenMode.ForRead);

  381                     AttributeReference ar =

  382                       obj2 as AttributeReference;

  383

  384                     // When we find the attribute

  385                     // we care about...

  386

  387                     if (ar.Tag == attbName)

  388                     {

  389                       try

  390                       {

  391                         // Attempt to extract the number from

  392                         // the text string property... use a

  393                         // try-catch block just in case it is

  394                         // non-numeric

  395

  396                         pos =

  397                           int.Parse(ar.TextString);

  398

  399                         // Add the object at the appropriate

  400                         // index

  401

  402                         bd.Nom.NumberObject(

  403                           o.ObjectId, pos, false, true

  404                         );

  405                       }

  406                       catch { }

  407                     }

  408                   }

  409                 }

  410               }

  411             }

  412           }

  413           tr.Commit();

  414         }

  415

  416         // Once we have analyzed all the block references...

  417

  418         int start = bd.Nom.GetLowerBound(true);

  419

  420         // If the first index is non-zero, ask the user if

  421         // they want to rebase the list to begin at the

  422         // current start position

  423

  424         if (start > 0)

  425         {

  426           ed.WriteMessage(

  427             "\nLowest index is {0}. ",

  428             start

  429           );

  430           PromptKeywordOptions pko =

  431             new PromptKeywordOptions(

  432               "Make this the start of the list?"

  433             );

  434           pko.AllowNone = true;

  435           pko.Keywords.Add("Yes");

  436           pko.Keywords.Add("No");

  437           pko.Keywords.Default = "Yes";

  438

  439           PromptResult pkr =

  440             ed.GetKeywords(pko);

  441

  442           if (pkr.Status != PromptStatus.OK)

  443             bd.Reset();

  444           else

  445           {

  446             if (pkr.StringResult == "Yes")

  447             {

  448               // We store our own base number

  449               // (the object used to manage objects

  450               // always uses zero-based indeces)

  451

  452               bd.BaseNumber = start;

  453               bd.Nom.RebaseList(bd.BaseNumber);

  454             }

  455           }

  456         }

  457

  458         // We found duplicates in the numbering...

  459

  460         if (bd.Nom.HasDuplicates())

  461         {

  462           // Ask how to fix the duplicates

  463

  464           PromptKeywordOptions pko =

  465             new PromptKeywordOptions(

  466               "Blocks contain duplicate numbers. " +

  467               "How do you want to renumber?"

  468             );

  469           pko.AllowNone = true;

  470           pko.Keywords.Add("Automatically");

  471           pko.Keywords.Add("Individually");

  472           pko.Keywords.Add("Not");

  473           pko.Keywords.Default = "Automatically";

  474

  475           PromptResult pkr =

  476             ed.GetKeywords(pko);

  477

  478           bool bAuto = false;

  479           bool bManual = false;

  480

  481           if (pkr.Status != PromptStatus.OK)

  482             bd.Reset();

  483           else

  484           {

  485             if (pkr.StringResult == "Automatically")

  486               bAuto = true;

  487             else if (pkr.StringResult == "Individually")

  488               bManual = true;

  489

  490             // Whether fixing automatically or manually

  491             // we will iterate through the duplicate list

  492

  493             if (bAuto || bManual)

  494             {

  495               ObjectIdCollection idc =

  496                 new ObjectIdCollection();

  497

  498               // Get each entry in the duplicate list

  499

  500               SortedDictionary<int,List<ObjectId>> dups =

  501                 bd.Nom.Duplicates;

  502               foreach (

  503                 KeyValuePair<int,List<ObjectId>> dup in dups

  504               )

  505               {

  506                 // The position is the key in the entry

  507                 // and the list of IDs is the value

  508                 // (we take a copy, so we can modify it

  509                 // without affecting the original)

  510

  511                 int pos = dup.Key;

  512                 List<ObjectId> ids =

  513                   new List<ObjectId>(dup.Value);

  514

  515                 // For automatic renumbering there's no

  516                 // user interaction

  517

  518                 if (bAuto)

  519                 {

  520                   foreach (ObjectId id in ids)

  521                   {

  522                     bd.Nom.NextObjectNumber(id);

  523                     idc.Add(id);

  524                   }

  525                 }

  526                 else // bManual

  527                 {

  528                   // For manual renumbering we ask the user

  529                   // to select the block to keep, then

  530                   // we renumber the rest automatically

  531

  532                   ed.UpdateScreen();

  533

  534                   ids.Add(bd.Nom.GetObjectId(pos));

  535                   HighlightBubbles(db, ids, true);

  536

  537                   ed.WriteMessage(

  538                     "\n\nHighlighted blocks " +

  539                     "with number {0}. ",

  540                     pos + bd.BaseNumber

  541                   );

  542

  543                   bool finished = false;

  544                   while (!finished)

  545                   {

  546                     PromptEntityOptions peo =

  547                       new PromptEntityOptions(

  548                         "Select block to keep (others " +

  549                         "will be renumbered automatically): "

  550                       );

  551                     peo.SetRejectMessage(

  552                       "\nEntity must be a block."

  553                     );

  554                     peo.AddAllowedClass(

  555                       typeof(BlockReference), false);

  556                     PromptEntityResult per =

  557                       ed.GetEntity(peo);

  558

  559                     if (per.Status != PromptStatus.OK)

  560                     {

  561                       bd.Reset();

  562                       return;

  563                     }

  564                     else

  565                     {

  566                       // A block has been selected, so we

  567                       // make sure it is one of the ones

  568                       // we highlighted for the user

  569

  570                       if (ids.Contains(per.ObjectId))

  571                       {

  572                         // Leave the selected block alone

  573                         // by removing it from the list

  574

  575                         ids.Remove(per.ObjectId);

  576

  577                         // We then renumber each block in

  578                         // the list

  579

  580                         foreach (ObjectId id in ids)

  581                         {

  582                           bd.Nom.NextObjectNumber(id);

  583                           idc.Add(id);

  584                         }

  585                         RenumberBubbles(db, idc);

  586                         idc.Clear();

  587

  588                         // Let's unhighlight our selected

  589                         // block (renumbering will do this

  590                         // for the others)

  591

  592                         List<ObjectId> redraw =

  593                           new List<ObjectId>(1);

  594                         redraw.Add(per.ObjectId);

  595                         HighlightBubbles(db, redraw, false);

  596

  597                         finished = true;

  598