Implementing the Command pattern in ECO applications

By: Oleg Zhukov

Abstract: The article explains how to implement the Command pattern in CodeGear ECO applications, making use of ECO Undo/Redo facilities

Implementing the Command pattern in ECO applications

    Introduction

The idea of turning operations into objects had great influence upon the software development methods. Expressed in the Command pattern this idea finds its application in many modern object-oriented systems. In this article we are going to demonstrate the Command pattern in action empowered by CodeGear ECO undo/redo facilities. Before we proceed to the implementation aspects of the Command pattern let us start by discussing the motivation for its usage.

    Command pattern usage motivation

Imagine an application that has items in the main menu to add/remove objects (actually there are many such applications). However the response to a menu item click cannot be hard-coded, since the reaction should depend on the current context. For example “Add” item may create either a new company object or a new department depending on what list is selected in the user interface. A good solution is to have an abstract Command class with AddCompanyCommand and AddDepartmentCommand subclasses, both overriding the Do() operation. “Add” menu item then just invokes addCommand.do() without any knowledge about the concrete command class.

Hide image
0

This shows how representing an operation with an object detaches the operation client (menu item) from the actual operation implementation, adding flexibility and allowing us to easily extend the system with new operations (command classes).

Another advantage of command objects over simple methods is that a method can only be executed, whereas a command may contain other than Do() methods for example Undo()/Redo() or Validate() methods. More generally the Command pattern makes operations live, each with its own lifecycle.

Hide image
9

And although implementing the Command pattern with the undo/redo functionality always seemed to be a complicated task we will show how easy it can be with ECO framework. So let us start with creating basic command functionality and then we will extend it with undo/redo capabilities.

    Building simple commands

Create a new ECO WinForms application with the domain ECO model as follows:

Hide image
1

Then create a simple GUI with a menu bar and two grids displaying companies and corresponding departments.

Hide image
2

Now we are ready to introduce command classes. First create a Command abstract base class. It should hold a protected reference to the used ECO space instance, passed as a constructor parameter. It should also declare an abstract Do() operation. It is handy to design classes (not ECO, just plain classes) via the Together diagram editor. This is how the command class looks in the Together designer:

Hide image
3

            public Command( MyEcoSpace es ) {
                           this.es = es;
            }

Commands for adding companies and departments will be concrete subclasses of the Command class. Each one will have its own constructor and Do() operation implementation.

Hide image
4

            public AddCompanyCommand( MyEcoSpace es ) : base(es) {}

            public override void Do( ) {
                           new Company(es);
            }
            public AddDepartmentCommand( MyEcoSpace es, Company c ) : base(es) {
                           this.company = c;
            }

            public override void Do( ) {
                           Department d = new Department(es);
                           d.Company = company;
            }

Next step is to link commands to the user interface. For this we define an addCommand property in the main form. This property should return an appropriate command object depending on which grid is focused: AddCompanyCommand if companies grid is selected, AddDepartmentCommand if departments grid is selected:

            private Command addCommand {
                           get {
                                          if (dgCompanies.ContainsFocus)
                                                         return new AddCompanyCommand(EcoSpace);
                                          else if (dgDepartments.ContainsFocus)
                                                         return new AddDepartmentCommand(EcoSpace,
                                                         (Company)CurrencyManagerHandle.CurrentElement(dgCompanies).AsObject);
                                          else return null;
                           }
            }

The last thing here is to invoke the current add command. Just add the following handler for the “Add” menu item click:

            private void addItem_Click(object sender, System.EventArgs e)
            {
                           addCommand.Do();
            }

Hide image
5

    Adding notifications support

What our implementation lacks at the moment is the feedback from commands to the user interface. For example the UI might set focus to added objects in a grid whenever it receives notifications from command objects. In order to add notifications support let us associate the Command class with ICommandListener interface. A listener instance should be passed to a command through the constructor. Another two public fields will hold the information about the command execution context: processedIObject:IObject will reference the object affected by the command, context:object will describe the context UI control, grid for example. The UI will use this context information to set cursor to the proper row in the proper gird.

Hide image
6

            public interface ICommandListener {
                           void update( Command c );
            }
            public Command( MyEcoSpace es, ICommandListener listener, object context) {
                           this.es = es;
                           this.listener = listener;
                           this.context = context;
            }

Then make changes to both Command subclasses. Modify their constructors and Do() methods to make them assign processedIObject and send notifications to a listener. The code below illustrates the changes made to AddCompanyCommand class (for AddDepartmentCommand they are similar).

            public AddCompanyCommand(MyEcoSpace es, ICommandListener listener, object context)
                                          : base(es, listener, context) {}

            public override void Do( ) {
                           Company c = new Company(es);
                           processedIObject = c.AsIObject();
                           listener.update(this);
            }

The main form then should implement the ICommandListener interface and pass itself as a listener to the created commands. Data grids will serve as context for command objects.

// public class WinForm
            private Command addCommand {
                           get {
                                          if (dgCompanies.ContainsFocus)
                                                         return new AddCompanyCommand(EcoSpace, this, dgCompanies);
                                          …
                           }
            }
// public class WinForm
            public void update(Command c) {
                           DataGrid contextGrid = (DataGrid)c.context;
                           ElementHandle contextHandle = contextGrid.DataSource as ElementHandle;
                           IObjectList contextList = (IObjectList)contextHandle.Element;

                           contextHandle.EnsureBindingList();
                           int newRowIndex = contextList.IndexOf(c.processedIObject);
                           contextGrid.CurrentCell = new DataGridCell(newRowIndex, 0);
            }

Now the interface reflects changes done by commands setting cursor to added objects. The last thing remaining is to add Undo/Redo support.

Hide image
7

    Adding Undo/Redo support

Fortunately ECO allows caching all changes done to ECO objects in so called undo blocks. You may start such undo block then make some modifications to objects and then undo all these changes. This is possible because ECO keeps track of all modifications in the currently started undo block. Altogether undo blocks form two lists: one for done blocks (undo list), the other for undone ones (redo list). As soon as a block gets undone it is moved to the redo list and hence can be redone. All actions upon undo blocks in ECO are performed through the ECO Undo service. For example this shows how to start an undo block and get access to it:

EcoSpace.UndoService.StartUndoBlock();
IUndoBlock block = EcoSpace.UndoService.UndoList.TopBlock;

So how is it possible to undo/redo changes done by command objects? According to said above we may associate each command instance with its own undo block. Then the changes done by a command will be cached inside the associated undo block.

Hide image
7_1

Now it is possible to undo/redo a command, but how do we know which command is the latest, which one goes before it and etc.? In order to maintain done (and undone) commands sequence there should be a class which will hold all previously done commands. Let us call it CommandsManager. It should reference two lists: one for done commands (undo list), the other for undone ones (redo list). Let us also make the CommandsManager responsible for dealing with the ECO undo service. Thus command instances will ask the CommandsManager to start undo block and to undo/redo them. To make the CommandsManager object unique and accessible from any point we will implement it as a Singleton.

Hide image
8

public class CommandsManager {

            public static readonly CommandsManager instance = new CommandsManager();
            public MyEcoSpace es;
            public  ArrayList doneCommands = new ArrayList();
            public  ArrayList undoneCommands = new ArrayList();

            public void start( Command c ) {
                           es.UndoService.StartUndoBlock();
                           c.uBlock = es.UndoService.UndoList.TopBlock;
                           undoneCommands.Clear();
                           doneCommands.Add(c);
            }

            public void undo( Command c ) {
                           es.UndoService.UndoBlock(c.uBlock.Name);
                           undoneCommands.Add(c);
                           doneCommands.Remove(c);
            }

            public void redo( Command c ) {
                           es.UndoService.RedoBlock(c.uBlock.Name);
                           doneCommands.Add(c);
                           undoneCommands.Remove(c);
            }

}

Do not forget to link the CommandsManager to the used ECO space. This can be done in the main form constructor:

            public WinForm() {
                           …
                           CommandsManager.instance.es = EcoSpace;
            }

As we said above each command instance should hold the corresponding undo block. It should also make requests to the CommandsManager where necessary.

public abstract class Command {
            …
            public IUndoBlock uBlock;
            …

            public void Do() {
                           CommandsManager.instance.start(this);
                           doActions();
                           listener.update(this);
            }

            public void Undo() {
                           CommandsManager.instance.undo(this);
                           listener.update(this);
            }

            public void Redo() {
                           CommandsManager.instance.redo(this);
                           listener.update(this);
            }

            protected abstract void doActions();
}

Here we made use of the Template method pattern by defining Do() as a template with an abstract doActions() operation inside. Thus Command subclasses should only override the doActions() method. This is how it looks for the AddCompanyClass:

// public class AddCompanyClass
            protected override void doActions( ) {
                           Company c = new Company(es);
                           processedIObject = c.AsIObject();
            }

Now it is time to make changes to the user interface. Add “Undo” and “Redo” items to the main menu. Then create handlers for the corresponding item click events:

// public class WinForm
            private void undoItem_Click(object sender, System.EventArgs e)
            {
                           ArrayList doneCommands = CommandsManager.instance.doneCommands;
                           (doneCommands[doneCommands.Count - 1] as Command).Undo();
            }

            private void redoItem_Click(object sender, System.EventArgs e)
            {
                           ArrayList undoneCommands = CommandsManager.instance.undoneCommands;
                           (undoneCommands[undoneCommands.Count - 1] as Command).Redo();
            }

Another useful modification is to enable/disable “Undo” and “Redo” items depending on whether undo and redo lists are empty.

// public class WinForm
            public void update(Command c) {
                           …
                           undoItem.Enabled = CommandsManager.instance.doneCommands.Count > 0;
                           redoItem.Enabled = CommandsManager.instance.undoneCommands.Count > 0;
            }

This is how our application finally looks:

Hide image
9

    Conclusion

The article concerns the usage of the Command pattern in CodeGear ECO applications. One of the Command pattern features that is typically difficult to implement is the Undo/Redo functionality. However the power of ECO makes it quite a simple task and allows developers to construct more flexible and usable object-oriented solutions.

    Download

The full source code of the example can be downloaded from CodeGear CodeCentral portal. Here is link to it: http://cc.codegear.com/item/24635. Also visit the author’s website.

Server Response from: ETNASC02