C# – How to implement control validation in a Windows Forms application

ceventsvalidationwinforms

I am trying to better understand how validation works in a Windows Forms application. The internets are full of trivial examples, but I couldn't find a single non-trivial example explaining control validation. Anyway, thanks to SwDevMan81 and Hans Passant I am starting from a much better place than yesterday.

The "real application" has a dialog with many TextBox controls. Each of the controls implements the Validating event. As you can see in the example, ValidateChildren is called as a result of the Click event causing the Validating event to be sent to each of the controls. The app also uses an ErrorProvider control to give the user feedback. Yesterday, I did not understand how to use the Ok button Click event to perform this validation. Today, my dialog works as expected. Clicking the Ok button causes the ErrorProvider to do it's thing where a control is not valid and the dialog is not closing unexpectedly.

So while this seems to work, I am left feeling that that I "colored outside of the lines". Is there a "best practice" document/site for control validation in a Windows Forms application?

Of the many things that are still confusing me, I am unable to find an explanation for the behavior of my dialog when the Ok button DialogResult property is set to return DialogResult.OK. Why does setting this property interfere with validation? (Try my example with and without that line, to see what I mean.)

My problems from yesterday (it would appear) stem mostly from not understanding the ValidateChildren method and from my setting the Ok button DialogResult property to DialogResult.OK. Setting this property to DialogResult.None seems to change some automatic behavior of the Form class.

TIA

using System;
using System.ComponentModel;
using System.Windows.Forms;

namespace ConsoleApp
{
    class Program
    {
        static void Main( string[] args )
        {
            Dialog dialog = new Dialog();

            if( dialog.ShowDialog() == DialogResult.OK )
                Console.Beep();
        }
    }

    public class Dialog : Form
    {
        TextBox m_TextBox0;
        TextBox m_TextBox1; // not validated
        TextBox m_TextBox2;

        Button m_OkBtn;
        Button m_CancelBtn;

        ErrorProvider m_ErrorProvider;

        public Dialog()
        {
            m_TextBox0 = CreateTextBox( 0, "TextBox 0" );
            m_TextBox1 = CreateTextBox( 1, "TextBox 1" );
            m_TextBox2 = CreateTextBox( 2, "TextBox 2" );

            m_OkBtn     = CreateButton( 3, "Ok" );
            m_CancelBtn = CreateButton( 4, "Cancel" );

            m_ErrorProvider = new ErrorProvider( this );

            //m_BtnOk.DialogResult = DialogResult.OK;
            m_OkBtn.Click += new EventHandler( BtnOk_Click );
            m_OkBtn.CausesValidation = true;

            m_CancelBtn.DialogResult = DialogResult.Cancel;
            m_CancelBtn.CausesValidation = false;
        }

        void BtnOk_Click( object sender, EventArgs e )
        {
            if( ValidateChildren() )
            {
                DialogResult = DialogResult.OK;
                Close();
            }
        }

        void TextBox_Validating( object sender, CancelEventArgs e )
        {
            m_ErrorProvider.Clear();

            TextBox textBox = sender as TextBox;

            // m_TextBox1 is always valid, the others are valid if they have text.
            bool valid = textBox.TabIndex == 1 || textBox.Text.Length > 0;

            if( !valid )
                m_ErrorProvider.SetError( textBox, "Error " + textBox.Name );

            e.Cancel = !valid;
        }

        Button CreateButton( int index, string name )
        {
            Button button = new Button();

            button.TabIndex = index;
            button.Text = name;
            button.Location = new System.Drawing.Point( 0, index * 30 );

            Controls.Add( button );

            return button;
        }

        TextBox CreateTextBox( int index, string name )
        {
            Label label = new Label();
            label.Text = name;
            label.Location = new System.Drawing.Point( 0, index * 30 );

            TextBox textBox = new TextBox();

            textBox.TabIndex = index;
            textBox.CausesValidation = true;
            textBox.Validating += new CancelEventHandler( TextBox_Validating );
            textBox.Location = new System.Drawing.Point( 100, index * 30 );

            Controls.Add( label );
            Controls.Add( textBox );

            return textBox;
        }
    }
}

Edit: here is the final solution. I think it is easy to use while also fulfilling all the other requirements. I apologize in advance for how long this question ended up being. If I could show you all the real application, it would make more sense as to why this is so important. Anyway, thanks for helping this old dog learn a new trick.

The answer was to create one ErrorProvider for each control needing validation (vs. one ErrorProvider for the whole dialog. After that, it all was pretty simple.

using System;
using System.ComponentModel;
using System.Windows.Forms;

namespace ConsoleApp
{
    class Program
    {
        static void Main( string[] args )
        {
            Dialog dialog = new Dialog();

            if( dialog.ShowDialog() == DialogResult.OK )
                Console.Beep();
        }
    }

    public class CompositeControl
    {
        Label         m_Label;
        TextBox       m_TextBox;
        ErrorProvider m_ErrorProvider;

        Dialog m_Dialog;

        public CompositeControl( int index, string name, Dialog dialog )
        {
            m_Label = new Label();
            m_Label.Text = name;
            m_Label.Location = new System.Drawing.Point( 0, index * 30 );

            m_TextBox = new TextBox();

            m_TextBox.TabIndex = index;
            m_TextBox.CausesValidation = true;
            m_TextBox.Validating += new CancelEventHandler( TextBox_Validating );
            m_TextBox.Location = new System.Drawing.Point( 100, index * 30 );

            m_Dialog = dialog;

            m_ErrorProvider = new ErrorProvider( m_Dialog );

            m_Dialog.Controls.Add( m_Label );
            m_Dialog.Controls.Add( m_TextBox );
        }

        void TextBox_Validating( object sender, CancelEventArgs e )
        {
            TextBox textBox = sender as TextBox;

            if( !m_Dialog.IsClosing && textBox.Text.Length == 0 )
                return;

            // m_TextBox1 is always valid, the others are valid if they have text.
            bool valid = textBox.TabIndex == 1 || textBox.Text.Length > 0;

            if( !valid )
                m_ErrorProvider.SetError( textBox, "Error " + textBox.Name );
            else
                m_ErrorProvider.Clear();

            e.Cancel = !valid;
        }
    }

    public class Dialog : Form
    {
        CompositeControl m_CompositeControl0;
        CompositeControl m_CompositeControl1; // not validated
        CompositeControl m_CompositeControl2;

        Button m_OkBtn;
        Button m_CancelBtn;

        bool m_IsClosing = false;

        public Dialog()
        {
            m_CompositeControl0 = new CompositeControl( 0, "TextBox 0", this );
            m_CompositeControl1 = new CompositeControl( 1, "TextBox 1", this );
            m_CompositeControl2 = new CompositeControl( 2, "TextBox 2", this );

            m_OkBtn     = CreateButton( 3, "Ok" );
            m_CancelBtn = CreateButton( 4, "Cancel" );

            //m_BtnOk.DialogResult = DialogResult.OK;
            m_OkBtn.Click += new EventHandler( BtnOk_Click );
            m_OkBtn.CausesValidation = true;

            m_CancelBtn.DialogResult = DialogResult.Cancel;
            m_CancelBtn.CausesValidation = false;
        }

        void BtnOk_Click( object sender, EventArgs e )
        {
            m_IsClosing = true;

            if( ValidateChildren() )
            {
                DialogResult = DialogResult.OK;
                Close();
            }

            m_IsClosing = false;
        }

        Button CreateButton( int index, string name )
        {
            Button button = new Button();

            button.TabIndex = index;
            button.Text = name;
            button.Location = new System.Drawing.Point( 0, index * 30 );

            Controls.Add( button );

            return button;
        }

        public bool IsClosing { get { return m_IsClosing; } }
    }
}

This question is a follow up to one that I asked yesterday.

Best Answer

Assigning the DialogResult property is what makes a dialog close. It keeps running while it is set to None. You don't need the Close() call. The code that calls ShowDialog() gets the DialogResult value you assigned as the return value. So it knows whether the dialog was closed with OK or just canceled.

Also note that the way you wrote the validation event handler, you don't need ValidateChildren(). You set e.Cancel = true to prevent the user from moving away from the text box. Which means that she can only get to the OK button when the text box was validated to be okay. You do however have to make sure that a control that has validation is selected first when the dialog is shown.

A friendly dialog is one where the user is free to tab between controls and pick off the 'easy ones'. You now need two validations, one that checks if the entered value is valid, another one that checks if there are no missing values. You get this by accepting an empty string in the Validation event handler. But the latter is not well supported by Winforms, you need code.