Proper Usage of Form.AcceptButton and Form.CancelButton Properties in Windows Forms Applications


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.

Form's AcceptButton and CancelButton properties

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:

Form's AcceptButton and CancelButton properties

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.