| |
| Introduction |
| |
| Most Windows Forms projects consist
of a basic set of UI controls like the main menu, toolbar and other
buttons. The user will be able to accomplish the same action by using
different controls – he/she could either click a button, a toolbar
button, or the respective main menu item. A good example would be
saving a document in Microsoft Word – the user could accomplish
this by either clicking on the Save icon in the toolbar, or selecting
Save in the File menu or by just pressing Ctrl+S. |
| |
| You could handle these actions by attaching
separate event handlers for each of the controls. This means that
for the same action, you will have to duplicate the code at say the
toolbar button click event, the button click event, and the main menu
item click event. Before long, you would end up writing a lot of redundant
and messy code which would be tough to manage. |
| |
| This article takes you through an implementation
that would centralize user-interface event handling code irrespective
of the control from which the event was raised. The accompanying code
illustrates the implementation. |
| |
| The design |
| |
| Let’s consider a simple Windows
Forms project, where the user can execute a few actions namely –
New, Save, Exit and About. Assume that we have a main menu, a toolbar
and a number of buttons with which the user can execute any of these. |
| |
| Define an action enumeration |
| |
| The first step to achieve centralization
is to define a list of all actions that can be performed in the UI
using the controls. The best approach for this is to use an enumeration. |
| |
protected
enum Action
{
New,
Save,
Exit,
About,
} |
|
| |
| Define a control-to-action
map |
| |
| Each of the UI controls map to only
a single action. For example, a ‘New’ button maps to only
Action.New. To identify which action
goes with which control, we define a control to action map by using
a Hashtable. |
| |
| protected
Hashtable _controlActionMap = null; |
|
| |
| The keys for this Hashtable
will be the instances of the controls, and the values will be one
among the Action items. We populate the
Hashtable in the form load event handler
with the necessary entries. |
| |
private
void Form1_Load(object sender,
System.EventArgs e)
{
_controlActionMap = new
Hashtable();
_controlActionMap.Add(NewMenuItem,
Action.New);
_controlActionMap.Add(SaveMenuItem,
Action.Save);
_controlActionMap.Add(ExitMenuItem,
Action.Exit);
_controlActionMap.Add(AboutMenuItem,
Action.About);
_controlActionMap.Add(NewButton,
Action.New);
_controlActionMap.Add(SaveButton,
Action.Save);
_controlActionMap.Add(ExitButton,
Action.Exit);
_controlActionMap.Add(AboutButton,
Action.About);
_controlActionMap.Add(NewTButton,
Action.New);
_controlActionMap.Add(SaveTButton,
Action.Save);
_controlActionMap.Add(ExitTButton,
Action.Exit);
_controlActionMap.Add(AboutTButton,
Action.About);
}
|
|
| |
| Centralize the event handlers |
| |
| Next, we define a generic method HandlerUserAction
which would handle the click event of all the controls. We attach
handlers to the click event of the controls pointing to this method. |
| |
| private
void HandleUserAction(object
sender, System.EventArgs e) |
|
| |
| Unfortunately, the ToolbarButton
click handler does not share the same method signature since it uses
ToolBarButtonClickEventArgs as its event
handler argument type. This can be easily resolved by adding an overload
to the HandleUserAction method with ToolBarButtonClickEventArgs
as the argument type. We then simply invoke the second HandleUserAction
method backward casting argument to System.EventArgs. |
| |
private
void HandleUserAction(object
sender,
System.Windows.Forms.ToolBarButtonClickEventArgs e)
{
HandleUserAction(sender, (System.EventArgs)e);
} |
|
| |
| The task for HandlerUserAction
method is to figure out the sender and the action to be executed.
We can easily identify the type of the sender by using the GetType
method. But if this event was raised by a toolbar button, then the
actual sender is the Button instance
passed in the ToolBarButtonClickEventArgs
object. To handle this, we add a little condition block. |
| |
private
void HandleUserAction(object
sender, System.EventArgs e)
{
object
senderControl = sender;
if(e.GetType()==
typeof(
System.Windows.Forms.ToolBarButtonClickEventArgs))
{
senderControl
= (
(System.Windows.Forms.ToolBarButtonClickEventArgs)e)
.Button;
}
} |
|
| |
| Now, we need to find out which action
is to be executed. We use the Hashtable.ContainsKey
method to find out whether an action has indeed been defined for this
object. |
| |
private
void HandleUserAction(object
sender, System.EventArgs e)
{
object
senderControl = sender;
if(e.GetType()==
typeof(
System.Windows.Forms.ToolBarButtonClickEventArgs))
{
senderControl
= (
(System.Windows.Forms.ToolBarButtonClickEventArgs)e)
.Button;
}
if(senderControl!=null
&& _controlActionMap.ContainsKey(senderControl))
{
//
execute the action
}
} |
|
| |
| Though it’s possible to add code
to execute actions in the HandlerUserAction
method itself, we add another method DoAction
to keep things organized. This method would be where the final actions
will be carried out. Obviously, this method would take an Action
as the argument. |
| |
| Hence, our HandlerUserAction
method would invoke DoAction passing
the Action type. |
| |
private
void HandleUserAction(object
sender, System.EventArgs e)
{
object
senderControl = sender;
if(e.GetType()==
typeof(
System.Windows.Forms.ToolBarButtonClickEventArgs))
{
senderControl
= (
(System.Windows.Forms.ToolBarButtonClickEventArgs)e)
.Button;
}
if(senderControl!=null
&& _controlActionMap.ContainsKey(senderControl))
{
DoAction((Action)_controlActionMap[senderControl]);
}
} |
|
| |
| Define the central action method |
| |
| The DoAction
method, would a simple switch-case block
where we would finally code for executing the required action. |
| |
private
void DoAction(Action
actionType)
{
switch(actionType)
{
case
Action.New:
MessageBox.Show("You
clicked 'New'");
break;
case
Action.Save:
MessageBox.Show("You
clicked 'Save'");
break;
case
Action.Exit:
MessageBox.Show("You
clicked 'Exit'");
break;
case
Action.About:
MessageBox.Show("You
clicked 'About'");
break;
}
}
|
|
| |
| To summarize |
| |
| Let’s have an overall look on
the steps required to implement this design. |
| |
| 1. |
We define an enumeration with
all the actions that can be carried out. |
| 2. |
We define and populate a Hashtable
to map our UI controls to the respective actions. |
| 3. |
We define a user handler method
and add event handlers to all the controls pointing this method. |
| 4. |
Additional overloads for the
user handler method are defined if necessary. |
| 5. |
The method would identify the
proper sender object, and the action to be executed. It finally
invokes the central action method passing the action enumeration
type. |
| 6. |
The central action method would
execute the actions steps. |
|
| |
| Having developed the foundation, adding
new actions and controls would be quite simple. You just need to point
the new control’s event handler to your user handler method,
add another entry in the control-to-action map Hashtable,
and code for the action in central action method. As keyboard shortcuts
form part of menu items, they would work fine with no extra code. |
| |
| Parting thoughts |
| |
| • |
With Whidbey, you could achieve
more type-safety and performance by using generics for the control-to-action
map. |
| • |
Using the Command design pattern
coupled with the DoAction method
could provide you a good deal of flexibility including support
for undo. |
|
| |
| |
Download
the source code (After downloading
change the extension of the file to .zip) |
| |
| |
| |