Reactors-Making Aware Programs1 Sep, 1999 By: Bill Kramer
Get this code!. Or refer to the listings at the bottom of this page.
Application programs tend to be simple-minded once the program returns control to the AutoCAD user. At this point, the program has no way of knowing what the user does to the drawing. But the application that runs over and over again or has multiple restart points needs to know what happens to the drawing in between restarts. In more sophisticated cases, applications interrogate the geometry when they restart. Of course, this practice can quickly come to involve sometimes complex and tricky coding. And if the required geometry is compromised, then the application may not operate. So, you must make the application more aware of what goes on in between controlled operations.
To make an application aware of program events, use the tools in Visual LISP called reactors. Reactors keep an application up to speed on system events. The reactors discussed here are available in the AutoCAD 2000 environment.
Reactors are relatively easy to create and maintain-once they are understood. In actuality they are easier to program than dialog boxes both in the amount of code required and in the preparations needed when designing an application. They also provide access to a level of AutoCAD that past AutoLISP programmers could only dream of.
A reactor is a function or program that reacts to some sort of event inside the computer system. Reactors can respond to events defined by the host application (in this case, AutoCAD). Command activities, database updates, object changes and more are defined events that can result in handing system control to your program modules. This means that as AutoCAD is about to do or has just completed doing something, your program can take over and do something on its own while another event is taking place. This is an awesome responsibility to give a program, and giving it should not be taken lightly by programmers. Your program must behave properly; otherwise, AutoCAD can do unpredictable things or your program can cause a never-ending loop of reactors to begin. Users might also dislike your program because it interferes in places it should not (from their perspective that is!). With a little preparation and a lot of common sense, good reactor program modules can be developed that will greatly enhance the integration of applications that need more intimate control over AutoCAD.
The host-programming environment defines which reactors are available to the applications developer. The applications programmer then decides which, if any, of the reactors is useful in the context of the program being developed. AutoCAD makes a variety of reactors available. Table 1 lists the basic reactor categories. Use these reactors to obtain a higher level of control of an application and the AutoCAD environment in general. Have your program notified when entities are modified, the current drawing is changed, an AutoCAD command is issued and so forth. This greatly increases your applications ability to keep track of events even though it does not appear to be running.
Reactors can expand the way an application interfaces with the user. The traditional application program obtains input in one fashion or another from the user, processes it and generates some form of output. During this entire process, it is not unusual for the application to maintain complete control of the user interface and not allow AutoCAD commands to take place. Also, complex applications often involve multiple start points that allow the user to start a part of the application, exit and do some basic AutoCAD manipulations, restart the application and then repeat the process until the drawing is completed. When using reactors, the application no longer needs to check existing geometry as it already knows what has happened. If a critical geometry component is changed, then a flag can be set so that when the main application restarts it can take the appropriate action.
Reactors also open the door for development of new application interfaces. For example, a reactor-based system can be used to keep track of operator productivity or command usage. The only way a nonreactor type of program can accomplish this feat is to reassign the entire command set in AutoCAD to AutoLISP functions. This is a tedious exercise that causes the application to lose track of command usage inside other AutoLISP functions. To further complicate the issue, suppose that you want to count all of the added and the removed objects from the drawing database while tracking the command usage. This can be difficult to implement without reactors. It is also nearly impossible to achieve reliability without them because objects can be added or removed in between checks of the database. But by using a reactor-based approach, this application becomes an easy example that can be seen in Listings 1-5.
Listing 1 contains the command function (C:COUNTER) that is used to start the application for counting command and database activity. This function sets up the AutoCAD command and database reactors to notify the application when specific events occur. The events to watch are command starts and database additions and deletions. But before going any further, you must first enable the reactor system in Visual LISP.
Enabling the Reactor System
Because event reactors are not used in all applications and the supporting functions needed to enable this level of functionality to the Visual LISP programmer are so large, the runtime library elements required must be loaded under programmer control. The (VL-LOAD-COM) subr loads the extended library of Visual LISP that enables a host of functions that permit the use of reactors and ActiveX type programming interfaces. Loading only takes place the first time (VL-LOAD-COM) is called. Once loaded, a repeat call to the loader function results in an immediate return. Thus, the beginning of all Visual LISP reactor programs contains the (VL-LOAD-COM) expression in order to enable the runtime modules needed to complete the tasks. On some machines it can take Visual LISP a few seconds to load the extended library, so don't be surprised if the system is busy for a bit when first starting an application that uses the library.
Another housekeeping task that takes place at the beginning of the (C:COUNTER) function is the removal of any occurrences of the reactor objects that already exist in the drawing. Reactors are first added to a list, and once added, they must be explicitly removed. Assigning the same reactor again means that your reactor runs twice and that may not produce the desired results.
The function (Reactor_Remove) is shown in Listing 2. This function removes all reactors that have the same name data property. The programmer assigns the reactor's name when created, and it is a string. All the reactors created in the counter example are named "CntCommands," and the function in Listing 2 removes them from the list of reactors in the drawing.
The function (Reactor_Remove) in Listing 2 demonstrates several of the reactor functions in Visual LISP. The (VLR-Reactors) subr returns a list of reactors currently defined in the drawing. The list returned is a nested list where each member contains the type or classification of the reactor and the reactor objects attached to that reactor. There can be many reactors attached to a drawing, and so this function continues by looping though the reactor objects list and getting the data or name of each reactor using (VLR-Data). If the name equals the one to be removed, the (VLR-Remove) subr is used to stop the reactor. (VLR-Remove) removes the reactor from the event list inside AutoCAD (thereby disabling it). This utility function (Reactor_Remove) can be very useful when working with reactors.
Getting back to the (C:COUNTER) function, the reactors "CntCommands" are removed by a call to our toolbox utility (Reactor_Remove). That means you can add the reactors knowing they are the only ones active. I highly recommend this type of housecleaning. If you run (C:COUNTER) again in the same drawing, the reactors are called twice when the specified events take place. By removing them first, you know they are only called once.
Reactors are attached using the (VLR-xxx-Reactor) subr. The "xxx" varies depending on the type of reactor being defined. In (C:COUNTER), you define two types of reactors. The first is a command reactor, and it is defined using (VLR-Command-Reactor). There are two parameters to the (VLR-Command-Reactor): the name of the reactor and a list of reaction links. The name references the reaction in your program at a later time (as shown already when earlier reactors were removed using the name "CntCommands").
The reaction links are presented as a nested list containing dotted pairs. Each dotted pair list contains the name of the event and the function name to run when the associated event takes place. The events are all named starting with a colon character as in :VLR-CommandWillStart. The first characters are always ":VLR" and are followed by a descriptive name of the event. In this example, the event takes place as an AutoCAD command starts. The event or reaction names vary for each reactor type. The events associated with the command processor are shown in Table 2. Other event names for the other reactors can be found using the online help in Visual LISP.
Reading the (VLR-Command-Reactor) expression shows that the function (CountCommands) is attached to the event that a command starts. The (CountCommands) function is part of our function set and is shown in Listing 3. This function runs each time an AutoCAD command starts.
The function parameters in Listing 3 show the name of the reactor that calls the function and a list. The list parameter content for each callback function varies depending on the context of the callback. In the case of most command reactors, the parameter list contains only the command name as a string.
The counting application maintains an association list containing the names of the commands used and a count of command usage. You can find this list in MyCounter. If the command name already exists in the list, the counter value is incremented. Otherwise, a new entry is added to the list MyCounter using the command name and a value of 1 for the count. The callback function only maintains the list; it does not do any output or user interaction. In fact, callback functions should be quick and efficient at what they do; otherwise, they may bog down the system. Callback functions should not issue other commands that cause the callback function to run again as this could result in an infinite loop. And when a callback function gets stuck in an infinite loop, nothing else can happen in AutoCAD.
Looking back at the (C:COUNTER) function, notice that it prepares another reactor set for the AutoCAD database. It abbreviates the AutoCAD database name to AcDb and establishes the (VLR-AcDb-Reactor) function as the reactor linkage. These reactors also use the same reactor name-CntCommands. This occurs so that the remove reactor function removes these as well as the command reactor defined earlier.
The database reactors are associated with events that can take place in the AutoCAD drawing database. They use two events here: one notifies the application of an object added to the drawing, and the second indicates an object deleted from the database. The function names (CountAdd) and (CountDel) refer to callback functions that must run when these events take place. Both of these functions simply increment counters and return, as shown in Listing 4.
To complete our simple command and database monitor application, Listing 5 contains a function that prints the results of the data collected thus far. The function (C:COUNTS) reports each of the AutoCAD commands and the number of times used, as stored in the MyCounter list. It then displays how many objects were added and removed from the drawing database.
The use of reactors is not difficult, and with a little imagination you can expand this utility example into a management tool for monitoring user productivity or for a billing system that records user activity.
|Table 1. AutoCAD Reactor Types|
Database Reactors These react when objects are added, modified or erased from the drawing database.
Table 2. Command Reactor Events
:vlr-unknownCommand This event happens when an unknown command is issued.
Entity Object Reactors
Reactors can also keep track of entity objects created by your application. Reactors can be used to link these objects and to keep them related in some programmatic fashion. To illustrate, consider an application where two circles have a connecting line object. When the circles are modified, the line automatically regenerates and forms a direct connection between the two circles. Developing this using conventional AutoLISP is out of the question. But if you use the reactor system of Visual LISP, it is very easy to implement, as shown in Listings 6-8.
The function (C:CONNECT) in Listing 6 sets up the connection between two circles selected by the operator. Each time the (C:CONNECT) function is run, the operator selects two circles, a unique reactor assignment is made for the two circles and a line is created between the two. After obtaining the selection of two circles from the operator, the (VLR-LOAD-COM) subr is run to load the support library, and the line connection is created. Shown in Listing 7, (Connection) simply draws a line between two circle entities returning the entity name of the line just created. The entity names are then converted to Visual LISP objects for use in the reactor system.
Visual LISP Objects are data types that are closer to the ones used in the ObjectARX environment. Visual LISP heavily relies on this environment. As a result, the conversion utilities (VLAX-Ename>VLA-Object) and (VLAX-VLA-Object->Ename) are used frequently to convert between the two forms of entity referencing. It's a matter of programmer preference whether to use entity lists or Visual LISP function calls to obtain the details of an individual object. If you intend to manipulate the entity data extensively, then the entity list may offer a way to perform the changes using less typing as opposed to repeated calls to the object properties. The difference in speed does favor the property approach; however, the difference is negligible. In the example function (C:CONNECT), I use the entity names as the user supplied them and then convert them to VLA objects that connect the reactors. So as not to introduce too many new concepts at once, let's use entity list manipulation throughout the majority of this application example.
(VLR-Object-Reactor) is then used to link the AutoLISP function (ConnectFix) to the entity-modified event. This way, whenever one of the three entities is modified, it calls the event callback function. Note that the name of the reaction is a string defined as the concatenation of the constant Connect Circles and an incrementing counter. This was done so that each reactor has a unique name for later referencing in the callback function itself.
The callback for the connecting circles change event is shown in Listing 8. There are three parameters passed to callback functions associated with entity objects. The first is the object that caused the notification. This will be one of the two circles or the line. The object was changed in some way and the callback function invoked. The second parameter is the reactor object. The reactor object can be used with the (VLR-Owners) subr to obtain a list of the objects associated with the reactor object. Lastly there is the parameter list that varies from one callback type to another. In this case, it contains nothing of interest to our application and will be ignored. It is still required to be present in the callback function definition even if it is not used or contains no information.
The first thing to do in the callback function is check the value of a global flag variable named Connect_Flag. The callback proceeds if the flag is set to True. The callback returns immediately if the flag is set to nil. This prevents the callback from processing entities changed in the callback function itself. Here is the situation, our callback function runs if one of the three entities is modified. While it runs, it modifies the line object. This action causes the callback function to get caught in a never-ending loop. You don't want this to happen, and the global flag setting helps to prevent it from happening. At the end of the callback function after the changes are all made set the flag back to True. Now, the next time one of the objects is changed, the callback processes the entities.
Inside the callback function, the list of owners is obtained for the reactor object. The owners are a list of VLA Objects related to the reactor. Another way to look at it is that these are the entities that cause the event reactor to run. In this application it is always two circles and a line as associated in the reactor definition earlier.
The (ConnectFix) function then loops through entity objects in the ObjList and gets the entity data list for each of them. When encountered, it saves the entity list for the line, and a test takes place to see if the notification object is the line. If it is, then the update will not take place because the circles were not changed. A flag named SkipIt is set to True if the line object is the notifying object. If the object is one of the circles, then the variables P1, P2, R1 and R2 are established from the entity list data. These values are then used to update the end points of the connecting line.
This function set demonstrates how quickly you can develop reactors that keep track of the objects your application must maintain. You can connect multiple circles in this example to see what happens as the circles are individually and jointly manipulated. What may surprise you is how quickly this operation happens since the AutoLISP routines run as soon as the AutoCAD edit is completed.
Obviously the functions shown here are minimal. They have no error checks and do not respond to all possible situations. The intent is to provide simple examples showing how reactors solve application problems. Reactors provide a way for applications to integrate closely into the AutoCAD system to form even more seamless interfaces than ever before. And they are not that difficult to implement using Visual LISP. Until next time, keep on programmin'!
;;============================================= ;; Listing 1 - Set up counter reactors ;; (defun C:COUNTER () ;; (vl-load-com) ;;load reactor handling system ;; ;; Clear any current command reactors (Reactor_Remove "CntCommands") ;; ;; Define reactor link to AutoCAD (setq CommReact (vlr-command-reactor "CntCommands" '((:vlr-CommandWillStart . CountCommands))) AcDbReact (vlr-AcDb-Reactor "CntCommands" '((:vlr-ObjectAppended . CountAdd) (:vlr-ObjectErased . CountDel))) ) ;; ;; Initialize variables (setq MyCounter nil AddCounter 0 DelCounter 0) ;; ;; Define as persistant - will remain ;; with drawing and require reloading ;; of modules when drawing reloaded. ;;(vlr-pers CommReact) ;; (prompt "\nCOUNTER reactor is now ready.") (princ) )
;;============================================= ;; ;; Listing 2: Remove reactors by name ;; (defun Reactor_Remove ( Nam / ReactorsInDwg ReactorGroup ReactorObject ) (setq ReactorsInDwg (VLR-Reactors)) (foreach ReactorGroup ReactorsInDwg (foreach ReactorObject (cdr ReactorGroup) (if (= (VLR-Data ReactorObject) Nam) (VLR-Remove ReactorObject)))) )
;;============================================= ;; ;; Listing 3: Command reactor call back function ;; (defun CountCommands (Reactor-name Nam / TMP1 TMP2) (setq Nam (car Nam) ;;we only want the command TMP1 (assoc Nam MyCounter)) (if TMP1 ;;found in list already? (setq TMP2 (list Nam (1+ (cadr TMP1))) MyCounter (subst TMP2 TMP1 MyCounter)) (setq TMP1 (list Nam 1) MyCounter (cons TMP1 MyCounter)) ) )
;;============================================= ;; ;; Listing 4: Database Call back functions ;; (defun CountAdd (Reactor-Name Data) (setq AddCounter (1+ AddCounter)) ) ;; (defun CountDel (Reactor-Name Data) (setq DelCounter (1+ DelCounter)) )
;;============================================= ;; ;; Listing 5: Reporting function to list contents of MYCOUNTER ;; (defun C:COUNTS ( / TMP) (prompt "\nCommands counted.") (foreach TMP MyCounter (prompt (strcat "\n" (car TMP) "\t" (itoa (cadr TMP))))) (prompt (strcat "\n" (itoa AddCounter) " objects added and " (itoa DelCounter) " objects removed.")) (princ) )
;;============================================= ;; ;; Listing 6: Connect circles with a line ;; (defun C:CONNECT () (setq EN1 (car (entsel "\nPick a circle: ")) EN2 (car (entsel " and another: ")) RCnt (if RCnt (1+ RCnt) 1) ) (if (and EN1 EN2) (progn (vl-load-com) (setq EN3 (Connection EN1 EN2) EN3 (vlax-ename->vla-object EN3) EN1 (vlax-ename->vla-object EN1) EN2 (vlax-ename->vla-object EN2) ) (vlr-object-reactor (list EN1 EN2 EN3) (strcat "Connect Circles " (itoa RCnt)) '((:vlr-modified . ConnectFix) ;(:vlr-erased . ConnectKill) ) ) ) ) )
;;============================================= ;; ;; Listing 7: Drawing line between circles ;; (defun Connection (EN1 EN2 / EL1 EL2) (setq EN1 (if (= (type EN1) 'EName) EN1 (vlax-vla-object->ename EN1)) EN2 (if (= (type EN2) 'Ename) EN2 (vlax-vla-object->ename EN2)) EL1 (entget EN1) EL2 (entget EN2) R1 (cdr (assoc 40 EL1)) R2 (cdr (assoc 40 EL2)) P1 (cdr (assoc 10 EL1)) P2 (cdr (assoc 10 EL2)) A1 (angle P1 P2) P1 (polar P1 A1 R1) P2 (polar P2 (+ A1 PI) R2) ) (entmake (list '(0 . "LINE") (assoc 8 EL1) (cons 10 P1) (cons 11 P2) ) ) (entlast) )
;;============================================= ;; ;; Listing 8: Entity Object Callback function ;; (defun ConnectFix ( Not_Obj ;;caused notification Re_Obj ;;reactor object PList ;;parameters list / ObjList ;;objects in reactor set VObj ;;VLA object EN ;;Entity name EL ;;Entity list ENL ;;Entity list for line P1 ;;Center/end point 1 P2 ;;Center/end point 2 R1 ;;Radius 1 R2 ;;Radius 2 SkipIt ;;Process change flag ) ;; ;;Get list of objects associated with the ;;reactor that caused the call back. ;; (setq ObjList (vlr-owners Re_Obj)) ;; ;;Remove reactor for now, modification to ;;the line object will cause it to be run ;;again. ;; (vlr-remove Re_Obj) ;; ;;Loop through each object in list (foreach VObj ObjList ;; ;;Convert object reference to AutoLISP style (setq EN (vlax-vla-object->ename VObj) EL (entget EN)) (cond ;;what type of entity is it? ((= (cdr (assoc 0 EL)) "LINE") ;;Did the line object cause the callback? (if (eq Not_Obj VObj) ;;if so, skip it. (setq SkipIt 'T) ) (setq ENL EL) ;;save entity list of line ) ('T ;;Otherwise it is one of the circles. ;;get the center point and radius (set (if (boundp 'P1) 'P2 'P1) (cdr (assoc 10 EL))) (set (if (boundp 'R1) 'R2 'R1) (cdr (assoc 40 EL))))) ) (setq AA (angle P1 P2) ;;angle between circles ;;adjust points P1 and P2 P1 (polar P1 AA R1) P2 (polar P2 (+ AA PI) R2) ;;replace values in entity list ENL (subst (cons 10 P1) (assoc 10 ENL) ENL) ENL (subst (cons 11 P2) (assoc 11 ENL) ENL) ) (if (null SkipIt) (progn (entmod ENL) ;;update the line (vlr-add Re_Obj) ;;restart reactor ) (prompt "\nConnection broken.") ) ) ;;============================================= (prompt "\nREACTOR Example set. CADENCE 1999") (prompt "\n\tEditor reactor - COUNTER, COUNTS") (prompt "\n\tObject reactor - CONNECT") (princ)