Chapter 23. The Command Pattern

The Chain of Responsibility forwards requests along a chain of classes, but the Command pattern forwards a request only to a specific object. It encloses a request for a specific action inside an object and gives it a known public interface. It lets you give the client the ability to make requests without knowing anything about the actual action that will be performed and allows you to change that action without affecting the client program in any way.

Motivation

When you build a VB user interface, you provide menu items, buttons, check boxes, and so forth to allow the user to tell the program what to do. When a user selects one of these controls, the program receives a clicked event, which it receives into a special routine in the user interface. Let's suppose we build a very simple program that allows you to select the menu items File|Open, File|Red, and File|Exit, and click on a button marked Red that turns the background of the window red. The File|Red menu item also turns the background red. This program is shown in Figure 23-1.

A simple program that receives events from the button and menu items

Figure 23-1. A simple program that receives events from the button and menu items

The program consists of the File Menu object with the mnuOpen, mnuRed, and mnuExit MenuItems added to it. It also contains one button called btnRed. During the design phase, clicking on any of these items creates a little method in the Form class that gets called when the control is clicked.

As long as there are only a few menu items and buttons, this approach works fine, but when you have dozens of menu items and several buttons, the Form module code can get pretty unwieldy. In addition, the red command is carried out both from the button and the menu.

Command Objects

One way to ensure that every object receives its own commands directly is to use the Command pattern and create individual Command objects. A Command object always has an Execute() method that is called when an action occurs on that object. Most simply, a Command object implements at least the following interface.

'Class Command
Public Sub Execute()
End Sub

One objective of using this interface is to separate the user interface code from the actions the program must carry out, as shown here.

Private Sub mnuexit_Click()
  exitCmd.Execute
End Sub
'-------
Private Sub mnuOpen_Click()
  flCmd.Execute
End Sub

Then we can provide an Execute method for each object that carries out the desired action, thus keeping the knowledge of what to do inside the object where it belongs, instead of having another part of the program make these decisions.

One important purpose of the Command pattern is to keep the program and user interface objects completely separate from the actions that they initiate. In other words, these program objects should be completely separate from each other and should not have to know how other objects work. The user interface receives a command and tells a Command object to carry out whatever duties it has been instructed to do. The UI does not and should not need to know what tasks will be executed. This decouples the UI class from the execution of specific commands, making it possible to modify or completely change the action code without changing the classes containing the user interface.

The Command object can also be used when you need to tell the program to execute the command when the resources are available rather than immediately. In such cases, you are queuing commands to be executed later. Finally, you can use Command objects to remember operations so you can support Undo requests.

Building Command Objects

There are several ways to go about building Command objects for a program like this, and each has some advantages. We'll start with the simplest one: creating new classes and implementing the Command interface in each. Here is an example of the exit class.

'Class Exit command
Implements Command
Private Sub Command_Execute()
  End
End Sub

Then both the File|Exit command and the Form_Unload event can call it.

Private Sub Form_Unload(Cancel As Integer)
 exitCmd.Execute
End Sub
'-----
Private Sub mnuexit_Click()
  exitCmd.Execute
End Sub

You also have only one localized place to change what takes place if, for example, you want to add an “Are you sure?” message box.

This certainly lets us simplify the user interface code, but it does require that we create and instantiate a new class for each action we want to execute. Further, because VB has fairly stringent type checking, we need to create two references to these objects, one as a specific class and one as a Command object.

Private exitCmd As Command
Private exitCl As ExitClass
'the exit command class
Set exitCl = New ExitClass
Set exitCmd = exitCl         'as command

Classes that require specific parameters to work need to have those parameters passed in the init method or in a set method. For example, the File|Open command requires that you pass it an instance of the CommonDialog object and the label where the filename will be displayed.

'and the file|open class
Set opner = New Opener
opner.init cDlg, Label1 'send it the common dialog and label
Set flCmd = opner

Similarly, our RedCommand object needs the Form to set its background to red.

'create the red command class
Set redCl = New RedClass
redCl.setForm Me
Set redCmd = redCl 'as a command

This can then be called both from the menu and button click event methods.

Private Sub btnRed_Click()
 redCmd.Execute
End Sub
'-----
Private Sub mnuRed_Click()
 redCmd.Execute
End Sub

Arrays of Commands

When you have a program with an array of similar controls, such as a series of buttons or radio buttons, you can create a parallel array of command objects and simply execute the right one. For example, you might have a program to display either or both sexes in a list of kids.

For example, the program in Figure 23-2 allows you to select the girls, the boys, or show all the kids at once.

A program that displays kids by sex or all at once

Figure 23-2. A program that displays kids by sex or all at once

You can create the three radio buttons as a control array when you design the program. Then you can simply create three command objects and put them in a Collection. When a radio button is clicked, you just pick that command and execute it.

'all button clicks come here
Private Sub opSex_Click(Index As Integer)
Dim cmd As Command
'execute the command from the Collection
 Set cmd = buttons(Index + 1)
 cmd.Execute
End Sub

In this program we create a Kids object containing a Collection of individual swimmers.

'Class kids
Private kds As New Collection
Private Index As Integer
Private sex As String
Private sw As Swimmer
'-----
Public Sub add(sw As Swimmer)
  kds.add sw
End Sub
'-----
Public Sub setSex(sx As String)
 sex = sx
 moveFirst
End Sub
'-----
Public Sub moveFirst()
 Index = 1
 Set sw = kds(Index)
End Sub
'-----
Public Sub readKids(fname As String)
 'details on CDROM
End Sub
'-----
'returns true if there are any more kids
'of the current sex
Public Function hasMoreElements() As Boolean
 Set sw = kds(Index)
 If sw.getSex = sex Or sex = "" Then
  hasMoreElements = Index < kds.Count
 Else
  nextElement
  hasMoreElements = Index < kds.Count
 End If
End Function
'-----
'moves to the next kids of that sex
Public Function nextElement() As Swimmer
  Set sw = kds(Index)
  If sex <> "" Then
    While sw.getSex <> sex And Index <= kds.Count
      Set sw = kds(Index)
      If sw.getSex <> sex Then Index = Index + 1
    Wend
    If sw.getSex = sex Then
      Set nextElement = sw
      Index = Index + 1
    Else
      Index = Index + 1
    End If
  Else
    Set nextElement = kds(Index)
    Index = Index + 1
  End If
End Function

Then we create a PickKids class that implements the Command interface that returns a Collection of the kids who match the criterion.

'Class pickKids
Implements Command
Private kds As Kids
Private lst As ListBox
Private sex As String
'-----
Public Sub init(sx As String, kidds As Kids, list As ListBox)
 Set kds = kidds
 sex = sx
 Set lst = list
End Sub
'-----
Private Sub loadList()
'loads the list box with the selected kids
Dim sw As Swimmer
lst.Clear
kds.setSex sex
kds.moveFirst
While kds.hasMoreElements
  Set sw = kds.nextElement
  lst.AddItem sw.getName
Wend
End Sub
'-----
'the command is executed here
Private Sub Command_Execute()
  loadList
End Sub

With this simple infrastructure, we can create three instances of the PickKids class and select the right one, depending on the button that the user clicks.

Dim pk As PickKids

Set kds = New Kids
Set buttons = New Collection
kds.readKids "Swimmers.txt"

'create 3 instances of PickKids
'for each of the 3 option selections
Set pk = New PickKids
pk.init "F", kds, lsKids
buttons.add pk  'and add to the collection

Set pk = New PickKids
pk.init "M", kds, lsKids
buttons.add pk

Set pk = New PickKids
pk.init "", kds, lsKids
buttons.add pk

Again, the advantage here is that the user interface no longer plays a tangled role in providing the actual execution of commands. Instead, it simply executes the command without ever knowing what it is or whether the programmer had changed the character of that command.

Consequences of the Command Pattern

The main disadvantage of the Command pattern seems to be a proliferation of little classes that clutter up the program. However, even in the case where we have separate click events, we usually call little private methods to carry out the actual function. It turns out that these private methods are just about as long as our little classes, so there is frequently little difference in complexity between building the command classes and just writing more methods. The main difference is that the Command pattern produces little classes that are much more readable.

Providing Undo

Another of the main reasons for using Command design patterns is that they provide a convenient way to store and execute an Undo function. Each command object can remember what it just did and restore that state when requested to do so if the computational and memory requirements are not too overwhelming. At the top level, we simply redefine the Command interface to have two methods.

'Class Command
Public Sub Execute()
End Sub
'-----
Public Sub Undo()
End Sub
'-----
Public Function isUndo() As Boolean
End Function

Then we have to design each command object to keep a record of what it last did so it can undo it. This can be a little more complicated than it first appears, since having a number of interleaved Commands being executed and then undone can lead to some hysteresis. In addition, each command will need to store enough information about each execution of the command that it can know what specifically has to be undone.

The problem of undoing commands is actually a multipart problem. First, you must keep a list of the commands that have been executed, and second, each command has to keep a list of its executions. To illustrate how we use the Command pattern to carry out undo operations, let's consider the program shown in Figure 23-3 that draws successive red or blue lines on the screen, using two buttons to draw a new instance of each line. You can undo the last line you drew with the undo button.

A program that draws red and blue lines each time you click the Red and Blue buttons

Figure 23-3. A program that draws red and blue lines each time you click the Red and Blue buttons

If you click on Undo several times, you'd expect the last several lines to disappear no matter what order the buttons were clicked in, as shown in Figure 23-4.

The same program as in Figure 23-3 after the Undo button has been clicked several times

Figure 23-4. The same program as in Figure 23-3 after the Undo button has been clicked several times

Thus, any undoable program needs a single sequential list of all the commands that have been executed. Each time we click on any button, we add its corresponding command to the list.

Private Sub btDraw_Click(Index As Integer)
Dim cmd As Command
'get the command and execute it
 Set cmd = buttons(Index + 1)
 cmd.Execute
 ud.add cmd     'Add to undo collection
 Refresh        'repaint screen
End Sub

Further, the list to which we add the Command objects is maintained inside the Undo command object so it can access that list conveniently.

Option Explicit
'Class UndoCommand
Implements Command
Private undoList As Collection
'-----
Public Sub init()
Set undoList = New Collection
End Sub
'-----
Public Sub add(cmd As Command)
If Not (cmd.isUndo) Then
  undoList.add cmd
End If
End Sub
'-----
Private Sub Command_Execute()
Dim Index As Integer
Dim cmd As Command
Index = undoList.Count
If undoList.Count > 0 Then
  Set cmd = undoList(Index)
  cmd.Undo
  undoList.Remove Index
End If
End Sub
'-----
Private Function Command_isUndo() As Boolean
Command_isUndo = True
End Function
'-----
Private Sub Command_Undo()
'do nothing
End Sub

The undoCommand object keeps a list of Commands, not a list of actual data. Each command object has its unDo method called to execute the actual undo operation. Note that since the undoCommand object implements the Command interface, it, too, needs to have an unDo method. However, the idea of undoing successive unDo operations is a little complex for this simple example program. Consequently, you should note that the add method adds all Commands to the list except the undoCommand itself, since we have just defined undoing an unDo command as doing nothing. For this reason, our new Command interface includes an isUndo method that returns false for the RedCommand and BlueCommand objects and true for the UndoCommand object.

The redCommand and blueCommand classes simply use different colors and start at opposite sides of the window, although both implement the revised Command interface. Each class keeps a list of lines to be drawn in a Collection as a series of drawData objects containing the coordinates of each line. Undoing a line from either the red or the blue line list simply means removing the last drawData object from the drawList collection. Then either command forces a repaint of the screen.

Implements Command
'Class RedCommand
Private drawList As Collection
Private x As Integer, y As Integer, dx As Integer, dy As Integer
Private pic As PictureBox
'-----
Public Sub init(pict As PictureBox)
  Set pic = pict
  Set drawList = New Collection
  x = 0
  dx = 200
  y = 0
  dy = 0
End Sub
'-----
Private Sub Command_Execute()
 Dim dl As DrawData
 Set dl = New DrawData
 dl.init x, y, dx, dy   'create a new DrawData object
 drawList.add dl        'and add it to the list
 x = x + dx             'next one has these values
 y = y + dy
 pic.Refresh            'repaint screen window
End Sub
'-----
Private Function Command_isUndo() As Boolean
 Command_isUndo = False
End Function
'-----
Private Sub Command_Undo()
'undo last draw
 Dim Index As Integer
 Dim dl As DrawData
 Index = drawList.Count
 If Index > 0 Then
   Set dl = drawList(Index)
   drawList.Remove Index
   x = dl.getX
   y = dl.getY
 End If
 pic.Refresh
End Sub
'-----
Public Sub draw()
'draw entire list of lines
Dim h As Integer, w As Integer
Dim i As Integer
Dim dl As DrawData
h = pic.Height
w = pic.Width

For i = 1 To drawList.Count
 Set dl = drawList(i)
 pic.Line (dl.getX, dl.getY)-(dl.getX + dx, dl.getdY + h), vbRed
Next i
End Sub

Note that the draw method in the drawCommand class redraws the entire list of lines the command object has stored. These two draw methods are called from the paint method of the form.

Private Sub Form_Paint()
 rc.draw    'redraw red lines
 bc.draw    'redraw blue lines
End Sub

The set of classes we use in this Undo program is shown in Figure 23-5.

The classes used to implement Undo in a Command pattern implementation

Figure 23-5. The classes used to implement Undo in a Command pattern implementation

The Command Pattern in VB.NET

While you can write more or less the same code in VB7, the availability of inheritance provides some additional advantages. If you reconsider our original program with File|Open, File|Exit, and a Red button, you can create derived Menu objects that also implement the Command interface. Here, our command interface initially contains only the Execute method.

Public Interface Command
    Sub Execute()
End Interface

One difference here is that we can derive our RedButton class directly from Button and have it also implement the Command interface.

Public Class redButton
    Inherits System.Windows.Forms.Button
    Implements Command
    Private frm As Form

    Public Sub setForm(ByVal frm_ As Form)
        frm = frm_
    End Sub
    Public Sub Execute() Implements BtnMenu.Command.Execute
        Dim clr As Color
        clr = Me.BackColor
        frm.BackColor = Color.Red
        Me.BackColor = clr
    End Sub
End Class

Recall that in order to create a control that is derived from a Windows control and that will still work with the Form designer in Visual Studio, we add UserControl and then change the code so the control is derived from Button. Then, after compiling the program once, the new cmdButton control will appear on the bottom of the toolbar. You can use this to create a button on the form.

To create a MenuItem that also implements the Command interface, you can use the MainMenu control on the toolbar and name it MenuBar. The designer is shown in Figure 23-6.

The menu designer interface

Figure 23-6. The menu designer interface

We derive the OpenMenu and ExitMenu classes from the MenuItem class. However, we have to add these in the program code, since there is no way to add them in the Form Designer. Here is the ExitMenu class.

Public class ExitMenu
  inherits MenuItem
  implements Command

Private frm as Form
'-----
    Public Sub New(ByVal frm_ As Form)
        MyBase.New("Exit")
        frm = frm_
    End Sub
    '-----
    Public Sub Execute() Implements Command.Execute
        frm.close()
    End Sub
End Class

One other major difference in VB7 is that you don't have to have separate click methods for each event. Instead, you can add the same event handler to each button and menu item. This handler simply calls the commands.

Private Sub commandHandler( _
         ByVal sender As System.Object, _
         ByVal e As System.EventArgs)
        Dim cmd As Command
        cmd = CType(sender, Command)
        cmd.Execute()
    End Sub

Here is how you register this method as the event handler.

Private Sub init()
        evh = New EventHandler(AddressOf commandHandler)
        AddHandler rdbutn.Click, evh
        rdbutn.setForm(Me)
        mnMain = New MainMenu()
        mnFile = New MenuItem("File")
        mnOpen = New FileOpen()
        mnQuit = New ExitMenu(Me)
        mnMain.MenuItems.AddRange(New MenuItem() { mnFile} )
        mnFile.MenuItems.AddRange(New MenuItem() { mnOpen, mnQuit} )
        Me.Menu = mnMain
        AddHandler mnOpen.Click, evh
        AddHandler mnQuit.Click, evh
    End Sub

The CommandHolder Interface

Now, while it is advantageous to encapsulate the action in a Command object, binding that object into the element that causes the action (such as the menu item or button) is not exactly what the Command pattern is about. Instead, the Command object really ought to be separate from the invoking client so you can vary the invoking program and the details of the command action separately. Rather than having the command be part of the menu or button, we can make the menu and button classes containers for a Command object that exists separately. We thus make these UI elements implement a CommandHolder interface.

Public Interface CommandHolder
    Function getCommand() As Command
End Interface

This simple interface says that there is a way to obtain that object to call its Execute method. We put the command object into the menu object as part of the constructor. This is particularly important where we have several ways of calling the same action, such as when we have both a Red button and a Red menu item. In such a case, you would certainly not want the same code to be executed inside both the MenuItem and the Buttn classes. Instead, you should fetch references to the same command object from both classes and execute that command.

Then we create cmdMenu class, which implements this interface.

Public Class CmdMenu
    Implements CommandHolder
    Inherits MenuItem
    Protected comd As Command
    '-----
    Public Sub New(ByVal lbl As String, _
                   ByVal cmd As Command, ByVal evh As EventHandler)
        MyBase.New(lbl)
        AddHandler Click, evh
        comd = cmd
    End Sub
    '-----
    Public Function getCommand() As Command _
                    Implements CommandHolder.getCommand
        Return comd
    End Function
End Class

This actually simplifies our program. We don't have to create a separate menu class for each action we want to carry out. We just create instances of the menu and pass them different Command objects.

Private Sub init()
        'called from the New constructor
        evh = New EventHandler(AddressOf CommandHandler)
        AddHandler rdButn.Click, evh
        rdButn.setCommand(New RedCommand(Me))
        mnMain = New MainMenu()
        mnFile = New MenuItem("File")
        mnOpen = New CmdMenu("Open", New FileOpenCommand(), evh)
        mnQuit = New CmdMenu("Exit", New ExitCommand(Me), evh)
        mnMain.MenuItems.AddRange(New MenuItem() { mnFile} )
        mnFile.MenuItems.AddRange(New MenuItem() { mnOpen, mnQuit} )
        Me.Menu = mnMain
        AddHandler mnOpen.Click, evh
        AddHandler mnQuit.Click, evh
    End Sub

Creating the cmdButton class is analogous, and we can use the same RedCommand instance we just created.

redbutton.setCommand(redc)

We still have to create separate Command objects, but they are no longer part of the user interface classes. For example, the FileCommand class is just this.

Public Class FileOpenCommand
    Implements Command
    'Command object to show file-open dialog
    Public Sub New()
        MyBase.New()
    End Sub
    '-----
    Public Sub Execute() Implements Command.Execute
        Dim fd As OpenFileDialog
        fd = New OpenFileDialog()
        fd.ShowDialog()
    End Sub
End Class

Then our action method needs to obtain the actual command object from the UI object that caused the action and execute that command.

Public Sub CommandHandler(ByVal sender As Object, _
            ByVal e As EventArgs)
        Dim cmdh As CommandHolder
        Dim cmd As Command
        cmdh = CType(sender, CommandHolder)
        cmd = cmdh.getCommand
        cmd.Execute()
    End Sub

This is only slightly more complicated than our original routine and again keeps the action separate from the user interface elements. We can see the relations between theses classes and interfaces clearly in the UML diagram in Figure 23-7.

A class structure for three different objects that all implement the Command interface and two that implement the CommandHolder interface

Figure 23-7. A class structure for three different objects that all implement the Command interface and two that implement the CommandHolder interface

Here you see that redButton and cmdMenu implement the CommandHolder interface and that there are three instances of cmdMenu in the UI class ComdHolder. Figure 21-7 also shows the classes ExitCommand, RedCommand, and FileCommand, which implement the Command interface and are instantiated in the ComdHolder UI class. This is finally the complete implementation of the Command pattern that we have been inching toward.

Handling Undo Commands in VB.NET

The UndoCommand version of the Command pattern is quite analogous. The command interface now becomes the following.

Public Interface Command
    Sub Execute()
    Sub Undo()
    Function isUndo() As Boolean
End Interface

We need only create one command button by deriving it from UserControl, as we did previously.

Public Class CmdButton
    Inherits System.WinForms.Button
    Implements CommandHolder
    Private cmd As Command
    Public Sub New()
        MyBase.New
        'This call is required by the Win Form Designer.
        InitializeComponent
    End Sub

    '-----
    Public Sub setCommmand(ByVal Comd As Command)
        cmd = comd
    End Sub
    '-----
    Public Function getCommand() As Command _
             Implements CommandHolder.getCommand
        Return cmd
    End Function
End Class

Then we create three instances of it for the Red, Blue and Undo buttons. The command objects for Red, Blue, and Undo differ slightly, since we must use a graphics object for drawing. Here is the BlueCommand.

Public class BlueCommand
  Implements Command

Private drawList As ArrayList
Protected colr as Color
Protected x, y , dx, dy As Integer
Private pic As PictureBox
    '-----
    Public Sub New(ByVal pict As PictureBox)
        MyBase.New()
        pic = pict
        drawList = New ArrayList()
        x = pic.Width
        Colr = color.Blue
        dx = -20
        y = 0
        dy = 0
    End Sub
    '-----
    Public Sub Execute() Implements Command.Execute
        Dim dl As DrawData
        dl = New DrawData(x, y, dx, dy)
        drawList.add(dl)
        x = x + dx
        y = y + dy
        pic.Refresh()
    End Sub
    '-----
    Public Function isUndo() As Boolean _
             Implements Command.IsUndo
        Return False
    End Function
    '-----
    Public Sub Undo() Implements Command.Undo
        Dim Index As Integer
        Dim dl As DrawData
        Index = drawList.Count - 1
        If Index >= 0 Then
            dl = CType(drawList(index), DrawData)
            drawList.RemoveAt(Index)
            x = dl.getX
            y = dl.getY
        End If
        pic.Refresh()
    End Sub
    '-----
    Public Sub draw(ByVal g As Graphics)
        Dim h, w As Integer
        Dim i As Integer
        Dim dl As DrawData

        Dim rpen As New Pen(Color.FromARGB(255, colr), 1)
        h = pic.Height
        w = pic.Width

        For i = 0 To drawList.Count - 1
            dl = CType(drawList(i), DrawData)
            g.drawLine(rpen, dl.getX, dl.getY, _
                      dl.getX + dx, dl.getdY + h)
        Next i
    End Sub
End Class

Then the command listener in the main form class is as follows.

Public Sub CommandHandler(ByVal sender As Object, _
                          ByVal e As EventArgs)
        Dim cmdh As CommandHolder
        Dim cmd As Command
        'get the command
        cmdh = CType(sender, commandholder)
        cmd = cmdh.getCommand
        undoc.add(cmd) 'add it to the undo list
        cmd.Execute()
    End Sub

We add a paint event handler to the picture box.

AddHandler Pict.Paint, _
New PaintEventHandler(AddressOf painthandler)

The paint handler routine just calls the red and blue command's draw methods.

Public Sub PaintHandler(ByVal sender As Object, _
          ByVal e As PaintEventArgs)
    Dim g As Graphics = e.Graphics
    redc.draw(g)
    bluec.draw(g)
End Sub

The Command Pattern in the VB Language

There is still another way to approach this. If you give every control its own EventHandler class, you are in effect creating individual command objects for each of them.

Thought Questions

Thought Questions
  1. Mouse clicks on list box items and on radio buttons also constitute commands. Clicks on multiselect list boxes could also be represented as commands. Design a program including these features.

  2. A lottery system uses a random number generator constrained to integers between 1 and 50. The selections are made at intervals selected by a random timer. Each selection must be unique. Design command patterns to choose the winning numbers each week.

Programs on the CD-ROM

CommandButtonMenu VB6 Buttons and menus using Command pattern
CommandRadioCommands VB6 program showing Commands applied to radio buttons
CommandUndo VB6 program showing line drawing and Undo
CommandVBNetButtonMenu VB7 menus and button commands
CommandVBNetComdHolder VB7 program showing CommandHolder interface
CommandVBNetUndoComd VB7 program showing line drawing and undo
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset