In the previous post we looked at some code that created chains of circles, linking them together using .NET events (a technique that can be used to maintain other types of geometric relationship, of course). In this post, we're going to extend our sample to support persistence of the link data in the AutoCAD database.
Firstly, here's the updated source file. Below I've posted the code, once again with line numbers - but this time the lines that have changed or been added since the previous post are marked in red. This should highlight the modified sections of the code.
Looking at the specific changes, the major updates are to our LinkedObjectManager class: between lines 124 and 454 there's some additional protocol to support persistence. Primarily the obviously named SaveToDatabase() and LoadFromDatabase(), but also some support functions: AddValidatedLinks(), which we use on loading data from the drawing to make sure only valid links get resurrected, and GetLinkDictionaryId(), which we use to identify (and create, if needed) the dictionary we're using to store the link data.
Some information on how the data is being stored: I decided to go ahead and use Xrecords to store the data. Xrecords are flexible, non-graphical data containers that can be stored in dictionaries (of type DBDictionary) in the DWG file. They are also supported natively by AutoCAD, so there's no need for a DBX module to help you access the data. DBDictionaries are basically persistent maps between keys and values. A simple "LINKXREC" gets suffixed by a counter ("0", "1", "2", etc.) to store our Xrecords - this way we know exactly where to look for them.
It's worth taking the trouble of creating nested dictionaries - an outer one for the "company", and an inner one for the "application". The outer one must, of course, be prefixed with your Registered Developer Symbol (RDS) to prevent conflicts with other applications. Having an inner dictionary just gives us greater flexibility if we later choose to extend the amount of custom data we store in the drawing file.
The rest of the changes are to add some simple commands - LOADLINKS and SAVELINKS - to call through to our new persistence protocol. There's also an event handler for BeginSave(), which will automatically put our data into the drawing file when it's about to be saved. This type of automatic persistence is clearly very convenient: an exercise I've left for the reader is to automatically load in the data when it exists. The idea would be to respond to a drawing load event (for instance), check whether our data is there (for which we have a very helpful function, GetLinkDictionaryId()) and then prompt the user whether our data should be loaded (or simply go and do it, depending on the extent to which you want to insulate your users from this kind of decision). The implementation is there, it should be fairly trivial to hook the pieces together.
Another note about the persistence of our data: it should be obvious by now, but we're only using the DBDictionary of Xrecords to store our data - at runtime we use an in-memory dictionary mapping ObjectIds to collections of ObjectIds. This means the data - as you create links between circles - could get out-of-sync with what is stored in the drawing, especially if we were just relying on a command being invoked to save the data.
If you're interested in checking out how the data is stored, you should look at the ArxDbg sample on the ObjectARX SDK (under samples/database/ARXDBG). This invaluable sample takes the lid off the structure of the drawing database, allowing you to see what is stored and where. The sample also contains some very useful code, showing how to use even some of the more obscure parts of ObjectARX.
Here's what we see when we use the SNOOPDB command from the ArxDbg sample to take a look at the contents of our custom dictionary:
Here's the C# code:
1 using System;
2 using System.Collections;
3 using System.Collections.Generic;
4 using Autodesk.AutoCAD.Runtime;
5 using Autodesk.AutoCAD.ApplicationServices;
6 using Autodesk.AutoCAD.DatabaseServices;
7 using Autodesk.AutoCAD.EditorInput;
8 using Autodesk.AutoCAD.Geometry;
9
10 [assembly:
11 CommandClass(
12 typeof(
13 AsdkLinkingLibrary.LinkingCommands
14 )
15 )
16 ]
17
18 namespace AsdkLinkingLibrary
19 {
20 /// <summary>
21 /// Utility class to manage and save links
22 /// between objects
23 /// </summary>
24 public class LinkedObjectManager
25 {
26 const string kCompanyDict =
27 "AsdkLinks";
28 const string kApplicationDict =
29 "AsdkLinkedObjects";
30 const string kXrecPrefix =
31 "LINKXREC";
32
33 Dictionary<ObjectId, ObjectIdCollection> m_dict;
34
35 // Constructor
36 public LinkedObjectManager()
37 {
38 m_dict =
39 new Dictionary<ObjectId,ObjectIdCollection>();
40 }
41
42 // Create a bi-directional link between two objects
43 public void LinkObjects(ObjectId from, ObjectId to)
44 {
45 CreateLink(from, to);
46 CreateLink(to, from);
47 }
48
49 // Helper function to create a one-way
50 // link between objects
51 private void CreateLink(ObjectId from, ObjectId to)
52 {
53 ObjectIdCollection existingList;
54 if (m_dict.TryGetValue(from, out existingList))
55 {
56 if (!existingList.Contains(to))
57 {
58 existingList.Add(to);
59 m_dict.Remove(from);
60 m_dict.Add(from, existingList);
61 }
62 }
63 else
64 {
65 ObjectIdCollection newList =
66 new ObjectIdCollection();
67 newList.Add(to);
68 m_dict.Add(from, newList);
69 }
70 }
71
72 // Remove bi-directional links from an object
73 public void RemoveLinks(ObjectId from)
74 {
75 ObjectIdCollection existingList;
76 if (m_dict.TryGetValue(from, out existingList))
77 {
78 m_dict.Remove(from);
79 foreach (ObjectId id in existingList)
80 {
81 RemoveFromList(id, from);
82 }
83 }
84 }
85
86 // Helper function to remove an object reference
87 // from a list (assumes the overall list should
88 // remain)
89 private void RemoveFromList(
90 ObjectId key,
91 ObjectId toremove
92 )
93 {
94 ObjectIdCollection existingList;
95 if (m_dict.TryGetValue(key, out existingList))
96 {
97 if (existingList.Contains(toremove))
98 {
99 existingList.Remove(toremove);
100 m_dict.Remove(key);
101 m_dict.Add(key, existingList);
102 }
103 }
104 }
105
106 // Return the list of objects linked to
107 // the one passed in
108 public ObjectIdCollection GetLinkedObjects(
109 ObjectId from
110 )
111 {
112 ObjectIdCollection existingList;
113 m_dict.TryGetValue(from, out existingList);
114 return existingList;
115 }
116
117 // Check whether the dictionary contains
118 // a particular key
119 public bool Contains(ObjectId key)
120 {
121 return m_dict.ContainsKey(key);
122 }
123
124 // Save the link information to a special
125 // dictionary in the database
126 public void SaveToDatabase(Database db)
127 {
128 Transaction tr =
129 db.TransactionManager.StartTransaction();
130 using (tr)
131 {
132 ObjectId dictId =
133 GetLinkDictionaryId(db, true);
134 DBDictionary dict =
135 (DBDictionary)tr.GetObject(
136 dictId,
137 OpenMode.ForWrite
138 );
139 int xrecCount = 0;
140
141 foreach (
142 KeyValuePair<ObjectId, ObjectIdCollection> kv
143 in m_dict
144 )
145 {
146 // Prepare the result buffer with our data
147 ResultBuffer rb =
148 new ResultBuffer(
149 new TypedValue(
150 (int)DxfCode.SoftPointerId,
151 kv.Key
152 )
153 );
154 int i = 1;
155 foreach (ObjectId id in kv.Value)
156 {
157 rb.Add(
158 new TypedValue(
159 (int)DxfCode.SoftPointerId + i,
160 id
161 )
162 );
163 i++;
164 }
165
166 // Update or create an xrecord to store the data
167 Xrecord xrec;
168 bool newXrec = false;
169 if (dict.Contains(
170 kXrecPrefix + xrecCount.ToString()
171 )
172 )
173 {
174 // Open the existing object
175 DBObject obj =
176 tr.GetObject(
177 dict.GetAt(
178 kXrecPrefix + xrecCount.ToString()
179 ),
180 OpenMode.ForWrite
181 );
182 // Check whether it's an xrecord
183 xrec = obj as Xrecord;
184 if (xrec == null)
185 {
186 // Should never happen
187 // We only store xrecords in this dict
188 obj.Erase();
189 xrec = new Xrecord();
190 newXrec = true;
191 }
192 }
193 // No object existed - create a new one
194 else
195 {
196 xrec = new Xrecord();
197 newXrec = true;
198 }
199 xrec.XlateReferences = true;
200 xrec.Data = (ResultBuffer)rb;
201 if (newXrec)
202 {
203 dict.SetAt(
204 kXrecPrefix + xrecCount.ToString(),
205 xrec
206 );
207 tr.AddNewlyCreatedDBObject(xrec, true);
208 }
209 xrecCount++;
210 }
211
212 // Now erase the left-over xrecords
213 bool finished = false;
214 do
215 {
216 if (dict.Contains(
217 kXrecPrefix + xrecCount.ToString()
218 )
219 )
220 {
221 DBObject obj =
222 tr.GetObject(
223 dict.GetAt(
224 kXrecPrefix + xrecCount.ToString()
225 ),
226 OpenMode.ForWrite
227 );
228 obj.Erase();
229 }
230 else
231 {
232 finished = true;
233 }
234 xrecCount++;
235 } while (!finished);
236 tr.Commit();
237 }
238 }
239
240 // Load the link information from a special
241 // dictionary in the database
242 public void LoadFromDatabase(Database db)
243 {
244 Document doc =
245 Application.DocumentManager.MdiActiveDocument;
246 Editor ed = doc.Editor;
247 Transaction tr =
248 db.TransactionManager.StartTransaction();
249 using (tr)
250 {
251 // Try to find the link dictionary, but
252 // do not create it if one isn't there
253 ObjectId dictId =
254 GetLinkDictionaryId(db, false);
255 if (dictId.IsNull)
256 {
257 ed.WriteMessage(
258 "\nCould not find link dictionary."
259 );
260 return;
261 }
262
263 // By this stage we can assume the dictionary exists
264 DBDictionary dict =
265 (DBDictionary)tr.GetObject(
266 dictId, OpenMode.ForRead
267 );
268 int xrecCount = 0;
269 bool done = false;
270
271 // Loop, reading the xrecords one-by-one
272 while (!done)
273 {
274 if (dict.Contains(
275 kXrecPrefix + xrecCount.ToString()
276 )
277 )
278 {
279 ObjectId recId =
280 dict.GetAt(
281 kXrecPrefix + xrecCount.ToString()
282 );
283 DBObject obj =
284 tr.GetObject(recId, OpenMode.ForRead);
285 Xrecord xrec = obj as Xrecord;
286 if (xrec == null)
287 {
288 ed.WriteMessage(
289 "\nDictionary contains non-xrecords."
290 );
291 return;
292 }
293 int i = 0;
294 ObjectId from = new ObjectId();
295 ObjectIdCollection to =
296 new ObjectIdCollection();
297 foreach (TypedValue val in xrec.Data)
298 {
299 if (i == 0)
300 from = (ObjectId)val.Value;
301 else
302 {
303 to.Add((ObjectId)val.Value);
304 }
305 i++;
306 }
307 // Validate the link info and add it to our
308 // internal data structure
309 AddValidatedLinks(db, from, to);
310 xrecCount++;
311 }
312 else
313 {
314 done = true;
315 }
316 }
317 tr.Commit();
318 }
319 }
320
321 // Helper function to validate links before adding
322 // them to the internal data structure
323 private void AddValidatedLinks(
324 Database db,
325 ObjectId from,
326 ObjectIdCollection to
327 )
328 {
329 Document doc =
330 Application.DocumentManager.MdiActiveDocument;
331 Editor ed = doc.Editor;
332 Transaction tr =
333 db.TransactionManager.StartTransaction();
334 using (tr)
335 {
336 try
337 {
338 ObjectIdCollection newList =
339 new ObjectIdCollection();
340
341 // Open the "from" object
342 DBObject obj =
343 tr.GetObject(from, OpenMode.ForRead, false);
344 if (obj != null)
345 {
346 // Open each of the "to" objects
347 foreach (ObjectId id in to)
348 {
349 DBObject obj2;
350 try
351 {
352 obj2 =
353 tr.GetObject(id, OpenMode.ForRead, false);
354 // Filter out the erased "to" objects
355 if (obj2 != null)
356 {
357 newList.Add(id);
358 }
359 }
360 catch (System.Exception)
361 {
362 ed.WriteMessage(
363 "\nFiltered out link to an erased object."
364 );
365 }
366 }
367 // Only if the "from" object and at least
368 // one "to" object exist (and are unerased)
369 // do we add an entry for them
370 if (newList.Count > 0)
371 {
372 m_dict.Add(from, newList);
373 }
374 }
375 }
376 catch (System.Exception)
377 {
378 ed.WriteMessage(
379 "\nFiltered out link from an erased object."
380 );
381 }
382 tr.Commit();
383 }
384 }
385
386 // Helper function to get (optionally create)
387 // the nested dictionary for our xrecord objects
388 private ObjectId GetLinkDictionaryId(
389 Database db,
390 bool createIfNotExisting
391 )
392 {
393 ObjectId appDictId = ObjectId.Null;
394
395 Transaction tr =
396 db.TransactionManager.StartTransaction();
397 using (tr)
398 {
399 DBDictionary nod =
400 (DBDictionary)tr.GetObject(
401 db.NamedObjectsDictionaryId,
402 OpenMode.ForRead
403 );
404 // Our outer level ("company") dictionary
405 // does not exist
406 if (!nod.Contains(kCompanyDict))
407 {
408 if (!createIfNotExisting)
409 return ObjectId.Null;
410
411 // Create both the "company" dictionary...
412 DBDictionary compDict = new DBDictionary();
413 nod.UpgradeOpen();
414 nod.SetAt(kCompanyDict, compDict);
415 tr.AddNewlyCreatedDBObject(compDict, true);
416
417 // ... and the inner "application" dictionary.
418 DBDictionary appDict = new DBDictionary();
419 appDictId =
420 compDict.SetAt(kApplicationDict, appDict);
421 tr.AddNewlyCreatedDBObject(appDict, true);
422 }
423 else
424 {
425 // Our "company" dictionary exists...
426 DBDictionary compDict =
427 (DBDictionary)tr.GetObject(
428 nod.GetAt(kCompanyDict),
429 OpenMode.ForRead
430 );
431 /// So check for our "application" dictionary
432 if (!compDict.Contains(kApplicationDict))
433 {
434 if (!createIfNotExisting)
435 return ObjectId.Null;
436
437 // Create the "application" dictionary
438 DBDictionary appDict = new DBDictionary();
439 compDict.UpgradeOpen();
440 appDictId =
441 compDict.SetAt(kApplicationDict, appDict);
442 tr.AddNewlyCreatedDBObject(appDict, true);
443 }
444 else
445 {
446 // Both dictionaries already exist...
447 appDictId = compDict.GetAt(kApplicationDict);
448 }
449 }
450 tr.Commit();
451 }
452 return appDictId;
453 }
454 }
455
456 /// <summary>
457 /// This class defines our commands and event callbacks.
458 /// </summary>
459 public class LinkingCommands
460 {
461 LinkedObjectManager m_linkManager;
462 ObjectIdCollection m_entitiesToUpdate;
463
464 public LinkingCommands()
465 {
466 Document doc =
467 Application.DocumentManager.MdiActiveDocument;
468 Database db = doc.Database;
469 db.ObjectModified +=
470 new ObjectEventHandler(OnObjectModified);
471 db.ObjectErased +=
472 new ObjectErasedEventHandler(OnObjectErased);
473 db.BeginSave +=
474 new DatabaseIOEventHandler(OnBeginSave);
475 doc.CommandEnded +=
476 new CommandEventHandler(OnCommandEnded);
477
478 m_linkManager = new LinkedObjectManager();
479 m_entitiesToUpdate = new ObjectIdCollection();
480 }
481
482 ~LinkingCommands()
483 {
484 try
485 {
486 Document doc =
487 Application.DocumentManager.MdiActiveDocument;
488 Database db = doc.Database;
489 db.ObjectModified -=
490 new ObjectEventHandler(OnObjectModified);
491 db.ObjectErased -=
492 new ObjectErasedEventHandler(OnObjectErased);
493 db.BeginSave -=
494 new DatabaseIOEventHandler(OnBeginSave);
495 doc.CommandEnded +=
496 new CommandEventHandler(OnCommandEnded);
497 }
498 catch(System.Exception)
499 {
500 // The document or database may no longer
501 // be available on unload
502 }
503 }
504
505 // Define "LINK" command
506 [CommandMethod("LINK")]
507 public void LinkEntities()
508 {
509 Document doc =
510 Application.DocumentManager.MdiActiveDocument;
511 Database db = doc.Database;
512 Editor ed = doc.Editor;
513
514 PromptEntityOptions opts =
515 new PromptEntityOptions(
516 "\nSelect first circle to link: "
517 );
518 opts.AllowNone = true;
519 opts.SetRejectMessage(
520 "\nOnly circles can be selected."
521 );
522 opts.AddAllowedClass(typeof(Circle), false);
523
524 PromptEntityResult res = ed.GetEntity(opts);
525 if (res.Status == PromptStatus.OK)
526 {
527 ObjectId from = res.ObjectId;
528 opts.Message =
529 "\nSelect second circle to link: ";
530 res = ed.GetEntity(opts);
531 if (res.Status == PromptStatus.OK)
532 {
533 ObjectId to = res.ObjectId;
534 m_linkManager.LinkObjects(from, to);
535 m_entitiesToUpdate.Add(from);
536 }
537 }
538 }
539
540 // Define "LOADLINKS" command
541 [CommandMethod("LOADLINKS")]
542 public void LoadLinkSettings()
543 {
544 Document doc =
545 Application.DocumentManager.MdiActiveDocument;
546 Database db = doc.Database;
547 m_linkManager.LoadFromDatabase(db);
548 }
549
550 // Define "SAVELINKS" command
551 [CommandMethod("SAVELINKS")]
552 public void SaveLinkSettings()
553 {
554 Document doc =
555 Application.DocumentManager.MdiActiveDocument;
556 Database db = doc.Database;
557 m_linkManager.SaveToDatabase(db);
558 }
559
560 // Define callback for Database.ObjectModified event
561 private void OnObjectModified(
562 object sender, ObjectEventArgs e)
563 {
564 ObjectId id = e.DBObject.ObjectId;
565 if (m_linkManager.Contains(id) &&
566 !m_entitiesToUpdate.Contains(id))
567 {
568 m_entitiesToUpdate.Add(id);
569 }
570 }
571
572 // Define callback for Database.ObjectErased event
573 private void OnObjectErased(
574 object sender, ObjectErasedEventArgs e)
575 {
576 if (e.Erased)
577 {
578 m_linkManager.RemoveLinks(e.DBObject.ObjectId);
579 }
580 }
581
582 // Define callback for Database.BeginSave event
583 void OnBeginSave(object sender, DatabaseIOEventArgs e)
584 {
585 Database db = sender as Database;
586 if (db != null)
587 {
588 m_linkManager.SaveToDatabase(db);
589 }
590 }
591
592 // Define callback for Document.CommandEnded event
593 private void OnCommandEnded(
594 object sender, CommandEventArgs e)
595 {
596 foreach (ObjectId id in m_entitiesToUpdate)
597 {
598 UpdateLinkedEntities(id);
599 }
600 m_entitiesToUpdate.Clear();
601 }
602
603 // Helper function for OnCommandEnded
604 private void UpdateLinkedEntities(ObjectId from)
605 {
606 Document doc =
607 Application.DocumentManager.MdiActiveDocument;
608 Editor ed = doc.Editor;
609 Database db = doc.Database;
610
611 ObjectIdCollection linked =
612 m_linkManager.GetLinkedObjects(from);
613
614 Transaction tr =
615 db.TransactionManager.StartTransaction();
616 using (tr)
617 {
618 try
619 {
620 Point3d firstCenter;
621 Point3d secondCenter;
622 double firstRadius;
623 double secondRadius;
624
625 Entity ent =
626 (Entity)tr.GetObject(from, OpenMode.ForRead);
627
628 if (GetCenterAndRadius(
629 ent,
630 out firstCenter,
631 out firstRadius
632 )
633 )
634 {
635 foreach (ObjectId to in linked)
636 {
637 Entity ent2 =
638 (Entity)tr.GetObject(to, OpenMode.ForRead);
639 if (GetCenterAndRadius(
640 ent2,
641 out secondCenter,
642 out secondRadius
643 )
644 )
645 {
646 Vector3d vec = firstCenter - secondCenter;
647 if (!vec.IsZeroLength())
648 {
649 // Only move the linked circle if it's not
650 // already near enough
651 double apart =
652 vec.Length - (firstRadius + secondRadius);
653 if (apart < 0.0)
654 apart = -apart;
655
656 if (apart > 0.00001)
657 {
658 ent2.UpgradeOpen();
659 ent2.TransformBy(
660 Matrix3d.Displacement(
661 vec.GetNormal() * apart
662 )
663 );
664 }
665 }
666 }
667 }
668 }
669 }
670 catch (System.Exception ex)
671 {
672 Autodesk.AutoCAD.Runtime.Exception ex2 =
673 ex as Autodesk.AutoCAD.Runtime.Exception;
674 if (ex2 != null &&
675 ex2.ErrorStatus != ErrorStatus.WasOpenForUndo)
676 {
677 ed.WriteMessage(
678 "\nAutoCAD exception: {0}", ex2
679 );
680 }
681 else if (ex2 == null)
682 {
683 ed.WriteMessage(
684 "\nSystem exception: {0}", ex
685 );
686 }
687 }
688 tr.Commit();
689 }
690 }
691
692 // Helper function to get the center and radius
693 // for all supported circular objects
694 private bool GetCenterAndRadius(
695 Entity ent,
696 out Point3d center,
697 out double radius
698 )
699 {
700 // For circles it's easy...
701 Circle circle = ent as Circle;
702 if (circle != null)
703 {
704 center = circle.Center;
705 radius = circle.Radius;
706 return true;
707 }
708 else
709 {
710 // Throw in some empty values...
711 // Returning false indicates the object
712 // passed in was not useable
713 center = Point3d.Origin;
714 radius = 0.0;
715 return false;
716 }
717 }
718 }
719 }
Now for an example of what happens when you reload a drawing without calling the LOADLINKS functionality...
Firstly, we create our chain of circles and SAVE (no need to call SAVELINKS specifically, because of our BeginSave() event handler):
If we close and reopen the drawing file, we see that when we stretch one of the circles, the links are not in place:
If we then call LOADLINKS, to resurrect our links, we see that when we stretch the head of the chain, the links are there, once again:
That's it for now - in the next post we'll take a look at adding support for automatic linking on object creation.