Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Parallel

An Unlimited Undo/Redo Stack Pattern For PowerBuilder


Dr. Dobb's Journal June 1997: An Unlimited Undo/Redo Stack Pattern For PowerBuilder

David is a principle consultant with Keane Inc., a national computer services firm. You can contact him at [email protected].


Users have become accustomed to applications that provide multilevel undo and redo capabilities. Undo/redo lets users quickly recover from simple mistakes and can save time and frustration. However, undo/redo is typically only available in mass-marketed applications, such as Microsoft Word.

Still, users of enterprise database applications can also benefit from an effective undo/redo facility. The main reason undo/redo is not usually provided in these applications is the perceived cost of implementation. If you can provide undo/redo capabilities for these applications in an easily reused component, the costs for providing this feature are drastically reduced.

In this article, I present a solution for providing unlimited undo and redo capabilities to windows developed using PowerBuilder. I used design patterns in the discovery of this solution to provide a simple, standardized approach that is quickly understood and highly adaptable to a variety of specific problems. Figure 1 shows an application that demonstrates this design and provides a collection of reusable and extendable components that you can readily incorporate into any PowerBuilder application or component library. The .PBL and related files that implement this capability are available electronically; see "Availability," page 3.

Why Use Design Patterns?

Design patterns provide good solutions to the problems that arise in the development of component features. These patterns explain the benefits and drawbacks of specific approaches and allow you to find the ideal solution quickly. Additionally, patterns provide an excellent description of how the resulting design works, thus reducing the time and effort required to explain each specific design. And, of course, reducing the documentation effort reduces development costs. Design patterns can also minimize training requirements because they provide clear, well-documented examples of good design.

But design patterns have even more advantages. Because each one discusses how it can work with other patterns, you can easily find ways to extend your existing designs when new or more flexible features must be incorporated into an evolving system. As your library of component features expands, existing applications can automatically receive feature enhancements if you are careful to encapsulate the features in a standardized manner. By consistently and continually applying design patterns, you can significantly reduce costs while increasing quality, maintainability, and enhanceability over long periods of time.

The Basic Approach

An effective approach to creating an undo/redo facility is to combine and enhance two of the design patterns described in Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley, 1995). I'll start with the Command pattern to encapsulate the basic undoable operations. Next, I incorporate elements of the Memento pattern to allow the commands to store the state information needed to actually perform undo and redo. Finally, I add an extended last-in-first-out (LIFO) stack to maintain a command history.

Figure 2 shows the basic structure of the Command pattern. This pattern describes a simple solution that encapsulates an operation as a parameterized object. A SpecificCommand is created by a Client and works in concert with a target object, called the Receiver, upon which the command is performed. The command is executed at the request of the Invoker. Invoker calls the Execute() method in a Command, which then performs its encapsulated operation by calling the Action() method in its associated Receiver. I will use the Command pattern to form the fundamental structure of the undo/redo facility.

The Memento pattern (see Figure 3) describes a way to capture the state of an object so that you can return the object to that state at a later time without violating encapsulation. In this pattern, the Memento object captures the state of the Originator. The Originator creates and initializes a Memento when its CreateMemento() method is invoked. A Caretaker is needed to maintain the Mementos in the system. Finally, whenever the Originator needs to be restored to a previous state, the SetMemento() method calls the GetState() method in Memento to retrieve and restore Originator to its previously saved state.

I incorporated the Memento pattern in the undo/redo design to hold the information a command needs by merging the state-holding capabilities of the Memento object directly into the Command objects. Also, since Commands must act on the same target object, both initially to perform an operation and to undo that operation by restoring the target's previous state, I merge the roles of Memento's Originator and Command's Receiver.

In addition to parameterizing commands and maintaining undo states, you also need a history of executed commands. This is achieved by combining the responsibilities of the Command pattern's Invoker and Memento's Caretaker. The resulting object is responsible both for maintaining this history and for invoking the commands. This history object places commands on a stack, initially executing a command as it is stacked. Later, it tells the top-most stacked command to reverse its effects when undo is requested and pop the command off the stack.

Figure 4 illustrates how this history operates. The effect on the history as commands are first pushed onto the stack are shown in step 1. When users decide to undo an action, the command at the top is asked to undo its operation, then it's popped off the top of the stack, as step 2 illustrates.

Since you want to be able to redo items that have been previously undone, you need to enhance this operation. As you can see in step 2 of Figure 4, whenever a command is undone, a second pointer is set to point to that command. As more commands are undone, those commands remain above the top of the undo stack, thus forming a second stack in the opposite direction. Consequently, the undo/redo history actually consists of two opposing stacks.

As soon as a new command is placed on the top of the undo stack, however, all commands are removed from the redo portion of the history. This is essentially how Microsoft Word handles redoing previously undone commands. If you have a copy of Word or another application that supports undo and redo, you may wish to experiment with it to get a feel for how this process is handled.

A New Design Pattern

The result of this merging of the Command and Memento patterns is a new Undo/Redo pattern; see Figure 5. Like all good patterns, Undo/Redo provides a highly generalized solution that is widely applicable to many kinds of applications and programming languages.

The Target object creates a SpecificCommand that will act upon the Target. Alternately, the Client may create the command and pass it to the Target. Either way, the Target is then responsible for setting the command's internal state by calling the SetState() method in the command. The command then internalizes the information that will be needed both to perform the command initially and to completely reverse the effects of that command if an undo action is requested. The Target then passes the command to the Client, which passes it to the UndoStack by calling its Do() method.

UndoStack maintains the history of SpecificCommands and calls the Do(), Undo(), and Redo() methods in each command at the appropriate times. When the UndoStack.Do() method is invoked, the Do() method in the command is executed and the command is pushed onto the history stack. UndoStack also provides the CanUndo() and CanRedo() methods so that the application can determine what actions are currently possible. These methods are typically called by Client at strategic times to enable or disable command buttons in the user interface as appropriate.

The Do() method in Command is analogous to the Execute() method in the Command pattern. Undo() and Redo() are new. Undo() is responsible for restoring the Target to the state it was in prior to the command's initial execution. Redo() usually performs the same operation as Do(); however, it is provided for special cases where re-execution of a command must somehow differ from the initial execution. By default, Redo() simply calls Do(). Minimally, each SpecificCommand must override the SetState(), Do(), and Undo() methods.

Dealing with Datawindows

Figure 6 shows the solution I implemented in PowerBuilder. It has a generalized window class containing a datawindow and a number of command buttons to support various undo/redo operations on the datawindow as well as update the database and close the window (see Figure 1). In this design, wa_sSpreadsheet and waBase together are the generalized window Client, udwa is the datawindow Target, nvUndoRedo is the UndoStack, nvaCmd is the abstract Command, and each of nvaCmd's descendants are the SpecificCommands. The stack and each of the command classes are defined in PowerBuilder as nonvisual user objects.

This design required some minor deviations from the idealized Undo/Redo pattern. Modifications were needed because PowerBuilder datawindows are not fully object oriented. Datawindows are composed of two tightly bound components: a datawindow object and a datawindow control.

New users of PowerBuilder often confuse these components. A datawindow control is a visual element that we place in a window. It fully supports methods, attributes, inheritance, and encapsulation and thus can be considered a true object-oriented component.

A datawindow object, on the other hand, is fundamentally a data structure -- it does not support methods or inheritance. A datawindow object encapsulates a SQL Select statement, defines the visual presentation used to display and edit a result set, and provides some predefined attributes to specify behavior. Together the object and control provide a view into a particular set of tables and columns from a database and determine the ways users can manipulate and update that data.

When you design a window in PowerBuilder, you drop a datawindow control class onto the window, then specify the associated datawindow object. Since the datawindow objects cannot be extended, you are forced to implement the Undo/ Redo pattern's Target as a datawindow control. This imposes a few restrictions on the design. First, since the visual elements are defined in the datawindow object, you cannot directly control or extend them. Second, datawindows are specifically designed to handle and process many actions internally. Consequently, you must take care when overriding the normal behavior to ensure that you do not cause unexpected results or serious errors.

Fortunately, finding solutions to these problems was not too difficult. Most of the specific commands in Figure 6 can be implemented in a straightforward manner by providing alternate methods in the datawindow control ancestor class, udwa. These methods use the appropriate command to later do the required action by calling the original datawindow method provided for that purpose. For example, the InsertRow() function is provided for inserting a new row into a datawindow. To encapsulate this operation, udwa provides an alternate function, ufInsertRow, which passes an nvCmdInsertRow command object to nvUndoRedo. Now nvCmdInertRow is responsible for calling the original InsertRow() method in the datawindow. No other object should ever call InsertRow() directly to perform this command.

Implementation of the other commands is similar. Each has a replacement in udwa for the associated PowerBuilder method -- except nvCmdEdit. Editing a column or field in a datawindow is normally handled directly by the datawindow itself. Encapsulating an edit operation could be done by capturing the editchanged and itemchanged events, creating a command object, forcing the datawindow to reject the user's actions, then having the command object apply the edit, but this would be difficult and inefficient. Instead, udwa simply creates an nvCmdEdit instance and passes it to the undo stack nvUndoRedo whenever the user makes the first change to any particular field in the associated datawindow object. Unlike the other commands, however, nvCmdEdit simply records user edits -- it does not attempt to control user actions. As further changes are made to that same field, udwa will notify the edit command by calling the ufUpdate() function so that it can update the value it stores. This continues until the user begins working in a new field, then the process repeats with the new edit command.

Since the ufDo() function in nvCmdEdit acts only as a passive observer, however, the ufRedo() function has to be overridden to properly support redo. This is the only kind of command that requires an alternate implementation for redo -- all others simply use the default callback to ufDo() as defined in the command ancestor.

Creating Specialized Windows and Commands

You can easily incorporate undo/redo in your own applications. The waSpreadsheet ancestor window provides a fully working default implementation that has been rigorously tested. The design of waSpreadsheet is documented in Figure 7. To use waSpreadsheet, follow these steps:

1. Place UNDOREDO.PBL in your library path.

2. Change the declaration of the default global-error variable type to nv_error using the Default Global Variables option of the Edit menu in the application painter.

3. Create each new window by inheriting from wa_spreadsheet.

4. Create a datawindow object and place it in the provided dw_spreadsheet datawindow control for each window.

5. Write the code needed to set the transaction object and retrieve the data for the datawindow in the open event of each window. (You must use SetTransObject(); do not use SetTrans(), as it is not supported by udwa.)

Your new window will automatically support full undo and redo capabilities for edit, insert, copy, delete, and column sorting (the user may sort on any column by clicking the right mouse button over a column header label). Additionally, waSpreadsheet provides automatic support for database updates and window closing. If the user has made any unsaved changes, waSpreadsheet will prompt the user with a standard "Save changes? Yes, No, Cancel" message.

Most real-world applications will need some customized processing. You may override or extend any of the methods that waSpreadsheet provides. Typically, you might want to change the way database updates are handled by overriding ufUpdate(), or you might provide customized command handling.

Figure 8 shows how to create a customized command and associate it with your window. First, you must create a new nonvisual user-object class using the User Object painter and inheriting from the appropriate command. In the sample application shown in Figure 1, I extended the ncCmdDeleteRow command to pop up an "Are you sure?" message before deleting a row. To do this, I created a new command, called nvDeletePart, which extends the ufDo() function and pops up the message. If the user confirms the deletion, then ufDo() calls back to its superclass to allow the default action to proceed using super::ufDo(). If users decide not to delete the row, however, the function simply returns 0 to cancel the command.

Returning zero from the do, redo, or undo functions of any command tells the stack that the command was not performed. Returning a negative value indicates that the command failed and the stack will respond by generating an error. If the command completes successfully, it must return a positive value that is expected to be the current row in the datawindow.

Next, you must associate the command with the new window. You tell w_pwParts to use the extended nvCmdDeletePart command by overriding the wfDeleteRow() function; see Figure 8. This function must be defined with the same parameter and return type declarations used in waSpreadsheet -- failure to properly define the function will result in incorrect operation. The simplest way to override one of the command functions is to copy the code from waSpreadsheet, paste it into your new window, and make the required changes.

In a real application, you may need to extend the commands in more complex ways. For example, in an application I recently developed using this architecture, I extended the insert command nvCmdInsert to prefill some hidden fields after inserting a new row. More interesting, however, was a window that required the user to select an item from a list in a pop-up window, and prefilled the foreign key columns with the selected information.

Both of these cases required that I override the ufSetup() and ufDo() functions in the extended commands. The ufSetup() functions were extended to add the additional state information needed to perform the commands, and the ufDo() functions added the necessary functionality in much the same way that this was done for nvCmdDeletePart.

Conclusion

Design patterns simplify the development and documentation of reusable components. By combining design patterns in innovative ways, you can quickly create new patterns that are robust and easily understood.

The example application I have provided demonstrates PowerBuilder's ability to support these designs. This example implements a highly generic and widely applicable component that you can incorporate directly into your applications with little effort. Windows you develop using the simple steps I've outlined will automatically support the unlimited undo and redo features this component provides. By incorporating undo/redo in your applications, you can bring a new level of protection and enjoyment to your users. And your users will thank you for this.

Comments

The source code identifiers used in this article differ slightly from the actual identifiers in the sample code. I have removed the underscores normally used by PowerBuilder naming conventions to enhance readability. In all cases, the actual identifier name includes an underscore between each name part. For example, the actual source code name of nvCmdEdit is nv_cmd_edit.

References

Beck, K. and R. Johnson. "Patterns Generate Architectures." ECOOP '94 Proceedings. Bologna, Italy: Springer-Verlag Lecture Notes in Computer Science #821. July 1994.

Coplien, J. and D. Schmidt, eds. Pattern Languages of Program Design. Reading, MA: Addison-Wesley, 1995.

Gamma, E., R. Helm, R. Johnson, and J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley, 1995.

Nielsen, M. and N. Abdo. "Applying Design Patterns to PowerBuilder." San Mateo, CA: Dr. Dobb's Journal, June 1996.

DDJ


Copyright © 1997, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.