Click here to Skip to main content
15,881,172 members
Articles / Desktop Programming / Windows Forms

XML Based WinForms Validation Toolkit

Rate me:
Please Sign up or sign in to vote.
4.78/5 (19 votes)
10 Sep 2009CPOL6 min read 48.9K   1.3K   58   16
A toolkit for validating input based on XML.

XmlBasedValidation_demo.jpg

Introduction

One of the important aspects of programming is "input validation". Generally, two requirements exist in validation: a proper way to validate, and a user friendly way to display error messages. WinForms has a beautiful component named "ErrorProvider" for the second problem.

To validate user input, I think the worst way is writing if statements for each control. The more UI components you have, the more code you need. Also, complex validations cannot be done by simple statements.

There exists nice frameworks:

  • The Spring.Net application framework that can validate both WinForms and web pages based on XML rules. Although I professionally use it in many projects, it is too complex for small applications, and does not use the ErrorProvider component; you have to show messages manually.
  • The Validation Application Block from Enterprise Library. I have no experience with the Enterprise Library.

So, I decided to write my own XML rule based validation toolkit, using the nice XML syntax of the Spring validation framework. I tried to provide a solution to the WinForms validation problem with:

  • no if statements,
  • easy management of validation rules,
  • automatic use of the ErrorProvider component.

The main goal of this article and toolkit is to show developers that there are far elegant ways to validate user input. The Solution is in VS 2008 format, but the projects are .NET 2.0.

The Problem

Take the demo form as an example (see the screenshot from the demo program). We have three textboxes and a combobox. Validation rules: first three textboxes are required. We need to check if they are filled in the correct format. We also want to check the int range. The fourth textbox is conditional; if user checks the combobox, we need to validate it as a proper email.

Here is the stupid repetitive validation code:

C#
public bool ValidateForm()
{
    bool valid = true;
    eProvider.Clear();
    
    if(String.IsNullOrEmpty(txtRequired.Text))
    {
        valid = false;
        eProvider.SetError(txtRequired, 
           "this field cannot be empty.");
    }
    
    int i;
    if(String.IsNullOrEmpty(txtInt.Text))
    {
        valid = false;
        eProvider.SetError(txtInt, "this field cannot be empty.");
    }
    else if( !(Integer.TryParse(txtInt.Text, out i) &&  
                   i < 35 && i > 5) )
    {
        valid = false;
        eProvider.SetError(txtInt, "Allowed values are : 5 - 35");
    }

    // and continue to repeat code for other two field!
}

The Solution

I think now the problem is clear and everyone agrees how inefficient writing code per control is. This toolkit lets you define rules in XML, and it automatically validates the form using that XML.

Step by Step Tutorial

  1. Add a reference to Validation.dll.
  2. Define a rule XML for each form or a group of common controls. The currently supported types of validators are (TypeValidators):

    • required: Checks if the target value is null or an empty string.
    • rangeInt: Checks if the target value is in the interval; max and min values are optional. Defaults are: Int32.Max and Int32.Min.
    • rangeDouble: Checks if the target value is in the interval; max and min values are optional. Defaults are: Double.Max and Double.Min.
    • regExp: Checks if the target value matches the expression. The "exp" attribute is required.

    For all type validators, the "target" and "errorMessage" attributes are required, and the "when" attribute is optional. You can use the following wildcards in errorMessage: {v:value}, {v:min}, {v:max}.

    Here is the example rule.xml used in the demo project:

    XML
    <rules>
        <required target="txtRequired.Text" errorMessage="this field cannot be empty."/>
        <rangeInt target="txtInt.Text" minValue="0" maxValue="70" 
            errorMessage="Value {v:value} is not in the range : {v:min} - {v:max}"/>
        <rangeDouble target="txtDouble.Text" minValue="30" 
           maxValue="30.88" errorMessage="incorrect range"/>
        <regExp target="txtEmail.Text" exp="\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*" 
            errorMessage="incorrect email. check format" 
            when="cbEmailCondition.Checked = true"/>
    </rules>
  3. Set the build action of the XML to "embedded resource" (right click and go to Properties).
  4. Use Validation.ValidatedForm as a base for your form instead of System.Windows.Forms. In the constructor, create a "validator object" (inherited from ValidatedForm) and add a rule to it. Be careful about addressing! When you run the program, the toolkit will validate your XML using the schema. If it fails, you'll get an exception (check the console for XML errors).
  5. C#
    public partial class Form1 : Validation.ValidatedForm
    {
        public Form1()
        {
            InitializeComponent();
            validator = new Validator(this);
            validator.AddRule("TestApp.Rules.Form1.xml");
        }
    
        private void button1_Click(object sender, EventArgs e)
        {
            bool result = validator.Validate();
        }
    }
  6. Whenever you want to validate, just call the validator.Validate() function.

As simple as that! No matter how many input controls you have, you just need to code these lines. Also, to create a rule, you need to add just one line into the XML, instead of writing code. The toolkit will validate the form according to the rules in the given rule XML(s) and return a boolean validation result. In the case of an error, the toolkit will show the error using an ErrorProvider, inherited from ValidatedForm.

Inside the Toolkit

Since this article is not a tutorial about Reflection or XML parsing, I won't go into the details (because I don't know them in detail..). Here is the file structure:

  • Core
    • Types
      • ITypeValidator.cs
      • RangeDouble.cs
      • RangeInt.cs
      • RegExp.cs
      • Required.cs
    • Validator.cs
    • XmlToolkit.cs
  • Eval
    • Types
      • ITypeEvaluator.cs
      • SimpleFieldEvaluator.cs
    • Evaluator.cs
  • rules.xsd
  • ValidatedForm.cs

The classes for the rule validation are in the core directory. Type specific code is in the Core.Types directory.

Validator checks the XML structure using XmlToolkit (and rules.xsd), and parses and validates all the rules. The interesting parts are:

C#
public Validator(ValidatedForm form)
{
    this.form = form;
    rules = new List<string>();
    typeValidators = new Dictionary<string,>();

    //dynamically load all validators
    Type[] types = Assembly.GetExecutingAssembly().GetTypes();
    foreach (Type t in types)
    {
        // If object implemented ITypeValidator Interface
        if (t.GetInterface("ITypeValidator", true) != null)
        {
            // use classname as index
            typeValidators.Add(t.Name.ToLower(),
                (Types.ITypeValidator)Activator.CreateInstance(null, 
                                      t.FullName).Unwrap());
        }
    }

    eval = new Eval.Evalautor(form);
}

private object GetFormField(string name)
{
    return form.GetType().GetField(name, System.Reflection.BindingFlags.Instance 
        | System.Reflection.BindingFlags.NonPublic 
        | System.Reflection.BindingFlags.Public).GetValue(form);
}

private object GetControlProperty(object control, string name)
{
    return control.GetType().GetProperty(name, System.Reflection.BindingFlags.Instance 
        | System.Reflection.BindingFlags.NonPublic 
        | System.Reflection.BindingFlags.Public).GetValue(control, null);
}

In the constructor, first, we load all the available type validators dynamically, using Reflection. Binding from plain text to real objects is also done via Reflection. So, if you want to get the value of, let's say, txtName.Text, first call GetFormField("txtName") and get the txtName control. Then, call GetControlProperty(control, "Text").

The most important function, CheckRules(), takes an XML path. It parses each element in the XML and calls the proper type validator. The element names and type validators are matched.

C#
private void CheckRules(string ruleXml)
{
    //load Rule Xml
    Stream fileStream = 
      Assembly.GetEntryAssembly().GetManifestResourceStream(ruleXml);
    XmlDocument ruleSet = new XmlDocument();
    ruleSet.Load(fileStream);

    XmlNode rules = ruleSet.SelectSingleNode("rules");

    //get ErrorProvider
    ErrorProvider eProvider = GetFormField("eProvider") as ErrorProvider;

    //parse and check Rules
    foreach (XmlNode node in rules.ChildNodes)
    {
        if (node.NodeType == XmlNodeType.Comment)
        {
            continue;
        }

        //evaluate when condition first, if any
        if (node.Attributes["when"] != null && 
            !eval.Evaluate(node.Attributes["when"].Value))
        {
            continue;
        }

        string[] parts = node.Attributes["target"].Value.Split('.');

        Control control = GetFormField(parts[0]) as Control;
        object value = GetControlProperty(control, parts[1]);

        // go deeper, if any ..
        for (int i = 2; i < parts.Length; i++)
        {
            value = GetControlProperty(value, parts[i]);
        }
        
        //find appropriate validator, by node name
        string msg = typeValidators[node.Name.ToLower()].Validate(value, node);

        if (!String.IsNullOrEmpty(msg))
        {
            eProvider.SetError(control, msg);
            result = false;
        }
        else
        {
            eProvider.SetError(control, "");
        }
    }
}

A similar approach is used in evaluators. The evaluator used in expression evaluation in "when" conditions. Currently, only simple field expressions are supported. The expression format is: <Field> <operand> <value>.

  • Field: Like "target", this must be a field of any input. E.g.: txtName.Text, cbEmail.Checked.
  • Operand: The only supported operands are: <, >, =!, =.
  • Value: Can be string, boolean, int, or double. E.g.: true, false, 3.0, 5, asdf.

Points of Interest

Dynamic programming! As you can see, I have tried to avoid any hardcoded values. Check the constructor of the Validator class. It loads all type validators dynamically. So, add a new type validator: just code DateTimeValidator.cs by inheriting ITypeValidator and put it into Core.Types. Edit Rules.xsd for your specific format, add attributes like "beginDate" and/or "endDate". That's it! Selection is based on node name == class name. Unfortunately, for evaluation, this is not working, because it needs complex type selection. I'm still working on it.

The XML and schema is also very important for this toolkit. The schema must be prepared very carefully; otherwise at runtime, you'll get exceptions. The toolkit validates every rule.xml in the Validator.AddRule() function.

Final Words

Please comment! Be cruel. There are some parts I'm not proud of :) I optimized the article for 786px width. You should not see any horizontal scrollbar in any core area. I hate horizontal scrollbars. I love Reflection.

TODO

  • Write regexp for "when" attribute in Rules.xsd.
  • More validator types.
  • Not so secure.. need some try-catch'es!

History

  • 09 September, 2009: Slightly updated article. Added support for multi-level properties like: lstCars.SelectedItem.Name.Length.
  • 01 June, 2009: Release of the article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Sweden Sweden
I have a bachelors degree in computer engineering and currently studying a boring computer science master programme. I used to like coding, but now it started to get repetitive Smile | :)

Comments and Discussions

 
GeneralMy vote of 5 Pin
Member 14670210-Oct-10 16:31
Member 14670210-Oct-10 16:31 
QuestionValidation only if field is NOT null Pin
syednj7-Apr-10 12:12
syednj7-Apr-10 12:12 
AnswerRe: Validation only if field is NOT null Pin
syednj8-Apr-10 9:55
syednj8-Apr-10 9:55 
QuestionRe: Validation only if field is NOT null Pin
armitage036-Jun-12 23:00
armitage036-Jun-12 23:00 
GeneralTwo Controls Pin
Doncp11-Sep-09 7:28
Doncp11-Sep-09 7:28 
GeneralRe: Two Controls Pin
Gokhan B.13-Sep-09 10:27
Gokhan B.13-Sep-09 10:27 
GeneralSupport for conditional logic Pin
ubuntumax1-Sep-09 1:22
ubuntumax1-Sep-09 1:22 
GeneralRe: Support for conditional logic Pin
Gokhan B.8-Sep-09 12:52
Gokhan B.8-Sep-09 12:52 
GeneralVery nice, but.... Pin
kottan197017-Jun-09 4:38
kottan197017-Jun-09 4:38 
GeneralRe: Very nice, but.... Pin
Gokhan B.19-Jun-09 8:05
Gokhan B.19-Jun-09 8:05 
Generalvery good Pin
Donsw14-Jun-09 12:16
Donsw14-Jun-09 12:16 
GeneralExcellent! [modified] Pin
TheCardinal9-Jun-09 18:10
TheCardinal9-Jun-09 18:10 
GeneralRe: Excellent! Pin
Gokhan B.13-Jun-09 9:53
Gokhan B.13-Jun-09 9:53 
GeneralRe: Excellent! Pin
TheCardinal14-Jun-09 21:58
TheCardinal14-Jun-09 21:58 
GeneralNice article Pin
Tarabanko Yury2-Jun-09 2:24
Tarabanko Yury2-Jun-09 2:24 
GeneralRe: Nice article Pin
Gokhan B.2-Jun-09 9:33
Gokhan B.2-Jun-09 9:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.