Introduction
When coding Windows Forms applications, programmers should make sure that
Form's AcceptButton and CancelButton properties are correctly set at al times.
Doing so ensures that the user will always be able to perform default actions in
a single keystroke, which is a favorable feature. On the contrary, if these
property values are not maintained, most frequent actions would require a mouse
click on a button or traversing the form using the tab button. In both cases,
user will swing with arms quite a lot, and it is certainly not something to aim
for.
In this article we will first pass from simple to more complex cases which
programmers might encounter in practice and then provide a full scale solution
to the problem of maintaining AcceptButton and CancelButton properties in form
of ready-to-use set of classes. Finally, we will provide an example which
demonstrates how simple it is to use these classes.
Please feel free to download the attached source code, because it will not be
completely shown in this article due to its overall length.
Use Cases
The simplest case is a form which has a statically assigned AcceptButton and
CancelButton properties, like the form presenting a simple yes/no question, with
Yes and No buttons being the only active elements on it. Things get a bit more
complicated when other controls are added: for example, a text box receiving
file path, and an associated browse button next to it. When text box is focused,
user would not expect form to be closed when Enter key is pressed. Maybe it is
then better to set the browse button to be the AcceptButton on the form while
the focus is on the text box. Only when text box loses focus, AcceptButton would
be returned to previous value. That can be easily implemented by handling Enter
and Leave events on the text box control.
More complex example is a form which contains a list box and an associated
button which is used to move one item from the list to another list. We would
like this button to be the AcceptButton on the form when list box is focused, so
that Enter key may be used to quickly move the item out of the list box.
However, when list box is empty we might desire not to have any accept button,
i.e. to set AcceptButton property to null. This is because we do not want user
to press Enter key by mistake or by habit, and thus unintentionally to close the
form. In either case, when list box loses focus, we want AcceptButton to be
reset to previous value.
But what happens if form contains user controls, which operate in a way unknown
in advance. User control may have on requirements regarding AcceptButton and
CancelButton properties. Even then, complex user controls may contain other user
controls with even more specific criteria concerning the default buttons on the
form.
These cases have shown that maintaining proper values of AcceptButton and
CancelButton properties may not be an easy task at all. This task should
therefore be planned and implemented correctly both for the form and for user
controls that might be placed on it. Further on we will show the full solution
that can be applied to any Windows Forms application.
Solution Architecture
This solution starts from the idea that particular controls may be aware that
they have specific requirements regarding AcceptButton and CancelButton
properties of the parent form. We may designate such controls to be clients in
sense that they are providing requests regarding accept and cancel button to an
appropriate server.
Server, on the other hand, collects all the requests and handles them as it
finds suitable at run time. For example, client might declare that whenever a
focus is on a particular text box, AcceptButton should be switched to a
particular button associated to that control (what does this association mean,
that is up to the client to know). Similarly, user control which is a client
might require the server to set accept button when list box has received focus,
and to set accept button to null when that list box has focus and no item is
selected in it; in either case, accept button would be restored when list box
loses focus.
Criteria used to decide when to set or to reset accept or cancel button
references may obviously be complex, or at least arbitrary. From that comes the
idea that client and server should communicate using a third object called the
rule. Every rule object would be capable to test any conditions of interest, and
simply to raise an event to signal that conditions are met to perform the
particular change in accept or cancel button setting. That would make the
solution quite general, and cases which we have mentioned earlier would only be
implemented as different rule classes.
Client, server and rule are defined via their appropriate interfaces.
IDefaultButtonsClient interface is defined so that clients are able to publish
their rules to the server, as follows:
public
interface
IDefaultButtonsClient
{
void RegisterRules(IDefaultButtonsServer
srv);
}
This means that server contacts clients and asks them to report rules, rather
than the opposite, which might look more natural. This is because we want to put
the stress on the server, with most of the logic placed in it, which would
finally lead to a single reusable server class.
The IDefaultButtonsServer interface proposes AddRule, RemoveRule and ClearRules
that will be used by client objects to register and unregister all rules they
are concerned with, as given:
public
interface
IDefaultButtonsServer
{
void AddRule(IDefaultButtonsRule
rule);
void RemoveRule(object
key);
void ClearRules();
}
The IDefaultButtonsRule interface represents the rule itself:
public
interface
IDefaultButtonsRule
{
event
EventHandler<ApplyRuleEventArgs>
ApplyRule;
object Key { get;
}
}
Rule objects raise the ApplyRule event whenever particular implementation senses
that conditions are met to change the AcceptButton or CancelButton on the form.
IDefaultButtonsRule also provides the Key property, which should uniquely
identify the rule within all the rules added to the server, so that it can be
referenced later.
Note that ApplyRule event provides ApplyRuleEventArgs as argument, which exposes
three properties: button control reference and two Boolean flags, first
indicating whether accept or cancel button is affected, and second property
specifying whether default button should be set to new value or reset to
previous value. Here is the declaration of the ApplyRuleEventArgs event
arguments class:
public
class
ApplyRuleEventArgs : EventArgs
{
public
IButtonControl Button { get; }
public bool
SetDefaultButton { get; }
public bool
IsAcceptButton { get; }
}
For convenience, we have developed two particular rules. One is named
FocusButtonsRule, which sets accept and/or cancel button to reference specific
buttons whenever given control receives focus. When control loses focus accept
and/or cancel button is reset. Another class is FocusListButtonsRule, which
operates on a given list control and sets accept button when list control is
focused and item is selected. It also sets accept button to null when list
control is focused but no item is selected.
Finally, server is implemented in the
public
class
DefaultButtonsManager : IDefaultButtonsServer
{
public void
AddRule(IDefaultButtonsRule rule);
public void
RemoveRule(object key);
public void
ClearRules();
public Form
Form { get; set;
};
}
This class should be instantiated once in
the form class and reference to the form should be given to the instance by
setting its Form property. That will ensure that manager object finds out all
clients contained in the form, and collects all rules from them. Further on, it
subscribes to ControlAdded and ControlRemoved events on all contained controls,
so that all clients dynamically added to the form may be recognized later at run
time and questioned to register their rules as well. This strategy ensures that
using this manager is quite simple, while all the complex details are hidden
within its internal logic.
The following section will provide an example which demonstrates how easy it is
to use this complete architecture on a practical case.
Example
We will demonstrate use of classes presented in this article on an example of a
form which contains couple of controls. One of the child controls is a user
control which further on has its own child controls. The following picture shows
the form.
Text box with associated button is used to enter path to the file. List box
controls with two associated buttons are actually a single user control which
operates on its own. Rules for AcceptButton and CancelButton property values on
the form are these:
- By default, OK and Cancel are accept and
cancel button on the form.
- When text box contains focus, browse
button next to it should be the accept button.
- When list box control contains focus and
an item is selected in it, then corresponding button used to move item from
that list should be the accept button.
- When list box control contains focus and
no item is selected in it, then form should not have accept button set, i.e.
AcceptButton property should be null.
To implement these rules, one would have to
handle Enter and Leave events on the text box and to modify the user control in
order to implement similar logic in it. Instead of doing so, programmer might
plan these activities in advance, and then to implement IDefaultButtonsClient
interface both in the form and in the user control.
All modifications to the code required to implement the stated rules are these:
public
class MainForm
: Form,
IDefaultButtonsClient
{
private
DefaultButtonsManager _mgr;
public MainForm()
{
...
_mgr = new
DefaultButtonsManager(this);
}
public void
RegisterRules(IDefaultButtonsServer srv)
{
srv.AddRule(new
FocusButtonsRule(_fileName, _browseButton,
false, null,
true));
}
}
public
class TwoListControl
: UserControl,
IDefaultButtonsClient
{
public void
RegisterRules(IDefaultButtonsServer srv)
{
srv.AddRule(new
FocusListButtonsRule(_leftPane, _moveToRight));
srv.AddRule(new
FocusListButtonsRule(_rightPane, _moveToLeft));
}
}
As you can see, form and the user control have only declared their specific
rules for accept button. In addition, form had to instantiate the default
buttons manager object and to provide it with reference to self as the last line
in the constructor - this ensures that all child controls have already been
created, which simplifies rules registration code (reducing it to total of three
lines of code in this case).
Result of this modification is that AcceptButton property on the form is now a
function of particular contents of the form, as well as of currently focused
control. For example, if focus is moved to the list box on the left, form will
look like this:
As soon as item in the list has been selected, accept button reference has been
moved to the button used to move items between list boxes. This action has been
performed by the default buttons manager instantiated in the form. When list box
loses focus, OK button will become the form's accept button again.
Conclusion
In this article we have shown that AcceptButton and CancelButton property values
must be maintained on the form carefully when programming Windows forms intended
to be easy to use. Default buttons play significant role as they allow user to
perform default action on the form with a single push on the button. In this
article we have presented a set of classes which help manage default buttons on
the form in a declarative manner, by simply naming rules that should be applied
when determining which button control on the form should have specific default
button role. Please feel free to download the attached file for the source code
of all classes described in this article.