An automatic numbering system for AutoCAD blocks using .NET - Part 2
In the last post we saw some code to perform simple sequential numbering of blocks (reflected in a particular attribute contained in each block). In this next installment we'll extend the code by introducing a NumberedObjectManager class, which will manage the activities related to maintaining the sequence of numbers used by the various blocks. The main code will create an object of this class which will be used extensively in this and the next post by a number of new commands.
Here's the updated C# code, with changed & new lines marked with a red line-number. For your convenience here is the source file, to save you having to strip off the 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
8 namespace AutoNumberedBubbles
9 {
10 public class Commands : IExtensionApplication
11 {
12 // Strings identifying the block
13 // and the attribute name to use
14
15 const string blockName = "BUBBLE";
16 const string attbName = "NUMBER";
17
18 // We will use a separate object to
19 // manage our numbering, and maintain a
20 // "base" index (the start of the list)
21
22 private NumberedObjectManager m_nom;
23 private int m_baseNumber = 0;
24
25 // Constructor
26
27 public Commands()
28 {
29 m_nom = new NumberedObjectManager();
30 }
31
32 // Functions called on initialization & termination
33
34 public void Initialize()
35 {
36 try
37 {
38 Document doc =
39 Application.DocumentManager.MdiActiveDocument;
40 Editor ed = doc.Editor;
41
42 ed.WriteMessage(
43 "\nLNS Load numbering settings by analyzing the current drawing" +
44 "\nDMP Print internal numbering information" +
45 "\nBAP Create bubbles at points" +
46 "\nBIC Create bubbles at the center of circles"
47 );
48 }
49 catch
50 { }
51 }
52
53 public void Terminate()
54 {
55 }
56
57 // Command to extract and display information
58 // about the internal numbering
59
60 [CommandMethod("DMP")]
61 public void DumpNumberingInformation()
62 {
63 Document doc =
64 Application.DocumentManager.MdiActiveDocument;
65 Editor ed = doc.Editor;
66 m_nom.DumpInfo(ed);
67 }
68
69 // Command to analyze the current document and
70 // understand which indeces have been used and
71 // which are currently free
72
73 [CommandMethod("LNS")]
74 public void LoadNumberingSettings()
75 {
76 Document doc =
77 Application.DocumentManager.MdiActiveDocument;
78 Database db = doc.Database;
79 Editor ed = doc.Editor;
80
81 // We need to clear any internal state
82 // already collected
83
84 m_nom.Clear();
85 m_baseNumber = 0;
86
87 // Select all the blocks in the current drawing
88
89 TypedValue[] tvs =
90 new TypedValue[1] {
91 new TypedValue(
92 (int)DxfCode.Start,
93 "INSERT"
94 )
95 };
96 SelectionFilter sf =
97 new SelectionFilter(tvs);
98
99 PromptSelectionResult psr =
100 ed.SelectAll(sf);
101
102 // If it succeeded and we have some blocks...
103
104 if (psr.Status == PromptStatus.OK &&
105 psr.Value.Count > 0)
106 {
107 Transaction tr =
108 db.TransactionManager.StartTransaction();
109 using (tr)
110 {
111 // First get the modelspace and the ID
112 // of the block for which we're searching
113
114 BlockTableRecord ms;
115 ObjectId blockId;
116
117 if (GetBlock(
118 db, tr, out ms, out blockId
119 ))
120 {
121 // For each block reference in the drawing...
122
123 foreach (SelectedObject o in psr.Value)
124 {
125 DBObject obj =
126 tr.GetObject(o.ObjectId, OpenMode.ForRead);
127 BlockReference br = obj as BlockReference;
128 if (br != null)
129 {
130 // If it's the one we care about...
131
132 if (br.BlockTableRecord == blockId)
133 {
134 // Check its attribute references...
135
136 int pos = -1;
137 AttributeCollection ac =
138 br.AttributeCollection;
139
140 foreach (ObjectId id in ac)
141 {
142 DBObject obj2 =
143 tr.GetObject(id, OpenMode.ForRead);
144 AttributeReference ar =
145 obj2 as AttributeReference;
146
147 // When we find the attribute
148 // we care about...
149
150 if (ar.Tag == attbName)
151 {
152 try
153 {
154 // Attempt to extract the number from
155 // the text string property... use a
156 // try-catch block just in case it is
157 // non-numeric
158
159 pos =
160 int.Parse(ar.TextString);
161
162 // Add the object at the appropriate
163 // index
164
165 m_nom.NumberObject(
166 o.ObjectId, pos, false
167 );
168 }
169 catch { }
170 }
171 }
172 }
173 }
174 }
175 }
176 tr.Commit();
177 }
178
179 // Once we have analyzed all the block references...
180
181 int start = m_nom.GetLowerBound(true);
182
183 // If the first index is non-zero, ask the user if
184 // they want to rebase the list to begin at the
185 // current start position
186
187 if (start > 0)
188 {
189 ed.WriteMessage(
190 "\nLowest index is {0}. ",
191 start
192 );
193 PromptKeywordOptions pko =
194 new PromptKeywordOptions(
195 "Make this the start of the list?"
196 );
197 pko.AllowNone = true;
198 pko.Keywords.Add("Yes");
199 pko.Keywords.Add("No");
200 pko.Keywords.Default = "Yes";
201
202 PromptResult pkr =
203 ed.GetKeywords(pko);
204
205 if (pkr.Status == PromptStatus.OK)
206 {
207 if (pkr.StringResult == "Yes")
208 {
209 // We store our own base number
210 // (the object used to manage objects
211 // always uses zero-based indeces)
212
213 m_baseNumber = start;
214 m_nom.RebaseList(m_baseNumber);
215 }
216 }
217 }
218 }
219 }
220
221 // Command to create bubbles at points selected
222 // by the user - loops until cancelled
223
224 [CommandMethod("BAP")]
225 public void BubblesAtPoints()
226 {
227 Document doc =
228 Application.DocumentManager.MdiActiveDocument;
229 Database db = doc.Database;
230 Editor ed = doc.Editor;
231 Autodesk.AutoCAD.ApplicationServices.
232 TransactionManager tm =
233 doc.TransactionManager;
234
235 Transaction tr =
236 tm.StartTransaction();
237 using (tr)
238 {
239 // Get the information about the block
240 // and attribute definitions we care about
241
242 BlockTableRecord ms;
243 ObjectId blockId;
244 AttributeDefinition ad;
245 List<AttributeDefinition> other;
246
247 if (GetBlock(
248 db, tr, out ms, out blockId
249 ))
250 {
251 GetBlockAttributes(
252 tr, blockId, out ad, out other
253 );
254
255 // By default the modelspace is returned to
256 // us in read-only state
257
258 ms.UpgradeOpen();
259
260 // Loop until cancelled
261
262 bool finished = false;
263 while (!finished)
264 {
265 PromptPointOptions ppo =
266 new PromptPointOptions("\nSelect point: ");
267 ppo.AllowNone = true;
268
269 PromptPointResult ppr =
270 ed.GetPoint(ppo);
271 if (ppr.Status != PromptStatus.OK)
272 finished = true;
273 else
274 // Call a function to create our bubble
275 CreateNumberedBubbleAtPoint(
276 db, ms, tr, ppr.Value,
277 blockId, ad, other
278 );
279 tm.QueueForGraphicsFlush();
280 tm.FlushGraphics();
281 }
282 }
283 tr.Commit();
284 }
285 }
286
287 // Command to create a bubble at the center of
288 // each of the selected circles
289
290 [CommandMethod("BIC")]
291 public void BubblesInCircles()
292 {
293 Document doc =
294 Application.DocumentManager.MdiActiveDocument;
295 Database db = doc.Database;
296 Editor ed = doc.Editor;
297
298 // Allow the user to select circles
299
300 TypedValue[] tvs =
301 new TypedValue[1] {
302 new TypedValue(
303 (int)DxfCode.Start,
304 "CIRCLE"
305 )
306 };
307 SelectionFilter sf =
308 new SelectionFilter(tvs);
309
310 PromptSelectionResult psr =
311 ed.GetSelection(sf);
312
313 if (psr.Status == PromptStatus.OK &&
314 psr.Value.Count > 0)
315 {
316 Transaction tr =
317 db.TransactionManager.StartTransaction();
318 using (tr)
319 {
320 // Get the information about the block
321 // and attribute definitions we care about
322
323 BlockTableRecord ms;
324 ObjectId blockId;
325 AttributeDefinition ad;
326 List<AttributeDefinition> other;
327
328 if (GetBlock(
329 db, tr, out ms, out blockId
330 ))
331 {
332 GetBlockAttributes(
333 tr, blockId, out ad, out other
334 );
335
336 // By default the modelspace is returned to
337 // us in read-only state
338
339 ms.UpgradeOpen();
340
341 foreach (SelectedObject o in psr.Value)
342 {
343 // For each circle in the selected list...
344
345 DBObject obj =
346 tr.GetObject(o.ObjectId, OpenMode.ForRead);
347 Circle c = obj as Circle;
348 if (c == null)
349 ed.WriteMessage(
350 "\nObject selected is not a circle."
351 );
352 else
353 // Call our numbering function, passing the
354 // center of the circle
355 CreateNumberedBubbleAtPoint(
356 db, ms, tr, c.Center,
357 blockId, ad, other
358 );
359 }
360 }
361 tr.Commit();
362 }
363 }
364 }
365
366 // Internal helper function to open and retrieve
367 // the model-space and the block def we care about
368
369 private bool
370 GetBlock(
371 Database db,
372 Transaction tr,
373 out BlockTableRecord ms,
374 out ObjectId blockId
375 )
376 {
377 BlockTable bt =
378 (BlockTable)tr.GetObject(
379 db.BlockTableId,
380 OpenMode.ForRead
381 );
382
383 if (!bt.Has(blockName))
384 {
385 Document doc =
386 Application.DocumentManager.MdiActiveDocument;
387 Editor ed = doc.Editor;
388 ed.WriteMessage(
389 "\nCannot find block definition \"" +
390 blockName +
391 "\" in the current drawing."
392 );
393
394 blockId = ObjectId.Null;
395 ms = null;
396 return false;
397 }
398
399 ms =
400 (BlockTableRecord)tr.GetObject(
401 bt[BlockTableRecord.ModelSpace],
402 OpenMode.ForRead
403 );
404
405 blockId = bt[blockName];
406
407 return true;
408 }
409
410 // Internal helper function to retrieve
411 // attribute info from our block
412 // (we return the main attribute def
413 // and then all the "others")
414
415 private void
416 GetBlockAttributes(
417 Transaction tr,
418 ObjectId blockId,
419 out AttributeDefinition ad,
420 out List<AttributeDefinition> other
421 )
422 {
423 BlockTableRecord blk =
424 (BlockTableRecord)tr.GetObject(
425 blockId,
426 OpenMode.ForRead
427 );
428
429 ad = null;
430 other =
431 new List<AttributeDefinition>();
432
433 foreach (ObjectId attId in blk)
434 {
435 DBObject obj =
436 (DBObject)tr.GetObject(
437 attId,
438 OpenMode.ForRead
439 );
440 AttributeDefinition ad2 =
441 obj as AttributeDefinition;
442
443 if (ad2 != null)
444 {
445 if (ad2.Tag == attbName)
446 {
447 if (ad2.Constant)
448 {
449 Document doc =
450 Application.DocumentManager.MdiActiveDocument;
451 Editor ed = doc.Editor;
452
453 ed.WriteMessage(
454 "\nAttribute to change is constant!"
455 );
456 }
457 else
458 ad = ad2;
459 }
460 else
461 if (!ad2.Constant)
462 other.Add(ad2);
463 }
464 }
465 }
466
467 // Internal helper function to create a bubble
468 // at a particular point
469
470 private Entity
471 CreateNumberedBubbleAtPoint(
472 Database db,
473 BlockTableRecord btr,
474 Transaction tr,
475 Point3d pt,
476 ObjectId blockId,
477 AttributeDefinition ad,
478 List<AttributeDefinition> other
479 )
480 {
481 // Create a new block reference
482
483 BlockReference br =
484 new BlockReference(pt, blockId);
485
486 // Add it to the database
487
488 br.SetDatabaseDefaults();
489 ObjectId blockRefId = btr.AppendEntity(br);
490 tr.AddNewlyCreatedDBObject(br, true);
491
492 // Create an attribute reference for our main
493 // attribute definition (where we'll put the
494 // bubble's number)
495
496 AttributeReference ar =
497 new AttributeReference();
498
499 // Add it to the database, and set its position, etc.
500
501 ar.SetDatabaseDefaults();
502 ar.SetAttributeFromBlock(ad, br.BlockTransform);
503 ar.Position =
504 ad.Position.TransformBy(br.BlockTransform);
505 ar.Tag = ad.Tag;
506
507 // Set the bubble's number
508
509 int bubbleNumber =
510 m_baseNumber +
511 m_nom.NextObjectNumber(blockRefId);
512
513 ar.TextString = bubbleNumber.ToString();
514 ar.AdjustAlignment(db);
515
516 // Add the attribute to the block reference
517
518 br.AttributeCollection.AppendAttribute(ar);
519 tr.AddNewlyCreatedDBObject(ar, true);
520
521 // Now we add attribute references for the
522 // other attribute definitions
523
524 foreach (AttributeDefinition ad2 in other)
525 {
526 AttributeReference ar2 =
527 new AttributeReference();
528
529 ar2.SetAttributeFromBlock(ad2, br.BlockTransform);
530 ar2.Position =
531 ad2.Position.TransformBy(br.BlockTransform);
532 ar2.Tag = ad2.Tag;
533 ar2.TextString = ad2.TextString;
534 ar2.AdjustAlignment(db);
535
536 br.AttributeCollection.AppendAttribute(ar2);
537 tr.AddNewlyCreatedDBObject(ar2, true);
538 }
539 return br;
540 }
541 }
542
543 // A generic class for managing groups of
544 // numbered (and ordered) objects
545
546 public class NumberedObjectManager
547 {
548 // We need to store a list of object IDs, but
549 // also a list of free positions in the list
550 // (this allows numbering gaps)
551
552 private List<ObjectId> m_ids;
553 private List<int> m_free;
554
555 // Constructor
556
557 public NumberedObjectManager()
558 {
559 m_ids =
560 new List<ObjectId>();
561
562 m_free =
563 new List<int>();
564 }
565
566 // Clear the internal lists
567
568 public void Clear()
569 {
570 m_ids.Clear();
571 m_free.Clear();
572 }
573
574 // Return the first entry in the ObjectId list
575 // (specify "true" if you want to skip
576 // any null object IDs)
577
578 public int GetLowerBound(bool ignoreNull)
579 {
580 if (ignoreNull)
581 // Define an in-line predicate to check
582 // whether an ObjectId is null
583 return
584 m_ids.FindIndex(
585 delegate(ObjectId id)
586 {
587 return id != ObjectId.Null;
588 }
589 );
590 else
591 return 0;
592 }
593
594 // Return the last entry in the ObjectId list
595
596 public int GetUpperBound()
597 {
598 return m_ids.Count - 1;
599 }
600
601 // Store the specified ObjectId in the next
602 // available location in the list, and return
603 // what that is
604
605 public int NextObjectNumber(ObjectId id)
606 {
607 int pos;
608 if (m_free.Count > 0)
609 {
610 // Get the first free position, then remove
611 // it from the "free" list
612
613 pos = m_free[0];
614 m_free.RemoveAt(0);
615 m_ids[pos] = id;
616 }
617 else
618 {
619 // There are no free slots (gaps in the numbering)
620 // so we append it to the list
621
622 pos = m_ids.Count;
623 m_ids.Add(id);
624 }
625 return pos;
626 }
627
628 // Store an ObjectId in a particular position
629 // (shuffle == true will "insert" it, shuffling
630 // the remaining objects down,
631 // shuffle == false will replace the item in
632 // that slot)
633
634 public void NumberObject(
635 ObjectId id, int index, bool shuffle)
636 {
637 // If we're inserting into the list
638
639 if (index < m_ids.Count)
640 {
641 if (shuffle)
642 // Insert takes care of the shuffling
643 m_ids.Insert(index, id);
644 else
645 {
646 // If we're replacing the existing item, do
647 // so and then make sure the slot is removed
648 // from the "free" list, if applicable
649
650 m_ids[index] = id;
651 if (m_free.Contains(index))
652 m_free.Remove(index);
653 }
654 }
655 else
656 {
657 // If we're appending, shuffling is irrelevant,
658 // but we may need to add additional "free" slots
659 // if the position comes after the end
660
661 while (m_ids.Count < index)
662 {
663 m_ids.Add(ObjectId.Null);
664 m_free.Add(m_ids.LastIndexOf(ObjectId.Null));
665 m_free.Sort();
666 }
667 m_ids.Add(id);
668 }
669 }
670
671 // Dump out the object list information
672 // as well as the "free" slots
673
674 public void DumpInfo(Editor ed)
675 {
676 if (m_ids.Count > 0)
677 {
678 ed.WriteMessage("\nIdx ObjectId");
679
680 int index = 0;
681 foreach (ObjectId id in m_ids)
682 ed.WriteMessage("\n{0} {1}", index++, id);
683 }
684
685 if (m_free.Count > 0)
686 {
687 ed.WriteMessage("\n\nFree list: ");
688
689 foreach (int pos in m_free)
690 ed.WriteMessage("{0} ", pos);
691 }
692 }
693
694 // Remove the initial n items from the list
695
696 public void RebaseList(int start)
697 {
698 // First we remove the ObjectIds
699
700 for (int i=0; i < start; i++)
701 m_ids.RemoveAt(0);
702
703 // Then we go through the "free" list...
704
705 int idx = 0;
706 while (idx < m_free.Count)
707 {
708 if (m_free[idx] < start)
709 // Remove any that refer to the slots
710 // we've removed
711 m_free.RemoveAt(idx);
712 else
713 {
714 // Subtracting the number of slots
715 // we've removed from the other items
716 m_free[idx] -= start;
717 idx++;
718 }
719 }
720 }
721 }
722 }
Some information on the changes...
Rather than maintaining an integer for our "current number", we now have an instance of the NumberedObjectManager class, as well as a "base number" should we choose to start the numbering at something other than 0 (see lines 18-23 & 29). We use these variables when numbering the objects at lines 510-511.
We've added a couple of additional commands, LNS (Load Numbering System) and DMP (DuMP numbering system). These commands are announced to the user on load (lines 43-44) and implemented from lines 57-219. The DMP command is really an internal command to show the contents of the number list, while the LNS command analyses the current drawing and determines how the blocks inserted into it have been numbered. Should the numbering start at a higher number than 0, it asks the user whether to re-base the list, which essentially removes the initial entries (from 0 to the new start) and sets the "base number" variable. The function behind the LNS command could have been called automatically on drawing load, but I've chosen to keep it as a command, to allow more control (and to allow the user to re-analyze the drawing, should the numbering for some reason get out of sync).
So why did I choose to implement an analysis command, rather than storing the numbering information in the drawing? Mainly because the quantity of numbered blocks in any real-world (even highly complex) drawing is unlikely to be unmanageable (and therefore slow to analyze), so going to the effort of storing the list in XRecords in the DWG seems like overkill. We would - in any case - want to have something like the LNS command to allow existing drawings to be managed by this system. Seralizing to the drawing is always an option, of course, should your users provide feedback that this approach is innefficient.
The remainder of the changes are for the numbering system itself, the NumberedObjectManager (lines 543-721). As mentioned in the last post, this class has been kept as generic as possible, so it can be used for objects other than blocks, for instance. At the core of the class are two lists:
- m_ids - a list of ObjectIds, which is the list of numbered objects
- m_free - a list of free positions (integers) in the object list, which is useful for us to know the gaps in the list created by object deletion, etc.
An alternative implementation would have been to maintain a "map" between list positions and objects, but using two simple lists makes life easier in certain ways: if we want to delete a number we can simply set its value to ObjectId.Null and add its position to the "free" list, and if we want to move an item in the list we can remove it and insert it elsewhere - the other objects simply shift automatically (although they will need to be updated to reflect their new number, of course - more on this in the next post).
The current implementation fills gaps in the list when we number a new object, but we could very easily adjust the code to ignore those gaps and add numbers at the end.
So let's see what happens when we run the LNS command on the drawing we created in the last post. LNS does nothing very visible - although as we previously started the numbering at 1, it does ask us whether we want to rebase the list. To understand how the numbering system works, let's call LNS and DMP twice, and choosing a different re-basing option each time:
Command: LNS
Lowest index is 1. Make this the start of the list? [Yes/No] <Yes>: No
Command: DMP
Idx ObjectId
0 (0)
1 (2129683752)
2 (2129683776)
3 (2129683800)
4 (2129683824)
5 (2129683848)
6 (2129683872)
7 (2129683896)
8 (2129683920)
9 (2129683944)
10 (2129683968)
11 (2129683992)
12 (2129684016)
13 (2129684040)
14 (2129684064)
15 (2129684088)
16 (2129684112)
17 (2129684136)
18 (2129684160)
19 (2129684184)
20 (2129684208)
Free list: 0
Command: LNS
Lowest index is 1. Make this the start of the list? [Yes/No] <Yes>: Yes
Command: DMP
Idx ObjectId
0 (2129683752)
1 (2129683776)
2 (2129683800)
3 (2129683824)
4 (2129683848)
5 (2129683872)
6 (2129683896)
7 (2129683920)
8 (2129683944)
9 (2129683968)
10 (2129683992)
11 (2129684016)
12 (2129684040)
13 (2129684064)
14 (2129684088)
15 (2129684112)
16 (2129684136)
17 (2129684160)
18 (2129684184)
19 (2129684208)
We can see that the first time we run the LNS command, choosing not to re-base the list, we start the list at 0 and have a free slot at that position. If we start adding more numbered blocks, using the BIC or BAP commands, the system will start by numbering an item with 0 before carrying on at 21, 22, etc.
The second time we run the command, we do re-base the list, which means that our m_baseNumber variable will be set to 1 and will be added each time we map the internal list to what is numbered in the drawing.
That's it for today - in the next post we'll look at some more interesting usage of the numbering system, where we highlight, delete and move items in the list, as well as showing how to fill any gaps automatically.

Subscribe
In every post I miss the possiblility that we could have dynamic blocks.
Therefore, e.g. line 132 should look like this, shouldn't it?
if (br.IsDynamicBlock ? br.DynamicBlockTableRecord == blockId : br.BlockTableRecord == blockId)
Posted by: Roland Feletic | May 09, 2008 at 10:00 AM
Good point - I should have mentioned that this implementation has been designed and tested with boring old static blocks.
I haven't even considered what's required to support Dynamic Blocks, but I probably should.
Thanks for the reminder.
Kean
Posted by: Kean | May 09, 2008 at 10:04 AM
Hi Kean. Nice sample code you have there.
I've always had an interest in automatic serial numbering, having done a quite few implementations of that myself.
The intersting parts for me have been dealing with erasing, undoing the creation of, and cloning/copying of objects with application-managed serial numbers.
For starters, I would recommend storing your numbering 'seed' (e.g., the next available serial number) in the drawing, thereby placing it under control of AutoCAD's undo mechanism, so that if you were to create and assign serial numbers to objects, and then rolled that back via undo, the seed value is also rolled back and correct, allowing you to avoid 'holes' or gaps in the sequence.
How to deal with erasure of serial numbered objects is something that I've found to be very domain-specific. In some cases it may be appropriate to automatically renumber, while in others, it is definitely not. In fact, many schedules and tables that show serially-numbered objects in their rows, retain rows corresponding to erased objects (with a strike-through or other visual indicator that the object has been erased), and in some cases, they are removed only after the next published revision, so that anyone that looks at the schedule can clearly see that something was removed or deleted.
The next interesting aspect of this kind of project, is how to deal with duplication and cloning. For example, in many of the apps I've done that managed serially-numbered objects, I handle it using a deep clone reactor, so that when the user copied objects (only to the same space, but not for other forms of cloning), the new copies would have their serial numbers updated automatically by the deep clone reactor callback.
Oh well, just some food for thought ;)
Posted by: Tony Tanzillo | May 10, 2008 at 08:14 AM
Hi Tony,
Great input - thanks for that.
I was on a long weekend, and had queued up part 3 to go out before I got back today, so some of what you've mentioned is at least commented on in this last post.
I've so far avoided storing anything in the DWG, but your point is very valid: right now UNDO does cause a problem.
The way I'm thinking of addressing cloning is likely to be an objectAppended() event handler - I'm thinking of doing that next (we'll see how it goes).
Thanks again,
Kean
Posted by: Kean | May 13, 2008 at 09:01 AM
There is what I consider a major problem with in the AcDbBlockTable::Collection Functions and how it is applied in the code here.
if (!bt.Has(blockName))
The AcDbBlockTable::has function will return false if the block Iserased property is true.
(i.e. after you have PURGED the drawing and erased the AcDbBlockReference).
if you then add a block to the block table again instead of setting erase to false on the AcDbBlockReference;
(i.e. from documentation):
"When an entity is erased, it is simply flagged as erased in the block table record. The entity can be unerased with erase(kfalse)."
When you return the blocks ObjectID like this then you can return the ObjectID of the erased object in this code here:
blockId = bt[blockName];
Hence geting a deleted AcDbBlockReference from the database. this means that you need to iterate through the blocktable collection to check that you are not returning an objectID of an erased entity.
Regards,
Evan
Posted by: Evan | June 26, 2008 at 02:55 AM
You shouldn't need a very drastic change: you can open an erased object using tr.GetObject(id,OpenMode.ForRead,true) and check its erased flag. No need for iteration.
This sample is not exhaustive - there may be specific issues that you'll need to address. Feel free to post back any changes you've had to make.
Kean
Posted by: Kean | June 26, 2008 at 12:07 PM
Sorry this was not about the sample, it was more to open up a discusion and awareness about some very unexpected behavour in the Database .net API with no mention in the documentation.
This problem comes from the derived type AcDbSymbolTable and hence creates this same problem in all its derived classes i.e.
AcDbAbstractViewTable
-AcDbViewTable
-AcDbViewportTable
AcDbBlockTable
AcDbDimStyleTable
AcDbLayerTable
AcDbLinetypeTable
AcDbRegAppTable
AcDbTextStyleTable
AcDbUCSTable
Here is what i used to get the latest block definition from the BlockTable.
Public Overloads Shared Function BlockNametoID(ByVal BlockName As String, ByVal tr As Transaction, ByVal bt As BlockTable) As ObjectId
Dim id As ObjectId = Nothing
If bt.Has(BlockName) Then
id = bt.Item(BlockName)
'check it is not erased
If Not id.IsErased Then
Return id
Else
'Else intrate through the collection to find the block latest definition
For Each btrID As ObjectId In bt
If Not btrID.IsErased Then
Using btr As BlockTableRecord = tr.GetObject(btrID, OpenMode.ForRead)
If btr.Name.ToUpper = BlockName.ToUpper Then
id = btrID
Return id
End If
End Using
End If
Next
End If
End If
End Function
Posted by: Evan | June 30, 2008 at 05:51 AM