[Update] The entire source for this post can be found here. Just cut and paste into a WinForm.
I was recently tasked with writing a change password dialog box for a windows forms application we are shipping soon. This sounds simple enough (I've only written dozens for ASP.NET), however I have a minimal background in Win32 programming...been doing ASP.NET for quite some time now, so I knew it would be an interesting adventure moving from the stateless world of http to the stateful world of WinForms. The constraints behind the form were straightforward:
- Three fields, one for the old password, one for the new password, and one to confirm the new password.
- None of the fields can be blank on form submittal.
- The new password and confirm new password fields must contain the same value.
- If a control is not valid, set focus to the control.
- If the form is closed, all field values are set to empty strings (this is because modal forms maintain state between calls, more on that later).
The ErrorProvider winforms control was the obvious choice for data validation, however this immediately started to gum stuff up, mainly because I was showing this form via the ShowDialog method, thus making it a modal dialog box. I found out through some research that modal dialogs behave completely different than their modal-less counterparts, mainly in that when they are closed via either the X button, or a cancel button, they aren't really closed at all; a DialogResult of DialogResult.Cancel is returned via the ShowDialog method call, and the modal form's Hide method is called. This was getting in the way of running validation code as if the form was closed by either of these mechanisms, validation needed to be short-circuited. Even though the modal form was merely being hidden, the Closing event was being fired, so I couldn't put my validation code there as if anything wasn't valid, it would lock focus to the invalid control and the user would be stuck with no way out of the form. Testing to see who sent (object sender) the event always resulted in the sender being the form, not a constituent control. It also turns out there is no control name for the X button, so trapping for that control was also a major hurdle. So here was the list of issues I was having initially:
- Override the ErrorProvider's tendency to lock focus on a control and not let the user navigate away until it was valid.
- Trap for both the cancel button and the X button to short-circuit validation, but still run validation in the closing event of the form.
- Clear all the textboxes when the form is hidden, without violating validation. When the form is re-displayed, set focus to the old password textbox.
- Recursively run validation on the textboxes (making sure they all have values, plus the new and confirm must match without either being blank -- both being blank matches and is technically valid).
At this point I was really starting to question the sanity of using the ErrorProvider control as it was creating more issues than it was solving. In ASP.NET, there are multiple validation controls that don't mind being sandwhiched together on one control to validate. IMO, ASP.NET has much better validation handling than WinForms does, but that's another post. The ErrorProvider control is there for a reason, so I decided to forge ahead and make them work for me.
The solution to the first bullet point above seems trivial enough, but it took some some serious head scratching to come up with. How to override the ErrorProvider control's tendency to lock focus on the invalid control? The answer is to define a delegate to a method accepting the control to set focus to as a parameter, then call that delegate on the control's BeginInvoke method and pass the name of a routine that sets the focus to that control. Here is the code:
1private delegate void setFocusEventHandler(Control ctrl);
2
3private void doSetFocus(Control ctrl)
4{
5 ctrl.Focus();
6}
7
8private void setFocusDeferred(Control ctrl)
9{
10 ctrl.BeginInvoke(new setFocusEventHandler(doSetFocus), new object[] {ctrl});
11}
Then simply call setFocusDeferred and pass in the control to set focus to from the validation routines (code for those routines later on in the post). BeginInvoke is called later in the forms lifecycle than the ErrorProvider, so in essence this overrides the behavior of locking focus to the invalid control.
Second bullet point; trap for both the cancel button and the X button click events, and bypass validation (still running validation from the form's closing event handler). From my poring over documentation, there is no intrinsic way to trap for either of these, and furthermore, there is no way to trap for the X button without digging into some header file values and overriding the WndProc method in managed code. Yikes! Tapping into the form's message pump from managed code? In the end, it didn't turn out to be that difficult (had to get some direction from some Win32 guys on my team). So, the solution is to define a member level boolean variable, then set this value to true if either the cancel button or X button was clicked, then test for this value in the closing event handler; if it's true, bypass validation and just close the form. Here's the code for trapping for the X button by tapping into the message pump for the form:
1private const int SC_CLOSE = 0xF060;
2private const int WM_SYSCOMMAND = 0x0112;
3
4private bool _isCancelleable = false;
5
6
7protected override void WndProc(ref System.Windows.Forms.Message m)
8{
9 if(m.Msg == WM_SYSCOMMAND && (int)m.WParam == SC_CLOSE)
10 {
11 this._isCancelleable = true;
12 }
13 base.WndProc(ref m);
14}
15
16private void cancelButton_Click(object sender, System.EventArgs e)
17{
18 this._isCancelleable = true;
19}
This ties into the next bullet point, which is to clear all the values of the textboxes (and clear any validation errors the user has encountered along the way) if the form is closed by either cancel or X. The code for this is quite straightforward:
1private void ChangePassword_VisibleChanged(object sender, System.EventArgs e)
2{
3 _errorLabelName = this.errorLabel.Name.ToUpper();
4
5 foreach(Control control in Controls)
6 {
7 if(control.GetType().Equals(typeof(TextBox))
8 {
9 control.Text = "";
10 passwordMatchProvider.SetError(control, "");
11 }
12 }
14 setFocusDeferred(oldPasswordTextBox);
15}
And now for the validation routines themselves. Each textbox gets it's own instance of an ErrorProvider control, and each violation marks the appropriate control with the appropriate error, as well as setting focus to the control (if there are multiple violations, set focus to the topmost control on the form). As violations are corrected, clear the ErrorProvider for that textbox. Here is the code for this:
1private void ChangePassword_Closing(object sender, System.ComponentModel.CancelEventArgs e)
2{
3 if (!_isCancelleable)
4 {
5 if (!isValidated(newPasswordTextBox.Text, confirmPasswordTextBox.Text, newPasswordTextBox))
6 {
7 e.Cancel = true;
8 }
9 else // everything is valid, store textbox values in member vars for property retrieval
10 {
11 _oldPassword = oldPasswordTextBox.Text;
12 _newPassword = confirmPasswordTextBox.Text;
13 }
14 }
15 else
16 {
17 e.Cancel = false;
18 }
19}
20
21private bool isValidated(string string1, string string2, Control controlToFlag)
22{
23 if (string1 + string2 != string.Empty)
24 {
25 if (string1 != string2)
26 {
27 confirmPasswordTextBox.Clear();
28 newPasswordTextBox.Clear();
29 setFocusDeferred(controlToFlag);
30 passwordMatchProvider.SetError(controlToFlag, PASSWORD_MISMATCH_ERROR);
31 errorLabel.Text = PASSWORD_MISMATCH_ERROR;
32
33 return false;
34 }
35 else
36 {
37 return fieldHasText();
38 }
39 }
40 else
41 {
42 return fieldHasText();
43 }
44}
45
46private bool fieldHasText()
47{
48 bool flag = true; // assume there is text
49 int count = 0;
50
51 foreach (Control control in Controls)
52 {
53 if (control.GetType().Equals(typeof(TextBox)))
54 {
55 if (control.Text.Length == 0)
56 {
57 ++count;
58 setFocusDeferred(control);
59 // just a formality to display correct grammar
60 if (count > 1)
61 {
62 errorLabel.Text = MULTIPLE_PASSWORD_BLANK_ERROR;
63 }
64 else
65 {
66 errorLabel.Text = PASSWORD_BLANK_ERROR;
67 }
68 passwordMatchProvider.SetError(control, PASSWORD_BLANK_ERROR);
69 flag = false;
70 }
71 else
72 {
73 passwordMatchProvider.SetError(control, "");
74 }
75 }
76 }
77 return flag;
78}
This should be pretty straightforward and easy to understand, just remember that setFocusDeferred is discussed earlier in the post. So, what about code that calls this form? Just create an instance of the form, then check for the dialog result returned from the modal dialog. In my case, I abstracted out the actual code that changes the password into a passwordAgent class, which in turn communicates with the underlying security framework.
The winforms guys usually complain about moving from the stateful world of Windows programming to ASP.NET, and in this case, the reverse is true. I guess what surprised me most was lack of validation controls in the winforms world, a lot more has to be done by hand, which of course is fine...but when you've gotten used to the plethora of validation controls in ASP.NET, it just seems kind of tedious. Questions/comments are always welcome, and I will post the entire code in an article in the very near future. Happy validating.
Posted
Aug 10 2004, 06:04 PM
by
Jayson Knight