Refracted Paladin
Refracted Paladin

Reputation: 12216

WinForm UI Validation

I need to implement input validation throughout my winform app. There are many different forms where data can be entered and I would like to not go control by control by form and create isValid etc per item. How have others dealt with this?

I see that most related posts deal with Web Apps and/or mention Enterprise Library Validation Application Block. Now I admit I haven't thoroughly researched ELVAB but it seems like overkill for what I need. My current thought is to write a class library with the various requirements and pass it a control as a parameter. I already have a Library of RegEx functions for things like isValidZipCode and such so that may be a place for me to start.

What I would like to have is a Validate button that onClick cycles through all the controls on that Form Page and performs the needed validation. How can I accomplish this?

Upvotes: 45

Views: 67160

Answers (11)

Bruce
Bruce

Reputation: 541

I realize this thread is pretty old but I thought I'd post the solution I came up with.

The biggest problem with validation on WinForms is the validation is only executed when the control has "lost focus". So the user has to actually click inside a text box then click somewhere else for the validation routine to execute. This is fine if your only concerned about the data that is entered being correct. But this doesn't work well if you're trying to make sure a user didn't leave a textbox empty by skipping over it.

In my solution, when the user clicks the submit button for a form, I check each control on the form (or whatever container is specified) and use reflection to determine if a validating method is defined for the control. If it is, the validation method is executed. If any of the validations fail, the routine returns a failure and allows the process to stop. This solution works well especially if you have several forms to validate.

1) Just copy and paste this section of code to your project. We're using Reflection so you need to add System.Reflection to your using statements

class Validation
{
    public static bool hasValidationErrors(System.Windows.Forms.Control.ControlCollection controls)
    {
        bool hasError = false;

        // Now we need to loop through the controls and deterime if any of them have errors
        foreach (Control control in controls)
        {
            // check the control and see what it returns
            bool validControl = IsValid(control);
            // If it's not valid then set the flag and keep going.  We want to get through all
            // the validators so they will display on the screen if errorProviders were used.
            if (!validControl)
                hasError = true;

            // If its a container control then it may have children that need to be checked
            if (control.HasChildren)
            {
                if (hasValidationErrors(control.Controls))
                    hasError = true;
            }
        }
        return hasError;
    }

    // Here, let's determine if the control has a validating method attached to it
    // and if it does, let's execute it and return the result
    private static bool IsValid(object eventSource)
    {
        string name = "EventValidating";

        Type targetType = eventSource.GetType();

        do
        {
            FieldInfo[] fields = targetType.GetFields(
                 BindingFlags.Static |
                 BindingFlags.Instance |
                 BindingFlags.NonPublic);

            foreach (FieldInfo field in fields)
            {
                if (field.Name == name)
                {
                    EventHandlerList eventHandlers = ((EventHandlerList)(eventSource.GetType().GetProperty("Events",
                        (BindingFlags.FlattenHierarchy |
                        (BindingFlags.NonPublic | BindingFlags.Instance))).GetValue(eventSource, null)));

                    Delegate d = eventHandlers[field.GetValue(eventSource)];

                    if ((!(d == null)))
                    {
                        Delegate[] subscribers = d.GetInvocationList();

                        // ok we found the validation event,  let's get the event method and call it
                        foreach (Delegate d1 in subscribers)
                        {
                            // create the parameters
                            object sender = eventSource;
                            CancelEventArgs eventArgs = new CancelEventArgs();
                            eventArgs.Cancel = false;
                            object[] parameters = new object[2];
                            parameters[0] = sender;
                            parameters[1] = eventArgs;
                            // call the method
                            d1.DynamicInvoke(parameters);
                            // if the validation failed we need to return that failure
                            if (eventArgs.Cancel)
                                return false;
                            else
                                return true;
                        }
                    }
                }
            }

            targetType = targetType.BaseType;

        } while (targetType != null);

        return true;
    }

}

2) Use the standard Validating event on any control you want to validate. Be Sure to use e.Cancel when the validation fails!

private void txtLastName_Validating(object sender, CancelEventArgs e)
    {
        if (txtLastName.Text.Trim() == String.Empty)
        {
            errorProvider1.SetError(txtLastName, "Last Name is Required");
            e.Cancel = true;
        }
        else
            errorProvider1.SetError(txtLastName, "");
    }

3) Don't skip this step! Set the AutoValidate property on the form to EnableAllowFocusChange. This will allow tabbing to another control even when the validation fails.

4) Finally, in your Submit Button method, call the Validation method and specify what container you want to check. It can be the whole form, or just a container on the form like a Panel or a Group.

private void btnSubmit_Click(object sender, EventArgs e)
    {
        // the controls collection can be the whole form or just a panel or group
        if (Validation.hasValidationErrors(frmMain.Controls))
            return;

        // if we get here the validation passed
        this.close();
    }

Happy Coding!

Upvotes: 43

Rey
Rey

Reputation: 29

If you combine the ideas above with this a generic Validating event handler you will get a good validation error "framework" with all validation methods in your business classes. I just extended Bruce code with Danish idea. The was done for Entity Framework and Dev Express components, but those dependencies can be easily removed. Enjoy!

public class ValidationManager
{
    /// <summary>
    /// Call this method to validate all controls of the given control list 
    /// Validating event will be called on each one
    /// </summary>
    /// <param name="controls"></param>
    /// <returns></returns>
    public static bool HasValidationErrors(System.Windows.Forms.Control.ControlCollection controls)
    {
        bool hasError = false;

        // Now we need to loop through the controls and deterime if any of them have errors
        foreach (Control control in controls)
        {
            // check the control and see what it returns
            bool validControl = IsValid(control);
            // If it's not valid then set the flag and keep going.  We want to get through all
            // the validators so they will display on the screen if errorProviders were used.
            if (!validControl)
                hasError = true;

            // If its a container control then it may have children that need to be checked
            if (control.HasChildren)
            {
                if (HasValidationErrors(control.Controls))
                    hasError = true;
            }
        }
        return hasError;
    }

    /// <summary>
    /// Attach all youe Validating events to this event handler (if the controls requieres validation)
    /// A method with name Validate + PropertyName will be searched on the binded business entity, and if found called
    /// Throw an exception with the desired message if a validation error is detected in your method logic
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    public static void ValidationHandler(object sender, CancelEventArgs e)
    {
        BaseEdit control = sender as BaseEdit;

        if (control.DataBindings.Count > 0) //control is binded
        {
            string bindedFieldName = control.DataBindings[0].BindingMemberInfo.BindingField;

            object bindedObject = control.BindingManager.Current;

            if (bindedObject != null) //control is binded to an object instance
            {
                //find and call method with name = Validate + PropertyName

                MethodInfo validationMethod = (from method in bindedObject.GetType().GetMethods()
                                               where method.IsPublic &&
                                                     method.Name == String.Format("Validate{0}",bindedFieldName) &&
                                                     method.GetParameters().Count() == 0
                                               select method).FirstOrDefault();

                if (validationMethod != null) //has validation method
                {
                    try
                    {
                        validationMethod.Invoke(bindedObject, null);

                        control.ErrorText = String.Empty; //property value is valid
                    }
                    catch (Exception exp)
                    {
                        control.ErrorText = exp.InnerException.Message;
                        e.Cancel = true;
                    }
                }
            }
        }
    }

    // Here, let's determine if the control has a validating method attached to it
    // and if it does, let's execute it and return the result
    private static bool IsValid(object eventSource)
    {
        string name = "EventValidating";

        Type targetType = eventSource.GetType();

        do
        {
            FieldInfo[] fields = targetType.GetFields(
                 BindingFlags.Static |
                 BindingFlags.Instance |
                 BindingFlags.NonPublic);

            foreach (FieldInfo field in fields)
            {
                if (field.Name == name)
                {
                    EventHandlerList eventHandlers = ((EventHandlerList)(eventSource.GetType().GetProperty("Events",
                        (BindingFlags.FlattenHierarchy |
                        (BindingFlags.NonPublic | BindingFlags.Instance))).GetValue(eventSource, null)));

                    Delegate d = eventHandlers[field.GetValue(eventSource)];

                    if ((!(d == null)))
                    {
                        Delegate[] subscribers = d.GetInvocationList();

                        // ok we found the validation event,  let's get the event method and call it
                        foreach (Delegate d1 in subscribers)
                        {
                            // create the parameters
                            object sender = eventSource;
                            CancelEventArgs eventArgs = new CancelEventArgs();
                            eventArgs.Cancel = false;
                            object[] parameters = new object[2];
                            parameters[0] = sender;
                            parameters[1] = eventArgs;
                            // call the method
                            d1.DynamicInvoke(parameters);
                            // if the validation failed we need to return that failure
                            if (eventArgs.Cancel)
                                return false;
                            else
                                return true;
                        }
                    }
                }
            }

            targetType = targetType.BaseType;

        } while (targetType != null);

        return true;
    }

}

Sample validation method:

partial class ClientName
{
    public void ValidateFirstName()
    {
        if (String.IsNullOrWhiteSpace(this.FirstName))
            throw new Exception("First Name is required.");
    }

    public void ValidateLastName()
    {
        if (String.IsNullOrWhiteSpace(this.LastName))
            throw new Exception("Last Name is required.");
    }
}

Upvotes: 2

Matt Brunell
Matt Brunell

Reputation: 10389

Validation is already built into the WinForms library.

Each Control-derived object has two events named Validating and Validated. Also it has a property called CausesValidation. When this is set to true (it is true by default) then the control participates in validation. Otherwise, it does not.

Validation occurs as part of focus. When you focus off of a control, its validation events are fired. In fact the focus events are fired in a specific order. From MSDN:

When you change the focus by using the keyboard (TAB, SHIFT+TAB, and so on), by calling the Select or SelectNextControl methods, or by setting the ContainerControl..::.ActiveControl property to the current form, focus events occur in the following order:

  1. Enter
  2. GotFocus
  3. Leave
  4. Validating
  5. Validated
  6. LostFocus

When you change the focus by using the mouse or by calling the Focus method, focus events occur in the following order:

  1. Enter
  2. GotFocus
  3. LostFocus
  4. Leave
  5. Validating
  6. Validated

If the CausesValidation property is set to false, the Validating and Validated events are suppressed.

If the Cancel property of the CancelEventArgs is set to true in the Validating event delegate, all events that would usually occur after the Validating event are suppressed.

Also a ContainerControl has a method called ValidateChildren() which will loop through contained controls, and validate them.

Upvotes: 68

danish
danish

Reputation: 5600

Either that way. Or you can have a single validating event associated with all or controls which need similar validations. This will remove the looping from the code. Say you have four textboxes which can have integer only. What you can do is have a single event for each of them. I am not having any IDE so code below is the best I can come up with.

this.textbox1.Validated += <ValidatedEvent>
this.textbox2.Validated += <ValidatedEvent>
this.textbox3.Validated += <ValidatedEvent>
this.textbox4.Validated += <ValidatedEvent>

In the event:

  1. Cast sender as textbox.
  2. Check if the value in the textbox is numeric.

And so forth you have events lined up.

Hope this helps.

Upvotes: 2

Steven Evers
Steven Evers

Reputation: 17196

In all of my forms, I implement the isValidating event for the particular control in question and if the data doesn't validate I have an errorProvider on the form and I use its SetError(...) method to set the error to the control in question with relevant information as to why it's wrong.

edit> I should note that I generally use the mvc pattern when doing this, so the specific validation for that control/member of the model happens at the model, so the isValidating looks kinda like this:

private uicontrol_isValidating(...)
{
    if(!m_Model.MemberNameIsValid())
    {
        errorProvider.SetError(...);
    }
}

Upvotes: 2

danish
danish

Reputation: 5600

Why are you not using Validating event? You can have a single validating event and validate the controls there. There will be no need of using loops and each control will be validated as the data is entered.

Upvotes: 1

Will Eddins
Will Eddins

Reputation: 13907

Just a rough idea:


void btnValidate_Click(object sender, EventArgs e)
{
  foreach( Control c in this.Controls )
  {
    if( c is TextBox )
    {
      TextBox tbToValidate = (TextBox)c;
      Validate(tbToValidate.Text);
    }
  }
}

You could stick the textboxes inside a panel and only loop through controls in there if you want to avoid looping through other controls.

Upvotes: 1

RS Conley
RS Conley

Reputation: 7196

In my own application I need to validate dimensions as they are typed in. The sequence I used is as follows

  1. The user selects or types then moves away from the control.
  2. The control loses focus and notifies the View sending it's ID and the entry text.
  3. The View checks what Shape Program (a class implementing a interface) created the Form and passes it the ID and entry text
  4. The Shape Program returns a response.
  5. If the Response is OK the View updates correct Entry of the Shape Class.
  6. If the Response is OK the View tells the Form through a Interface that it is OK to shift the focus to the next entry.
  7. If the Response is not OK, the View looks at the response and using the Form Interface tells the form what to do. This usually means the focus shifts back to the offending entry with a message displayed telling the user what happened.

The advantage of this approach that validation is centralized in one location for a given Shape Program. I don't have to go modify each control or even really worry about the different types of controls on the form. Way back when I designed the software I decided how the UI going to work for textboxes, listboxes, combo boxes, etc. Also different levels of severity is handled differently.

The View takes care of that instructing the Form what to do through the Interface. How it actually is implemented is handled by the Form itself in it's implementation of the Interface. The View doesn't care if the Form is displaying yellow for warning and red for error. Only that it handles those two levels. Later if a better idea of displaying warning vs errors comes along I can make the change in the Form itself rather mucking around with the View logic or the validate in Shape Program.

You are already halfway there if you are considering making a class to hold your validation logic this will get you the rest of the way in your new design.

Upvotes: 9

Kenneth Cochran
Kenneth Cochran

Reputation: 12064

Cycling through controls can work but it's error prone. I worked on a project that used that technique (granted it was a Delphi project not C#) and it did work as expected but it was very difficult to update if a control was added or changed. This may have been correctible. I'm not sure.

Anyway it worked by creating a single event handler which was then attached to each control. The handler would then use RTTI to determine the type of the control. Then it would use the control's name property in a large select statement to find the validation code to run. If the validation failed, an error message was sent to the user and the control was given focus. To make things more complex, the form was divided into several tabs and the proper tab had to be visible for it's child control to get the focus.

So that's my experience.

I would much rather use a Passive View design pattern to remove all business rules from the form and push them into a Presenter class. Depending on the state of your form that may be more work than your willing to invest.

Upvotes: 1

Joel Coehoorn
Joel Coehoorn

Reputation: 415690

I would like to not have to go control by control by form and create isValid etc per item.

As some level you will have to define what it means to be valid for each control, unless all you care about is that the control has a value of some kind.

That said, there's an ErrorProvider component you can use that works pretty well.

Upvotes: 4

Jamie Ide
Jamie Ide

Reputation: 49261

We've had good luck with the Noogen ValidationProvider. It's simple for simple cases (data type checks and required fields) and easy to add custom validation for more complex cases.

Upvotes: 3

Related Questions