.NET: CausesValidation, Validating and the Escape Key

I had a bug report recently which said that some of my dialog windows didn't close when the Escape key was pressed. As it was for a C# Windows Forms application, I figured I must have forgotten to set the CancelButton for the form; I was surprised I'd missed it, but it seemed like the most logical explanation.

In .NET forms, the CancelButton property can be set to a control which will be activated if the user presses the Escape key.

When I checked the form, I found that the CancelButton property had been set correctly. The problem seemed to be that the validation routine on the active control was blocking the simulated Cancel click as the Escape key was pressed.

This wasn't supposed to happen, unless I hadn't set the CausesValidation property of the Cancel button to false? The CausesValidation property allows the focus to transfer from controls which have validation events, to other controls, such as the Cancel button of a form, without the validation code running. I hadn't made a mistake, both the CancelButton and CausesValidation properties were set correctly, but when the Escape key was pressed, the validation routines kicked in regardless.

A few quick searches led me to a potential work around, overriding the ProcessCmdKey routine on the form. ProcessCmdKey starts on the active control, then bubbles it's way up to the form, offering the opportunity to catch and process a non-input key like Escape. A bit of experimentation left me with this basic work around:-

	protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
	{
		const int WM_KEYDOWN = 0x100;
		if (msg.Msg == WM_KEYDOWN)
		{
			if (keyData == Keys.Escape)
			{
				DialogResult = DialogResult.Cancel;
				return true;
			}
		}
		return base.ProcessCmdKey (ref msg, keyData);
	}

Basically, it's catching the Escape key on the KeyDown event, canceling the form, then returning true to indicate to the framework that the Escape key has been processed. All other key handling is just passed on to the base form. The DialogResult = DialogResult.Cancel line could be replaced by a call to the cancel routine, as used in the event handler for the Cancel button. I had hoped to be able to use the ProcessClick method of the button, but this produced the same unwanted validation symptom as I was trying to work around.

This looked quite promising, and because the Escape key is handled in the form, it shouldn't interfere with the behavior of any child controls, because if they wanted the Escape key, they would have claimed it before ProcessCmdKey bubbled up to the form. Or so I thought. It turns out that a quite common basic .NET control, the ComboBox, doesn't exactly work that way. With the code sample above, if you have a dropped down ComboBox and you press Escape, the entire form will close, not just the dropped down part of the ComboBox. The ComboBox processes the Escape key at a different point in the code, so I needed to add a special case to the work around. I ended up with this:-

	protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
	{
		const int WM_KEYDOWN = 0x100;
		// Only Process Key Down events for the Escape Key
		if (msg.Msg == WM_KEYDOWN)
		{
			if (keyData == Keys.Escape)
			{
				// .net comboBox doesn't claim this command key until
				// later, so Escape won't close the dropped down part of 
				// the comboBox until after the form's ProcessCmdKey runs.
				// Check if the active control is a dropped down combo box.
				if (!IsDroppedDownComboBox(msg.HWnd))
				{
					DialogResult = DialogResult.Cancel;
					// Could call cancel click handler here
					// Return true to indicate that Escape key was handled
					return true;
				}
			}
		}
		return base.ProcessCmdKey (ref msg, keyData);
	}

	protected bool IsDroppedDownComboBox(IntPtr handle)
	{
		// For ProcessCmdKey, use window handle to check if we're 
		// dealing with a dropped down ComboBox.
		Control active = Control.FromHandle(handle);
		if (active is ComboBox)
		{
			return (active as ComboBox).DroppedDown;
		}
		return false;
	}

I'm reasonably happy with it so far, it processes the key correctly, and the form gets cancelled regardless of whether the active control needs validated. Maybe someone out there has a better solution?