Last Updated: 05/19/03
Let me start by saying, I've looked everywhere, and I can't find a comprehensive
source of information on this topic. I have 3 C# books, and not a single one
discusses dialogs (modal forms) in any reasonable detail. Great amounts of really
important information is either glossed over or omitted altogether.
I've searched online, and maybe I'm just missing, but I can't find anything
worth a damn, so I hope this helps keep people from going through all the headaches
I went through dealing with Dialogs. Coming from the C++/MFC/Win SDK world, I'll
probably use "dialog" and "modal form" interchangably. They both mean the same
thing, though.
I've only begun this page and will be adding more as time goes on. Time is
short at the moment, but if there's an area you'd like to see covered here that
isn't, let me know by sending an e-mail to
pdavis68@hotmail.com
Overview
First of all, my samples and explanations are based in C#, since that's the
language I use. My guess is that this all translates to VB.NET pretty nicely. While
this page is a bit less than comprehensive, I expect to complete it at some point
and actually make it a fairly comprehensive reference. If you have any suggestions,
please e-mail me at pdavis68@hotmail.com
After much searching, I've found code samples to skin a dialog, do dialogs that
are non-rectangular, and all sorts of crazy stuff. What I haven't found is how to
just do a plain old dialog that's robust, has good data validation, and so forth.
I guess us people writing boring apps can forget about decent support ;-) What follows
is simply how to do your "run-of-the-mill" dialog that performs data validation.
Setup
The first thing to do when creating a modal form is to go through the properties
for the form and make the following settings:
- FormBorderStyle=FixedDialog
- MaximizeBox=false
- MinimizeBox=false
- ShowInTaskbar=false
If you are going to use localization (internationalization), do not set
localization to true at this point. See the section below on localization.
Controls
Most controls are fairly straight-forward, and I'm not going to go into how
to use each control because most of that's covered in a number of other places.
What we're concerned with is the dialog itself and the things common to modal
dialogs. Generally you'll have an OK and Cancel button, so we will discuss those.
First, you'll add the two buttons and make the text "&OK" and "&Cancel" or
whatever you want. And give them names such as "okButton" and "cancelButton".
For the OK button you want to set the DialogResult in the properties to "OK"
and for the Cancel button, you want to set the DialogResult to "Cancel". This
provides the return value from the call to Form.ShowDialog() so that you can
determine from the caller, how the user exited the form. Either accepting or
canceling the data.
Now, go back to your form's properties and set the AcceptButton to okButton
and the CancelButton to cancelButton. The reason we do this is that the AcceptButton
property identifies the "Default" button in MFC/SDK terminology. It is the button
that gets pressed if the user hits the Enter key. The CancelButton is the button
that gets pressed when we hit the Esc key.
Data Validation
There are basically two schools of thought on data validation. The authors
of .NET apparently wanted us to validate each control as the user enters data.
The other school of thought is that you validate the controls after the OK
button has been pressed. I'm generally against the notion of validating the controls
as the users enter data. While you can argue that it gives users quicker feedback,
and I'll grant that it does, that's the only advantage it really offers.
The disadvantages are that it locks the user into a certain order of entering data.
Maybe "locks" is too strong a word, but every field you fail to fill out properly as
you're filling out data, becomes marked as such. Second, there are cases where a
single field can't be validated until after other data has been provided. In
this case, field-by-field validation won't work and is actually misleading to the
user, as they may become confused about why no matter what data they enter into
a control, it won't validate. Finally, if you do field-by-field
validation, your validation code is necessarily broke up into a number of event
handlers, one for each field. This is simply messy and difficult to maintain, which
is the biggest problem of all, in my mind. Since the .NET team built .NET around
field-by-field validation, you're kind of stuck out in the cold with no built-in
solution. I did see a transcript between the .NET team and developers where
there was talk of a potential "ValidateAll" event in the future, and that
would be great. I need a solution NOW!
So, I'm from the school that believes in validating the entire form at once.
In addition to the advantages above, it's also how validation has been done for
years and it's what users are used to. When it comes to users, you don't want to
rock the boat. You want them to do what they're used to and what's comfortable
for them. Things start changing, and users get nervous and confused. I don't
like my users to be nervous and confused.
So, to perform form level validation there are a few steps we need to take.
The first is that we need to create a base class from which all of our forms that
perform data validation are derived. Below is what I've called the "ValidatingForm"
class. Discussion follows the listing.
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
namespace SharedForms
{
public class ValidatingForm : System.Windows.Forms.Form
{
private bool formValidated;
///
/// Required designer variable.
///
private System.ComponentModel.Container components = null;
public ValidatingForm()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
formValidated = true;
}
///
/// Clean up any resources being used.
///
protected override void Dispose( bool disposing )
{
if( disposing )
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#region Windows Form Designer generated code
///
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.Size = new System.Drawing.Size(300,300);
this.Text = "ValidatingForm";
this.Closing += new System.ComponentModel.CancelEventHandler(this.form_Closing);
}
#endregion
protected bool FormValidated
{
get
{
return formValidated;
}
set
{
formValidated = value;
}
}
private void form_Closing(object seder, System.ComponentModel.CancelEventArgs e)
{
if (false == formValidated)
{
e.Cancel = true;
formValidated = true;
}
}
}
}
You can set the namespace to whatever makes sense for you. I have a "SharedForms"
namespace where I have several derivable forms and user components that I
reuse. The key here is that we're handling the "Closing" event for the form.
In the closing, we check the formValidated member to see if it's false. If it
is, then the derived class has told us validaton should fail. We set the
e.Cancel to true which will keep the form from closing, and then we reset
the formValidated to true.
In the dervied class, we will handle the OK buttons Clicked event and
either perform our validation directly in the OK clicked event or in a
"ValidateForm" method or something of that sort. If at any point, any data in
the form does not meet our validation criteria, we simply notify the users
(using the ErrorProvider class and/or message boxes) set this.FormValidated=false.
(keep in mind that formValidated is a private member, and FormValidated is
a protected property accessible from derived classes)
ErrorProvider
The ErrorProvider is a great little class, and if you come from the C++/MFC world,
it performs the error notification aspect that DDX/DDV handled in MFC, though
it does it in a different way. Instead of popping up a message box, the ErrorProvider
class puts an icon next to each field that failed to validate properly. Placing
the cursor over that icon provides text describing the nature of the validation
failure (programmer supplied).
The first step to using the ErrorProvider is to simply drop the ErrorProvider
component onto your form from the toolbox. When validation for a control fails,
simply call myErrorProvider.SetError() (where myErrorProvider is whatever you
named the ErrorProvider component). SetError takes two paramters: The control
that failed validation and the string describing the cause of the validation
failure. For example, an empty name text field might require something like:
myErrorProvider.SetError(nameTextBox, "Name cannot be blank").
Localization
Localization is the last thing you want to do to your dialog. When I say last,
I mean, after you've written all your code and tested your dialog in the default
language, then you can go back and localize. I say this because it's happened
to me and I've heard from others, of entire forms getting wiped out because
you localize and then make some innocent change. There appears to be a bug
in some aspect of the form editor, so make a back-up of your form code before
you localize. Or better yet, use source control. If you're already using source
control, check in what you have before localizing.
First of all, if you're using the HelpProvider component, your forms won't
localize. Well, they will, but the code generated won't compile. This is a bug
which is supposed to be addressed but as yet, I'm unaware of it being corrected.
In the meantime, I've actually provided a class
that derives from HelpProvider that can be used in its place and will compile
properly. Simply build the component, add it to your toolbox, and drop it on the
form instead of the normal HelpProvider component. It will act just like the
HelpProvider component (if it doesn't, that's a bug, and please notify me if you
find any).
Okay, so you have your working dialog backed up in source safe or somewhere
else, right? No you can go ahead and set Localizable=true. At this point, Language
will be set to (Default). From here, you want to select the individual languages
you're going to use. Each time you select a language, a duplicate of your dialog
layout information is saved to a new resx file, one for each language. Try to
stick to the languages you're going to actually use. Don't go and select every
language, or you're going to end up with a HUGE resource file.
For each language, go through and translate the text and re-arrange the
controls as necessary. Make sure the Language setting in the properties is
selected to the correct language. You'll probably have to move some controls
around to make room for longer or shorter phrases. In general, in the initial
layout of your controls, try to make enough room for the labels to assume for
the longest language. For example, I develop primarily in English but translate
to Spanish which generally has longer phrases, so I add extra space. Spanish
isn't a very compact language.