I received this question some time ago from Paul Richardson from CAD System Engineering:
I have never been sure when to update objects programmatically. An example would be a user edits an entity that I’m tracking and I need to edit another entity in reaction to that change. Is there a standard used to cache the handle, and make the change.
Doesn’t seem editing of entities should be done in the event that is watching for the change. When/how does one do it? Doesn’t seem to be any info on a standard for this.
This is such an excellent question I'm going to spend a number of posts answering it. :-)
Introduction
But first, time for a little nostalgia. One of my first ObjectARX projects was back in 1996 - I'd been using LISP and ADS for several years by that point, but I decided what I really needed was a nice, juicy problem to help me immerse myself in ObjectARX. Along with a colleague at the time, I came up with the idea of using ObjectARX's notification mechanism to link circles together in a chain.
The idea was essentially that you "link" sets of two circles together, and whenever you move one of these circles, the other circle moves in the most direct line to stay attached to it. You would then be able to build up "chains" of linked circles, and the movement of the head of the chain would cause the rest of the chain to follow, with a ripple of notification events modifying one circle after the other.
It was my first major ObjectARX undertaking, so I was fairly heavy-handed with the architecture: each "link" was maintained by two persistent reactors - one attached to each of the linked entities. There were also a number of other reactors and objects involved in the whole system which, in spite of it's weight, worked pretty well. I demoed the sample to developers at a number of different events, to show the power of ObjectARX, and also built it into my first AutoCAD OEM demo application (called SnakeCAD :-).
Anyway - I hadn't thought about this code for several years, but then I received Paul's question and by chance stumbled across the source attached to an old email, so thought I'd spend some time reimplementing the system in .NET. I was able to recode the whole thing in less than a day, partly thanks to the additional experience of being 10 years longer-in-the-tooth, but mainly because of the advantages of using a much more modern development environment.
I'm going to serialize the code over a few posts. The first shows the basic implementation, which should allow you to focus on how the events do their stuff, and I'll later on deal with persistence of our data and some more advanced features (such as automatic linking and support for other circular - even spherical - objects).
The Basic Application
For this application I'm going to try something different, by putting line numbers in the below code (to make the explanation simpler) and providing the main class file as a download.
First, a little on the approach:
The basic application defines one single command - "LINK" (lines 162-194). This command asks the user to select two circles, which it then links together. It does this by using a special "link manager" object (the LinkedObjectManager class is defined from lines 23 to 115), which is used to maintain the references between the various circles.
This LinkedObjectManager stores one-to-many relationships by maintaining a Dictionary, mapping between ObjectIds and ObjectIdCollections. This means that any particular circle can be linked to multiple other circles. The relationships also get added bi-directionally, so the LinkedObjectManager will create a backwards link when it creates the forwards one (lines 37-38).
The linking behaviour is maintained by two main event callbacks: the first is Database.ObjectModified(), which is called whenever an object stored in the active drawing has been changed in some way. This event callback is implemented between lines 196 and 206. All it does is check whether the object that has been modified is one that is being "managed" by our link manager - if so, we add its ID to the list of entities to update later on (the collection that is declared on line 122).
This is really the answer to Paul's question: we store the ObjectId in a list that will get picked up in the Editor.CommandEnded() callback, where we go and update the various objects. My original implementation didn't do that: it opened the objects directly using Open()/Close() (which are marked as obsolete in the .NET API, as we're encouraging the use of Transactions instead), and made the changes right then. Overall the implementation in this version is safer and, I feel, more elegant - CommandEnded() is really the way to go for this kind of operation.
[Aside: for those of you that are ADN members, you should find additional information on this limitation on the ADN website. Here's an article that covers this for VBA, for instance: How to modify an object from object's Modified or document's ObjectAdded, ObjectModified, and so on events.]
The Editor.CommandEnded() callback is implemented between lines 219 and 227, and calls through to another function (UpdateLinkedEntities()) to do the heavy lifting (lines 230-316). This function checks the geometry of the linked objects - I've tried to keep the code fairly generic to make it easier for us to extend this later to handle non-circles - and moves the second one closer to the first one. This in turn fires the Database.ObjectModified() event again, which adds this entity's ObjectId into the list of entities to update. What's interesting about this implementation is that the foreach loop that is making the calls to UpdateLinkedEntities() for each object in the list (lines 222-225), will also take into account the newly added entities. This allows the change to ripple through the entire chain of circles.
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 links between objects
22 /// </summary>
23 public class LinkedObjectManager
24 {
25 Dictionary<ObjectId, ObjectIdCollection> m_dict;
26
27 // Constructor
28 public LinkedObjectManager()
29 {
30 m_dict =
31 new Dictionary<ObjectId,ObjectIdCollection>();
32 }
33
34 // Create a bi-directional link between two objects
35 public void LinkObjects(ObjectId from, ObjectId to)
36 {
37 CreateLink(from, to);
38 CreateLink(to, from);
39 }
40
41 // Helper function to create a one-way
42 // link between objects
43 private void CreateLink(ObjectId from, ObjectId to)
44 {
45 ObjectIdCollection existingList;
46 if (m_dict.TryGetValue(from, out existingList))
47 {
48 if (!existingList.Contains(to))
49 {
50 existingList.Add(to);
51 m_dict.Remove(from);
52 m_dict.Add(from, existingList);
53 }
54 }
55 else
56 {
57 ObjectIdCollection newList =
58 new ObjectIdCollection();
59 newList.Add(to);
60 m_dict.Add(from, newList);
61 }
62 }
63
64 // Remove bi-directional links from an object
65 public void RemoveLinks(ObjectId from)
66 {
67 ObjectIdCollection existingList;
68 if (m_dict.TryGetValue(from, out existingList))
69 {
70 m_dict.Remove(from);
71 foreach (ObjectId id in existingList)
72 {
73 RemoveFromList(id, from);
74 }
75 }
76 }
77
78 // Helper function to remove an object reference
79 // from a list (assumes the overall list should
80 // remain)
81 private void RemoveFromList(
82 ObjectId key,
83 ObjectId toremove
84 )
85 {
86 ObjectIdCollection existingList;
87 if (m_dict.TryGetValue(key, out existingList))
88 {
89 if (existingList.Contains(toremove))
90 {
91 existingList.Remove(toremove);
92 m_dict.Remove(key);
93 m_dict.Add(key, existingList);
94 }
95 }
96 }
97
98 // Return the list of objects linked to
99 // the one passed in
100 public ObjectIdCollection GetLinkedObjects(
101 ObjectId from
102 )
103 {
104 ObjectIdCollection existingList;
105 m_dict.TryGetValue(from, out existingList);
106 return existingList;
107 }
108
109 // Check whether the dictionary contains
110 // a particular key
111 public bool Contains(ObjectId key)
112 {
113 return m_dict.ContainsKey(key);
114 }
115 }
116 /// <summary>
117 /// This class defines our commands and event callbacks.
118 /// </summary>
119 public class LinkingCommands
120 {
121 LinkedObjectManager m_linkManager;
122 ObjectIdCollection m_entitiesToUpdate;
123
124 public LinkingCommands()
125 {
126 Document doc =
127 Application.DocumentManager.MdiActiveDocument;
128 Database db = doc.Database;
129 db.ObjectModified +=
130 new ObjectEventHandler(OnObjectModified);
131 db.ObjectErased +=
132 new ObjectErasedEventHandler(OnObjectErased);
133 doc.CommandEnded +=
134 new CommandEventHandler(OnCommandEnded);
135
136 m_linkManager = new LinkedObjectManager();
137 m_entitiesToUpdate = new ObjectIdCollection();
138 }
139
140 ~LinkingCommands()
141 {
142 try
143 {
144 Document doc =
145 Application.DocumentManager.MdiActiveDocument;
146 Database db = doc.Database;
147 db.ObjectModified -=
148 new ObjectEventHandler(OnObjectModified);
149 db.ObjectErased -=
150 new ObjectErasedEventHandler(OnObjectErased);
151 doc.CommandEnded +=
152 new CommandEventHandler(OnCommandEnded);
153 }
154 catch(System.Exception)
155 {
156 // The document or database may no longer
157 // be available on unload
158 }
159 }
160
161 // Define "LINK" command
162 [CommandMethod("LINK")]
163 public void LinkEntities()
164 {
165 Document doc =
166 Application.DocumentManager.MdiActiveDocument;
167 Database db = doc.Database;
168 Editor ed = doc.Editor;
169
170 PromptEntityOptions opts =
171 new PromptEntityOptions(
172 "\nSelect first circle to link: "
173 );
174 opts.AllowNone = true;
175 opts.SetRejectMessage(
176 "\nOnly circles can be selected."
177 );
178 opts.AddAllowedClass(typeof(Circle), false);
179
180 PromptEntityResult res = ed.GetEntity(opts);
181 if (res.Status == PromptStatus.OK)
182 {
183 ObjectId from = res.ObjectId;
184 opts.Message =
185 "\nSelect second circle to link: ";
186 res = ed.GetEntity(opts);
187 if (res.Status == PromptStatus.OK)
188 {
189 ObjectId to = res.ObjectId;
190 m_linkManager.LinkObjects(from, to);
191 m_entitiesToUpdate.Add(from);
192 }
193 }
194 }
195
196 // Define callback for Database.ObjectModified event
197 private void OnObjectModified(
198 object sender, ObjectEventArgs e)
199 {
200 ObjectId id = e.DBObject.ObjectId;
201 if (m_linkManager.Contains(id) &&
202 !m_entitiesToUpdate.Contains(id))
203 {
204 m_entitiesToUpdate.Add(id);
205 }
206 }
207
208 // Define callback for Database.ObjectErased event
209 private void OnObjectErased(
210 object sender, ObjectErasedEventArgs e)
211 {
212 if (e.Erased)
213 {
214 m_linkManager.RemoveLinks(e.DBObject.ObjectId);
215 }
216 }
217
218 // Define callback for Document.CommandEnded event
219 private void OnCommandEnded(
220 object sender, CommandEventArgs e)
221 {
222 foreach (ObjectId id in m_entitiesToUpdate)
223 {
224 UpdateLinkedEntities(id);
225 }
226 m_entitiesToUpdate.Clear();
227 }
228
229 // Helper function for OnCommandEnded
230 private void UpdateLinkedEntities(ObjectId from)
231 {
232 Document doc =
233 Application.DocumentManager.MdiActiveDocument;
234 Editor ed = doc.Editor;
235 Database db = doc.Database;
236
237 ObjectIdCollection linked =
238 m_linkManager.GetLinkedObjects(from);
239
240 Transaction tr =
241 db.TransactionManager.StartTransaction();
242 using (tr)
243 {
244 try
245 {
246 Point3d firstCenter;
247 Point3d secondCenter;
248 double firstRadius;
249 double secondRadius;
250
251 Entity ent =
252 (Entity)tr.GetObject(from, OpenMode.ForRead);
253
254 if (GetCenterAndRadius(
255 ent,
256 out firstCenter,
257 out firstRadius
258 )
259 )
260 {
261 foreach (ObjectId to in linked)
262 {
263 Entity ent2 =
264 (Entity)tr.GetObject(to, OpenMode.ForRead);
265 if (GetCenterAndRadius(
266 ent2,
267 out secondCenter,
268 out secondRadius
269 )
270 )
271 {
272 Vector3d vec = firstCenter - secondCenter;
273 if (!vec.IsZeroLength())
274 {
275 // Only move the linked circle if it's not
276 // already near enough
277 double apart =
278 vec.Length - (firstRadius + secondRadius);
279 if (apart < 0.0)
280 apart = -apart;
281
282 if (apart > 0.00001)
283 {
284 ent2.UpgradeOpen();
285 ent2.TransformBy(
286 Matrix3d.Displacement(
287 vec.GetNormal() * apart
288 )
289 );
290 }
291 }
292 }
293 }
294 }
295 }
296 catch (System.Exception ex)
297 {
298 Autodesk.AutoCAD.Runtime.Exception ex2 =
299 ex as Autodesk.AutoCAD.Runtime.Exception;
300 if (ex2 != null &&
301 ex2.ErrorStatus != ErrorStatus.WasOpenForUndo)
302 {
303 ed.WriteMessage(
304 "\nAutoCAD exception: {0}", ex2
305 );
306 }
307 else if (ex2 == null)
308 {
309 ed.WriteMessage(
310 "\nSystem exception: {0}", ex
311 );
312 }
313 }
314 tr.Commit();
315 }
316 }
317
318 // Helper function to get the center and radius
319 // for all supported circular objects
320 private bool GetCenterAndRadius(
321 Entity ent,
322 out Point3d center,
323 out double radius
324 )
325 {
326 // For circles it's easy...
327 Circle circle = ent as Circle;
328 if (circle != null)
329 {
330 center = circle.Center;
331 radius = circle.Radius;
332 return true;
333 }
334 else
335 {
336 // Throw in some empty values...
337 // Returning false indicates the object
338 // passed in was not useable
339 center = Point3d.Origin;
340 radius = 0.0;
341 return false;
342 }
343 }
344 }
345 }
Here's what happens when you execute the LINK command on some circles you've drawn...
Some circles:
After the LINK command has been used to link them together, two-by-two:
Now grip-move the head of the chain:
And here's the result - the chain moves to remain attached to the head: