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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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
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.
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.
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.
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 |