Event Binding and Loose Coupling
This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
Event Binding and Loose Coupling
This month Nancy Folsom takes a look at a feature that's new to VFP 8.0 and relates it to the object-oriented programming concept of loose coupling.
Sorry, but I'm afraid this is G-rated, so this article won't be like that. In this context, coupling refers to the degree of dependency between objects. If there's little dependency, then a system is "loosely coupled." If objects refer directly to other objects, then they're "tightly coupled." There has always been at least some dependency between containers and their contained objects, although it's been possible to eliminate most if not all dependency between the objects within a container, be it a control class, container, form, and so on. Why? One of the first questions most people run into when starting to use Visual FoxPro is the problem of what happens when one renames—or even deletes—an object in a container that other objects refer to. When objects are tightly coupled, it's hard to change their behavior, swap them out for other objects, or trace bugs, since it can be a struggle to find where in the object hierarchy the problem is hiding.
Where does coupling arise?
Most of us create detailed data entry forms that do a number of common tasks. Users can input data, change data, save or undo edits, and they might even be able to lock a record against editing. Figure 1 shows a simple example of a data entry form.
In order to help users make sensible choices, it's helpful to disable the buttons that don't make sense in a given context. In the example, once the data is changed, the Save and Undo buttons are enabled, signaling to the user that there are pending changes. Figure 2 shows this state. If I click on the Edit button to lock the data from accidental editing, the data fields are disabled, as Figure 3 shows.
Coordinating various elements on the form that have to interact, and yet are independent components, can be tricky to implement so that the dependencies are minimal. In the example, the container that has the data is one logical unit, and the container of buttons is another. The buttons should be reusable on any data entry form, and the data will exist independently of what actions might be available. There's a third unit formed by the relationship between the button container and the data container.
In this example, the relationship between the data and the available functions (save, undo, edit) is represented by the form. The form will coordinate the two containers. The less the two containers know about each other, the better for reusability. One might want to save (or undo) changes to not just person-related data, but also to car models, inventory items, and so on. The container of person-related data might be reused on a page in a page frame, displayed as read-only for reporting, and so forth. Each class has some fundamental responsibilities. The button container has to be able to enable and disable buttons, and it has to have some method that parallels the functionality represented by the buttons. In other words, the button container will have a method for each of the button's functions that the buttons can call when they're clicked.
Why not put the functionality in the button? Arguably, the first step to achieving the enlightened state of loose coupling is to eliminate references that objects make to the objects contained within another container. So, for example, eliminate references like the following:
*!* SomeForm.SomePageFrame.SomePage.SomeButton.Click() THIS.PARENT.Page2.Text1.VALUE = "I've changed!"
Instead, let the containers simply notify the mediator (the form) that something has occurred, but then leave it up to the form to do something with the information. NoBindEventsEx.PRG, included in the Download file, shows a simplistic way of decoupling the button and data containers on a data entry form. In this scenario, a form has two containers: One displays data from a record, and the other container has three buttons for Save, Undo, and Edit Mode.
When data is changed, the Save and Undo buttons are enabled. When the Edit Mode is False, the data objects are disabled. When changes are saved or reversed, the Save and Undo buttons need to be disabled and the data needs to be saved, or reverted. When the Edit Mode is turned off, any pending changes are saved. So, in short, the buttons and the data objects have effects beyond their immediate responsibilities.
In order to accomplish this, the buttons pass their messages up to their owner container, which passes the messages up to the form, which then passes the messages down to the data container, which, finally, does something (or not) with the data objects. Somewhat ironically, in order to decouple the logic in a Lastname text box from the logic in a Save button, many objects need to be involved. It's only an apparent irony, though. Any tight coupling occurs between an object and its parent, which is acceptable so long as the container is solely responsible for communicating with the world. However, there's plenty of "snug-coupling."
The code shows that containers have methods that parallel the events they'll mediate. When the Save button is clicked, it calls the button container's Save method, which calls the mediator's Save(). The mediator (the form) calls the data container's Save(). This what I mean when I say snug-coupling.
Although the button container and the data container in this example are decoupled, there's still more dependency between the form and the containers than is comfortable. First, it's difficult to get the synchronization correct (it's best to implement one bit of functionality at a time and test it); second, it's difficult to remember where in the hierarchy one put the critical code; and third, if you want to drop the button container on a different form, for example, you have to make sure that the mediator methods (such as Save) are present.
Happily, Visual FoxPro 8.0 allows us to decouple complex messaging logic like I demonstrated in my previous example by allowing us to raise, bind to, and chain events together. We can even treat events like objects. First, let me back up a moment. Events are different from methods—events occur automatically, under certain circumstances, while methods run only when we invoke them programmatically. One can still call events as one would invoke methods, but it's not recommended.
Events should only run when VFP thinks they should run. So, instead of calling a button's click, for example, it's better to have a form-level method called, say, OnClick(), that both your code elsewhere and the button click() event can call. In the previous example, the Save and Undo buttons call method code in the container that saves, or reverses, changes, respectively.
Event "binding" means that we can associate the events that occur in one object with events in another object. Events don't need to be named the same in both objects. There are some complexities to the syntax and use, but that's beyond the scope of this particular article. Please see Mike Helland's introduction to event binding in last month's issue. It's important to consider, with respect to coupling, that my mediator can now replace all its custom methods for Save, Undo, and so on, and the containers can ignore notifying a parent about events. It's up to the mediator to set up the event binding between objects. Let's get to an example. BindEventsEx.PRG, also included in the Download file, rewrites the first example to make use of the BindEvent() function (which is new to VFP 8.0). The critical differences between the two methodologies are that containers don't have parallel methods that contained objects call when their state changes, and there's less need for Access and Assign methods. Here are some of the relevant code snippets from the program:
LOCAL loForm AS FORM loForm = NEWOBJECT("BindEvents") loForm.SHOW(1) DEFINE CLASS BindEvents AS FORM … PROCEDURE INIT *!* Set up event bindings. *!* When the edit button is clicked, trigger the *!* data container's enable method. BINDEVENT(THIS.objDataCmd,"SetEdit", ; THIS.objPerson,"EnableControls",2) *!* If we're switching out of edit mode, save *!* any changes. BINDEVENT(THIS.objDataCmd,"SetEdit", ; THIS.objDataCmd,"Save",2) *!* When data is being changed, trigger the *!* button container refresh. BINDEVENT(THIS.objPerson,"OnChange",THIS.objDataCmd, ; "OnChange",2) *!* When Save is clicked, trigger the data *!* container to save. BINDEVENT(THIS.objDataCmd,"Save",THIS.objPerson, ; "Save",2) *!* When Undo is clicked, trigger the data *!* container to revert changes. BINDEVENT(THIS.objDataCmd,"Undo",; THIS.objPerson,"Undo",2) ENDPROC ENDDEFINE
While the form still has two containers—one with buttons and one with data objects—the form itself no longer needs to have matching, parallel methods (like Save). Instead, the form sets up the relationship between the data and buttons by relating the two relevant methods in the containers. Notice, too, that events can be bound to more than one event. In the preceding example, the SetEdit will both Save and EnableControls. This is at the highest level. Even within the containers, event binding simplifies the task of objects communicating with their parent.
I added a custom method to the container of buttons for Save, Undo, and Edit Mode called BindEvents(), which I call from the Init(). This method lets the container hook into the interesting events of the buttons. In this case the container is notified when the Edit Mode changes, and when either Save or Undo is clicked. The Save and Undo click events don't even have any code in them.
DEFINE CLASS DataCommands AS CONTAINER … PROCEDURE BindEvents *!* Instead of button's click() calling a *!* method in the parent container. Simply *!* use the edit button's own event to trigger *!* the parent to some action. BINDEVENT(THIS.btnEdit,"InteractiveChange",; THIS,"SetEdit",2) BINDEVENT(THIS.btnEdit,"ProgrammaticChange",; THIS,"SetEdit",2) BINDEVENT(THIS.btnUndo,"Click",THIS,"Undo",2) BINDEVENT(THIS.btnSave,"Click",THIS,"Save",2) ENDPROC
I do something similar with the data container. I bind a customer container method (OnChange) to the InteractiveChange() events of each text box. So, instead of calling the EntityContainer.OnChange custom method from the text box InteractiveChange(), the EntityContainer defines, once, that InteractiveChange() events will run not only any code that might be in them, but also the OnChange method. In this case, the OnChange method doesn't do anything. However, the form can bind this method to the buttons' container method (called OnChange, coincidentally), which enables the Save and Undo buttons when there are changes pending.
Why is this a good thing?
BindEvent() takes us one step closer to loosely coupled implementations. Classes can be written to manage their own unit of work, without having to be designed to pass actions and messages to a parent, which then passes on the action or message, to other objects. The same functionality is achieved by simply binding the events together within the context they cooperate, such as in a form. In addition, this means that even our runtime code can, on-the-fly, create event bindings for a dynamic system. And event handlers can be objectified and thus attached to objects just as we're getting used to doing with business rules. Visual FoxPro 7.0 and now 8.0 offer ever more ways to implement object orientation in our applications. **
[Note: The idea for this article grew out of a discussion Lew Schwartz and I had on the Microsoft public newsgroup. I'd like to thank Lew for helping to solidify the connection between loose coupling and event handling.]
To find out more about FoxTalk and Pinnacle Publishing, visit their website at http://www.pinpub.com/html/main.isx?sub=57
Note: This is not a Microsoft Corporation website. Microsoft is not responsible for its content.
This article is reproduced from the February 2003 issue of FoxTalk. Copyright 2003, by Pinnacle Publishing, Inc., unless otherwise noted. All rights are reserved. FoxTalk is an independently produced publication of Pinnacle Publishing, Inc. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of Pinnacle Publishing, Inc. To contact Pinnacle Publishing, Inc., please call 1-800-493-4867 x4209.