13
votes

I have a simple Form with a single TextBox, plus OK and Cancel buttons. The Form's AcceptButton and CancelButton are set correctly, and the OK and Cancel buttons have their DialogResult set to 'OK' and 'Cancel'.

I want to add validation to the TextBox which will prevent the user from OK-ing the form when validation fails, but which will also allow them to cancel as usual.

The CausesValidation property is True by default on all the controls, but I have changed this to False on the Cancel Button.

Sure enough, clicking OK or pressing the Enter key will run the Validating event I wired up to the TextBox. Pressing the Cancel button bypasses Validating, which is perfect.

However, pressing Escape to cancel the form does not perform the same as pressing the Cancel button - it raises the Validating event and prevents the user from exiting.

Is there any way of making the Escape key perform as intended, i.e. not raise the Validating event, just as if the Cancel button had been pressed?

A complete worked solution is:

Create a new Windows Forms app. Add a second Form to the project.

Paste this code into Form1's constructor, after InitializeComponent():

MessageBox.Show((new Form2()).ShowDialog().ToString());

This shows the DialogResult passed back from our second form.

Paste this code into Form2's constructor, after InitializeComponent():

TextBox txtName = new TextBox();

txtName.Validating +=
    new CancelEventHandler((sender, e) =>
    {
        if (txtName.Text.Length == 3)
        {
            MessageBox.Show("Validation failed.");
            e.Cancel = true;
        }
    });

Button btnOk = new Button
{
    Text = "OK",
    DialogResult = DialogResult.OK
};
Button btnCancel = new Button
{
    Text = "Cancel",
    CausesValidation = false,
    DialogResult = DialogResult.Cancel
};
FlowLayoutPanel panel = new FlowLayoutPanel();
panel.Controls.AddRange(new Control[] 
{
    txtName, btnOk, btnCancel 
});

this.AcceptButton = btnOk;
this.CancelButton = btnCancel;

this.Controls.Add(panel);

In this simplified example the textbox will not let you proceed if there are 3 characters input. You can press the Cancel button or close the form directly even if there are 3 characters present; however pressing the Escape key will not do the same - it fires the Validating event whereas it should be doing the same as pressing Cancel.

4
"WinForms validation prevents escape key from closing the form" I have the same problem.Colonel Panic

4 Answers

16
votes

Yes, this an awkward quirk of the ValidateChildren method. It doesn't know that canceling was intended. Paste this code to fix the problem:

    protected override void OnFormClosing(FormClosingEventArgs e) {
        base.OnFormClosing(e);
        e.Cancel = false;
    }

To avoid having a Validate event handler running that causes side-effects, like a message box, add this statement to the top of the method:

    private void txtName_Validating(object sender, CancelEventArgs e)
    {
        if (this.DialogResult != DialogResult.None) return;
        // etc..
    }

Paste this code into your form to get the DialogResult set before it tries to validate the form:

    protected override bool ProcessDialogKey(Keys keyData) {
        if (keyData == Keys.Escape) this.DialogResult = DialogResult.Cancel;
        return base.ProcessDialogKey(keyData);
    }
6
votes

I just saw this problem as I was hunting a solution for the same and the override of ProcessdialogKey is the MS-approved solution until they fix the bug (Escape should do the same as clicking Cancel). A discussion of this bug is also found here (just working with Visual Basic instead of C#. Bug is over 5 years old and apparently still not fixed): Bug or Feature? CancelButton vs Escape Key I am trying to work out the C++ solution.

Edit to add: The solution from the link in C#:

protected override bool ProcessDialogKey(Keys keyData)
{
    if (keyData == Keys.Escape)
    {
        this.AutoValidate = AutoValidate.Disable;
        cancelButton.PerformClick();
        this.AutoValidate = AutoValidate.Inherit;
        return true;
    }
    return base.ProcessDialogKey(keyData);
}
1
votes

Actually, that causes new problems, since it intercepts escape that other controls should use, e.g. if you have a combobox dropped, pressing escape should close the combobox and not exit the dialog, which the above code will do.

It would be possible to exempt certain control types on the escape key event but that's not a good solution, it's just a matter of time until another control that uses escape internally is introduced into the form, e.g. a spread control where edit mode should be exited on escape.

IMO it's pretty retarded that they runs validate on escape. Does anyone know what the idea behind that is, because it's not a bug... but it IS a bug.

If that code were to call base.ProcessDialogKey(keyData) instead of cancelButton.PerformClick then you would be closer to a solution since it would be up to someone else to determine what to do with the escape key. But setting AutoValidate to Disabled here and then returning it to it's original value doesn't prevent the validation since it, probably do to simply posting events and places a message on the queue, doesn't use that value until after it has been set back to it's original value.

By just setting it to Disabled and not returning it to the original value, it will work, but if the escape key then is intercepted by e.g. above mentioned dropped combobox, then you all of a sudden have disabled validation also on OK.

Touché!

Anyone have any other bright ideas on how to get this to work, without having to specify all possible kinds of controls that do and what the conditions are when they need escape, e.g. control is ComboBox and check if it's dropped down and if so bail out.

0
votes

Neither the ProcessDialogKeys nor Validating handler answers worked for me, perhaps because I'm using an errorProvider instead of a MessageBox. The simplest answer I got to work is to forget about Validating and just use the following:-

buttonOk.Click += (_,__) =>
{
  if(txtName.Text.Length == 3)
  {
    errorProvider1.SetError(txtName, "Wrong Length!");
    DialogResult = DialogResult.None;
  }
  else
  {
    errorProvider1.SetError(txtName, string.Empty);
  }
};