Effectively Using Action Lists

By: Ray Konopka

Abstract: This paper describes, in detail, what actions and action lists can do for an application.

Action lists were introduced back in Delphi 4. However, this highly powerful feature of Delphi is still widely underused and in many cases misused by developers. This paper describes, in detail, just what actions and action lists can do for an application. However, the most important aspect of this paper is a set of guidelines for effectively using actions in an application.

CD Player: Take 1

The sample application that we will be converting in this article is the CD Player program shown in Figures 1 and 2. Well, it doesn't really play CDs, but the two list boxes are completely functional and so are all the buttons and menu items. This is fine because it's the list boxes that we are interested in and the actions that can be performed on them. By the way, the display in the upper right corner of the window is just an image.

Figure 1: The CD Player manages two list boxes of song titles.

Figure 2: The CD Player provides multiple ways to manipulate the list boxes.

Since our CD player does not actually read CDs, we have to assume that when a new CD is loaded the Tracks list box is automatically populated with the song titles of that CD. For our purposes, I've simply populated the Tracks list at design-time with the tracks from a-ha's first album.

When the player is running, the user selects the songs to be played by manipulating the Play List. Clicking the Add button adds all selected song titles in the Tracks list to the Play List. The Select All and Unselect All buttons are self-explanatory. Once the Play List contains some tracks, the user can change the order in which the tracks are played by selecting a track and then clicking the Move Up or Move Down buttons. Of course, tracks can be removed from the Play List by selecting the track and clicking the Delete button. Clicking the Clear button clears the entire list. In addition, Figure 2 shows that there are popup menus associated with each list box that provide access to this same set of commands-oh, I mean "actions."

MainForm1.pas is the form unit for the first version of our CD player. This first take does not use a TActionList. Instead, it is built using traditional Delphi techniques. That is, event handlers are written for each popup menu item to perform the desired actions. The OnClick event handlers of all the buttons simply point to the corresponding menu item event handler. This is straightforward Delphi coding and the ability to have multiple events handled by the same event handler reduces the amount of code we write.

However, sharing event handlers does not address the problem of updating command status. For example, in Figure 1, the Play List is empty and therefore none of the Play List commands are enabled. Likewise, in Figure 2, since the selected track in the Play List is the last track, the Move Down command is disabled.

In traditional Delphi applications, command enabling is handled in a manner similar to what is shown in MainForm1.pas. In this example, the UpdateStatus method takes care of updating the command status of all the buttons and menu items. However, there are two important issues regarding this approach. First, the enabling code for each command must be duplicated for each control that invokes the command. Second, the UpdateStatus method must be called at various times throughout the program.

A New Script

Delphi 4 provides a new approach to handling command implementation and command enablement through the use of action lists and actions. An action list is a nonvisual component that contains a set of TAction components. An action list provides design-time access to the individual action components, much like a menu component provides access to a set of menu items.

A TAction component implements a command, such as deleting a selected item, on a target, such as a list box. An action is invoked by a client control in response to some user command, typically a mouse click. The most common client controls are buttons and menu items. A client control is connected to a particular action by setting the control's Action property. This results in a link between the client control and the action component.

Figure 3 shows an example of how client controls are connected to actions. For instance, the EditCut1 action is assigned to SpeedButton1's Action property. When the assignment is made, several of the speedbutton's properties are changed based on the corresponding properties of the EditCut1 action. For example, the button's Caption is automatically changed to 'Cu&t'. When the user clicks the Cut button, the command implemented by the EditCut1 action is invoked.

Figure 3: Client controls are connected to actions via action links.

CD Player: Take 2

So now, let's take a look at the steps required to convert our CD player to use actions. First, we drop a TActionList component on the form. Next, we need to add new actions for each command associated with the list boxes. We accomplish this by invoking the Action List Editor and clicking the New Action button for each command. Figure 4 shows the Action List Editor along with the Object Inspector. As you can see, when you select an action in the editor, the Object Inspector shows all the published properties associated with it. For each action, set the desired properties.

Figure 4: The Action List Editor.

Next, we create and event handler for the ActionList1.OnUpdate event. The event handler will use the same code as the UpdateStatus method from Take 1, but instead of updating the Enabled property of both the buttons and menu items, only the Enabled property of each action is updated. We also need to set Handled to True at the end of this event handler (I'll explain why later). Next, we can remove the UpdateStatus method and all references to it.

The next step involves writing an event handler for each action's OnExecute event. You can generate the event by double clicking the action in the Action List Editor. In each method, simply move the code from the old menu or button OnClick event handler to the new action OnExecute event handler.

The final step is to connect all of the client controls to the appropriate action. For example, the BtnMoveUp.Action property is set to ActMoveUp. It is also important to remember to set the Action property of each menu item as well.

At this point, we can rebuild the application and run it. You will notice that the application behaves identically to the first version, but with fewer lines of code. In addition, the new version is also much easier to maintain. In addition, we were even able to add some custom hint processing for the delete action because TAction defines an OnHint event.

The Backstage Tour

As you can see, it is pretty simple to use actions. However, for a truly Oscar winning performance you need to have a much better understanding of how actions work. For example, if you performed the steps described in the previous section, you probably noticed that both TActionList and TAction define OnExecute and OnUpdate events. Unfortunately, it is not clear from the Delphi documentation when each event should be used. So, let's go on a backstage tour of the inner workings of the key classes that support actions.

The Update Building

Delphi 4's Application object knows about actions and action lists, and if your application uses any, the Application object will generate OnUpdate events whenever the application is idle.

For each action list, the TActionList.OnUpdate event gets generated first, and the handler for this event receives two parameters. The Action parameter represents the action that is being updated. The Handled parameter is used to control whether the OnUpdate event for the passed in action gets generated. If you do not want the action specific OnUpdate event to be generated, set Handled to True in the TActionList.OnUpdate event handler.

It is important to note that TActionList.OnUpdate is generated for each client control connected to any action contained in the list. For example, suppose ActionList1 contains Action1 and Action2. Now suppose Button1's and SpeedButton1's Action properties are set to Action1 while SpeedButton2.Action is set to Action2. Under these circumstances, the ActionList1.OnUpdate event will be generated a total of three times for each update cycle. The OnUpdate event handler is called twice with the Action parameter set to Action1 and the handler is called a third time with the Action parameter set to Action2.

Unfortunately, there are no real guidelines for when to use which OnUpdate event. However, the TActionList.OnUpdate event is often easier to manage because all of the status enabling code is located in one place and not in separate event handlers, and command enabling code is generally short.

You may have noticed that you can drop multiple action lists on a form. For example, the RichEdit demo application that comes with Delphi 4 and 5 uses two action lists. Why use separate action lists? For performance reasons. Because of the way action lists are updated, if you have a set of actions that do not need to be enabled/disabled, then it makes sense to put these actions in a separate action list that does not have an OnUpdate event handler. Remember that the OnUpdate event fires once for each client control connected to any action contained in the list. Therefore, even if an action does not have to be enabled and disabled, as long as a client control is linked to the action, a separate OnUpdate event for the action list will be generated.

It is also very important to realize that any OnUpdate event handler gets called a lot. Therefore, do not include code that takes a long time to execute in your OnUpdate handlers. If you do, you will notice degradation in your application's performance.

The Execution Offices

Executing an action is handled in a similar way to updating an action. When a client control instructs the associated action to be executed (e.g. on a mouse click), the action list containing the action generates its OnExecute event. Within the handler for this event, you could write the code that performs the action.

However, you must be sure to test the action that is passed to the event handler so that you perform the correct action at the appropriate time. The reason this is important is because the TActionList.OnExecute event gets generated whenever any action contained in the list needs to be executed.

As a result, I recommend creating an event handler for the OnExecute event of the action object instead. The benefit is that this event is only generated when the specific action needs to be performed. You might consider writing an event handler for a TActionList.OnExecute to perform some preprocessing before each action is executed, but I would not recommend this. Instead, I would suggest that each action's OnExecute handler call a common method. The result is a solution that is much easier to follow and does not rely on a subtle behavior of TActionList.

Unfortunately, the figure in the Delphi online help describing the execution process of actions is a bit misleading with respect to the order in which events are generated. Figure 5 shows a similar figure but shows the order of events more clearly. Notice that the action's OnExecute event occurs after the action list and Application objects get a chance to process the action.

Figure 5: Executing an action involves several steps.

The Wardrobe Department

In addition to OnUpdate and OnExecute, TActionList defines an OnChange event, which in my opinion is a rather strange event. This event is generated only when the Category property of an action in the list is changed or the Images property of the list is changed. What makes this event strange is that I'm not sure why this event is even available. The Category property is simply used at design-time to group actions together. There is no reason to change the Category of an action at runtime and besides, what would you do in response to this change in an event handler.

The Cue Card Department

The OnHint event of TAction is much more interesting. Providing an event handler for this event gives you the opportunity to customize the hint that is displayed on a client control connected to the action. There are two parameters passed to an OnHint event handler. The first is HintStr, which is used to customize the hint string. In our example, we handle the ActDelete.OnHint event by customizing the hint to show the contents of the currently selected item. For example, if the mouse were positioned over the Delete button in Figure 2, the hint would be:

Delete "The Sun Always Shines on T.V. "

Also notice that the second parameter, CanShow, is used to only show the hint if an item is selected. Unfortunately, there is a little problem with the OnHint event and the CanShow parameter when used with controls that do not take the input focus, such as TSpeedButton. In this situation, the CanShow parameter will only work correctly if the Hint property for the action is not set (that is, an empty string). If the Hint property is set, then this "default" hint string is displayed regardless of what you set CanShow to.

Moving Pictures

As you can see in Figure 2, the CD Player utilizes Delphi 4's ability to include glyphs next to menu items. Unfortunately, there are some issues that you must be aware of when using action lists and menus with images. First, it is not sufficient to simply assign the image list to the TActionList.Images property. You must also assign the image list to the menu's Images property.

The second issue involves the connection, or lack thereof, between an image list and client controls connected to actions in an action list. For example, let's suppose you have an action list with one action, and there is an image list connected to the action list. Consider a speed button that is connected to the action and as a result shows the glyph associated with the action. The glyph is stored in the image list, of course.

Now, let's suppose you want to change the image associated with the action. One would expect to be able to simply modify the image list component. Although modifying the image list is required, it is unfortunately not sufficient to accomplish this task. Unlike the other action properties (such as Caption and ShortCut), changes to an action's associated glyph are not propagated to all client controls connect to an action. As a result, after modifying the image in the image list, you must clear the Glyph property of the client control and then re-select the desired action using the client control's Action property. This will reset the control's Glyph property to match the image stored in the image list.

Unfortunately, there is yet another problem with actions and speed buttons and images. Specifically, if you do not want an action's image to appear on a speed button, you have to clear the speed button's Glyph property at runtime (e.g. the form's OnCreate event). It is not sufficient to clear the Glyph property at design-time because the change does not appear to be streamed out correctly. The result is that although the speed button does not have the glyph showing at design-time, when the form is loaded at runtime, the action connected to the speed button resets the Glyph property to the image associated with the action.

Summary

Actions and action lists were introduced back in Delphi 4. However, this highly powerful feature of Delphi is still widely underused and in many cases misused by developers. One of the challenges of incorporating actions into an application is that the Delphi documentation does not provide any guidelines on how best to utilize them. Unfortunately, make the wrong choice and your application could suffer performance penalties. Throughout this paper, specific attention was given to the way actions work behind-the-scenes. For example, new diagrams that completely (and accurately) describe the update process and the execution process are presented. With a better understanding of the way in which actions are implemented, it was possible to present a set of guidelines that developers can apply when utilizing actions in their own applications.

Contact Information

Ray Konopka rkonopka@raize.com
Raize Software http://www.raize.com

About the Author

Ray Konopka, Raize Software, Inc.

Ray Konopka is the founder of Raize Software, Inc. and the chief architect for their Raize Components and CodeSite products. Ray specializes in Delphi component development and is a frequent speaker at developer conferences.

Paper originally presented at the 11th Annual Borland Conference, July 2000.

Server Response from: SC2