Saturday, February 7, 2009

Custom Validation Step 4: Automated Validation Controls

In my previous posts we covered the creation of a ValidationError class that we can use as a validation error container (Custom Validation Step 1: The Validation Error Class), we showed how to implement a  List<ValidationError> to contain errors on a business object (Custom Validation Step 2: Business Object Validation), and then we showed an ASP.Net page implementation that demonstrated how page level validation and business object validation can be easily integrated when they both use the List<ValidationError> mechanism (Custom Validation Step 3: Simple Page Validation).  Now we’re going to take a look at how create a ValidationSummary control and a ValidationLabel control that will give us some automated error handling behavior.  This really is a continuation of the previous 3 posts.  If you haven’t read them, you may want to

Start at the End

We’ll start by looking at where we want to end up.  First, we want our pages to have a PageErrors member that contains a List<ValidationError> member that will contain all page level and business object level validation errors.  Then we want to be able to place ValidationSummary and ValidationLabel controls on our pages that will automatically generate an error summary and an indicator for which data is invalid whenever PageErrors contains errors.  The end result will look like this.

image

I also think it’s very important at this point to think about what we want our code to look like when using these controls.  The error handling code should be simple. We call page level validation, we call object level validation, we then add any errors to the PageErrors.  It should look something like this:

// Run page and entity level validation

this.PageErrors.AddList(ValidatePage());

this.PageErrors.AddList(person.Validate());

 

// If any errors were found bail out and let automated validation

// controls show the errors.

if (this.PageErrors.Errors.Count != 0) { return; }

Using the validation controls should be even simpler.  We want to be able to place tags anywhere in the markup, set some properties, write no code, and have them just work. The only thing we should have to do is give the ValidationLabel controls the name of the field that they are supposed to be validating. This name will use the pseudo fully qualified naming convention that we’ve been using throughout these posts.  This FieldName is how a label will be tied to validation errors for a specific field. Markup should look like:

<go:ValidationSummary ID="valSummary" runat="server" ValidationMode="Auto" BoxWidth="600px" />
&nbsp;
<table>
  <tr>
    <td class="formLbl">
      <go:ValidationLabel ID="vlblName" runat="server"  FieldName="Person.Name">Name:</go:ValidationLabel>
    </td>
    <td class="formTd">
      <asp:TextBox ID="txtName" runat="server" CssClass="formTextBox" />
    </td>
  </tr>
  <tr>
    <td class="formLbl">
      <go:ValidationLabel ID="vlblEmail" runat="server" FieldName="Person.Email">Email:</go:ValidationLabel>
    </td>
    <td class="formTd">
      <asp:TextBox ID="txtEmail" runat="server" CssClass="formTextBox" />
    </td>
  </tr>
  <tr>

 

The WebValidationControls Project

We’re going to put all of the automated web validation controls in a separate project that can be easily included by and referenced by any web application.

image

The project contains our ValidationLabel and ValidationSummary classes, a ValidationBox class that will serve as a container for all of our validation wire up code on a page, an IValidationContainer interface that a page must implement to indicate that it has the members required to behave as a valid validation container, and we have the PageErrorList class which we created in the previous post. The most important class is the ValidationBox.  It is the glue that holds everything else together. It contains our PageErrors (of type PageErrorList), it contains lists of all ValidationLabel and ValidationSummary controls on the page, and it encapsulates our logic for doing things like binding error lists to ValidaitonSummary controls and setting ErrorFlag and color on ValidationLabel controls.

image

The IValidationContainer Interface

The importance of interfaces to modern object oriented design just can’t be overemphasized.  They allow an object to tell us about all the different behaviors that it can support.  In our case, before we start doing things like registering our validation controls with the page, we need to make sure that the page contains a ValidationBox.  We’re encapsulating all of the things we need the page to do within this ValidationBox class.  That makes it really easy for us to tell if a page is a valid validation contiainer, it just needs to contain a ValdiationBox.  That’s the purpose of the IValidaitonContainer interface.  It requires that any page that implements it have a ValidationBox property that contains an instance of our ValidationBox class.

public interface IValidationContainer
{
    // ValidationBox
    ValidationBox ValidationBox { get; }
}

The ValidationLabel Class

ValidationLabel acts as the label for a field, and if there is a validation error for the value entered in that field, it changes color to flag the error.  To do this we just extend the existing Label class and add some functionality.  We want our control to have

  • A FieldName property that allows us to map a label to a specific fully qualified FieldName (remember FieldName is used by our ValidationError class to identify which data member produced an error),
  • An ErrorColor which is the color used for render if there is an error
  • A NormalColor which is the default render color when there is no error
  • An IsError flag which tells the control to render using the ErrorColor instead of the NormalColor
  • And we want the control to register itself with the page. Register just means it adds itself to a generic List<ValidationLabel> member kept in the page’s ValidationBox.

The implementation is listed below.  Notice that the control has properties that let it define what the ErrorColor and NormalColor are, but nowhere does it actually use these colors.  It just checks to see if its containing page is an IValidationContainer.  If it is, the control registers with the page and let’s the page’s ValidationBox decide which color to use.  Also, you’ll see that we set default values in the onInit method, but we check for existing values just in case they were already set in the markup.

public class ValidationLabel : Label
    {
        // FieldName
        public string FieldName { get; set; }
        // IsError
        private bool _isError;
        public bool IsError { get { return _isError; } }
        // Mode
        public Mode ValidationMode { get; set; }
        // ErrorColor
        public System.Drawing.Color ErrorColor { get; set; }
        // NormalColor
        public System.Drawing.Color NormalColor { get; set; }
 
        // Local Enums
        public enum Mode { Null, Auto, Manual }
 
        // OnInit
        // We want to set the initial error state of the 
        // label and register it with the page.
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            IValidationContainer page = this.Page as IValidationContainer;
            if (page != null) { page.ValidationBox.RegisterValidationLabel(this); }
            // set defaults colors
            this.ErrorColor = this.ErrorColor.IsEmpty ? System.Drawing.Color.Red : this.ErrorColor;
            this.NormalColor = this.NormalColor.IsEmpty ? System.Drawing.Color.Black : this.NormalColor;
            if (this.ValidationMode != Mode.Manual) { this.ValidationMode = Mode.Auto; }
            // always start assuming no error
            this.ClearError();
        }
 
        // SetError
        public void SetError()
        {
            _isError = true;
            this.ForeColor = this.ErrorColor;
        }
 
        // ClearError
        public void ClearError()
        {
            _isError = false;
            this.ForeColor = this.NormalColor;
        }
    }

The ValidationSummary Class

The ValidationSummary class gives us the red box that displays on our page and shows a summary of the error messages.  It contains a number of properties that allow us to control the look and feel of the box, things like BoxWidth, BoxTitle, BoxMessage, ErrorColor, and ErrorBullet.  It also contains an ErrorList member that is a List<ValidationError>.  The way the summary works is we add any errors to the ErrorList, then if there are any items in the ErrorList at render time, the summary uses the look and feel properties to render an error summary box. That’s an important point. The ValidationSummary implements it’s own custom render logic.  All we need to do is bind a list of ValidationErrors to it and it will handle the rest.  If ErrorList.Count > 0 then the control will render an error box.  If ErrorList.Count<1 then the control won’t even render.

public class ValidationSummary : WebControl

    {

        // Errors

        public PageErrorList ErrorList { get; set; }

        // BoxTitle

        public string BoxTitle { get; set; }

        // BoxMessage

        public string BoxMessage { get; set; }

        // Width

        public string BoxWidth { get; set; }

        // Mode

        public Mode ValidationMode { get; set; }

        // ErrorColor

        public System.Drawing.Color ErrorColor { get; set; }

        // ErrorBullet

        public string ErrorBullet { get; set; }

 

        // Local Enums

        public enum Mode{Null, Auto, Manual}

 

        // OnInit

        // We want to set the initial error state of the

        // label and register it with the page.

        protected override void OnInit(EventArgs e)

        {

            base.OnInit(e);

            IValidationContainer page = this.Page as IValidationContainer;

            if (page != null) { page.ValidationBox.RegisterValidationSummary(this); }

            // set defaults

            this.ErrorColor = this.ErrorColor.IsEmpty ? System.Drawing.Color.Red : this.ErrorColor;

            this.ErrorBullet = String.IsNullOrEmpty(this.ErrorBullet) ? "- " : this.ErrorBullet;

            this.BoxTitle = String.IsNullOrEmpty(this.BoxTitle) ? "Sorry, but an error was made" : this.BoxTitle;

            this.BoxMessage = String.IsNullOrEmpty(this.BoxMessage) ? "Please check the following:" : this.BoxMessage;

            if (this.ValidationMode != Mode.Manual) { this.ValidationMode = Mode.Auto; }

            if (this.BoxWidth == null) { this.BoxWidth = "auto"; }

            // always start assuming no error

            //this.Visible = false;

        }

 

        // Render

        protected override void Render(HtmlTextWriter writer)

        {

            try

            {

                // We're only going to render if there were errors.

                if (this.ErrorList.Errors.Count > 0)

                {

                    string color = System.Drawing.ColorTranslator.ToHtml(this.ErrorColor);

                    StringBuilder sb = new StringBuilder(512);

                    // Build out html for a box with a Title and a 2px border that

                    // displays the message for each ValidationError in the ErrorList.

                    sb.Append("<div style=\"width:" + this.BoxWidth + ";\" >");

                    // Show the title only if BoxTitle has a value.

                    if (!String.IsNullOrEmpty(this.BoxTitle))

                    { sb.Append("<div style=\"width:auto; background-color:" + color + "; padding-left:7px; padding-bottom:2px; padding-top:2px; color: White; font-family:Verdana; font-weight:bold; font-size:small;\">" + this.BoxTitle + "</div>"); }

                    // We always show the rest of the box.

                    sb.Append("<div style=\"width:auto; border:2px solid " + color + "; padding: 5px; color:" + color + "; font-family:Verdana; font-size:small;\">");

                    sb.Append("<strong>" + this.BoxMessage + "</strong><br />");

                    // Get a handle on the ValidationBox

                    IValidationContainer valPage = this.Page as IValidationContainer;

                    if (valPage == null){return;}

                    ValidationBox valBox = valPage.ValidationBox;

                    // Set the error sort order to match the order of the

                    // validation labels on the page.

                    foreach (ValidationError error in this.ErrorList.Errors)

                    {

                        if (valBox.ValidationLabels.Exists(n => n.FieldName == error.FieldName))

                        {

                            error.SortOrder = valBox.ValidationLabels.FindIndex(n => n.FieldName == error.FieldName);

                        }

                        else

                        {

                            error.SortOrder = int.MaxValue;

                        }

                    }

                    this.ErrorList.Errors.Sort((ValidationError n1, ValidationError n2) =>  n1.SortOrder.CompareTo(n2.SortOrder));

                    foreach (ValidationError error in this.ErrorList.Errors)

                    {

                        sb.Append(this.ErrorBullet + error.ErrorMessage + "<br />");

                    }

                    sb.Append("</div>");

                    sb.Append("</div>");

                    writer.Write(sb.ToString());

                }

            }

            catch (Exception e)

            {

                // do nothing

            }          

        }

The ValidationBox Class

This is the big one.  We encapsulate all of our logic for keeping the PageErrors list, implementing an IsValid property for the page, keeping a reference to the page’s FieldMapping method (which maps FieldNames to the UIFieldNames actually used in the UI), registering ValidationLabel controls, registering ValidationSummary controls, and handling the processing of ValidationLabel and ValidationSummary controls. 

public class ValidationBox

    {

 

        #region "PROPERTIES"

 

            // PageErrors

            private PageErrorList _pageErrors;

            public PageErrorList PageErrors

            {

                get { if (_pageErrors == null) { _pageErrors = new PageErrorList(); }; return _pageErrors; }

                set { _pageErrors = value; }

            }

            // ValidationLabels

            private List<ValidationLabel> _validationLabels;

            public List<ValidationLabel> ValidationLabels

            {

                get

                {

                    if (_validationLabels == null) { _validationLabels = new List<ValidationLabel>(); }

                    return _validationLabels;

                }

            }

            // ValidationSummaries

            private List<ValidationSummary> _validationSummaries;

            public List<ValidationSummary> ValidationSummaries

            {

                get

                {

                    if (_validationSummaries == null) { _validationSummaries = new List<ValidationSummary>(); }

                    return _validationSummaries;

                }

            }

            // SuccessMessageControls

            private List<SuccessMessage> _successMessageControls;

            public List<SuccessMessage> SuccessMessageControls

            {

                get

                {

                    if (_successMessageControls == null) { _successMessageControls = new List<SuccessMessage>(); }

                    return _successMessageControls;

                }

            }

            // SuccessMessage

            public String SuccessMessage { get; set; }

            // SuccessTitle

            public String SuccessTitle { get; set; }

            // IsValid

            public Boolean IsValid

            {

                get { return this.PageErrors.Errors.Count > 0 ? false : true; }

            }

 

 

            // MapFieldNames

            // Delegate for Page Method that maps BAL Entity field names

            // to the UI Names used in  error messages. Once fields are

            // mapped, the PageErrors object can automatically generate 

            // usable error messages for entity validation errors.

            public delegate void FieldMapper(PageErrorList ErrorList);

            public FieldMapper FieldMapperFunction{get; set; }

        #endregion

 

 

 

 

 

        #region "CONSTRUCTORS"

            public ValidationBox(FieldMapper MapperFunction)

            {

                // We get the field mapper function from the page as a

                // constructor parameter.

                this.FieldMapperFunction = MapperFunction;

                // Create the PageErrorList and run the field mapper.

                this.PageErrors = new PageErrorList();

                FieldMapperFunction.Invoke(this.PageErrors);

                // At this point we have a new ValidationBox with a

                // PageErrorList that contains no errors but has all

                // of it's field mappings set.

            }

        #endregion

 

 

 

 

 

        #region "CLASS METHODS"

            //

            // RegisterValidationLabel

            //

            public void RegisterValidationLabel(ValidationLabel label)

            {this.ValidationLabels.Add(label);}

 

 

            //

            // RegisterValidationSummary

            //

            public void RegisterValidationSummary(ValidationSummary summary)

            {this.ValidationSummaries.Add(summary);}

 

 

            //

            // RegisterSuccessMessageControl

            //

            public void RegisterSuccessMessageControl(SuccessMessage sm)

            { this.SuccessMessageControls.Add(sm); }

 

            //

            // ProcessValidationControls

            // To make this method run right before the render we manually

            // add it to the PreRender event in the constructor.

            //

            public void ProcessValidationControls(Object sender, EventArgs e)

            {

                // Set the ErrorList collection for all summaries

                foreach (ValidationSummary summary in this.ValidationSummaries)

                { summary.ErrorList = this.PageErrors; }

                // Reset all ValidationLabels

                foreach (ValidationLabel label in this.ValidationLabels)

                { label.ClearError(); }

 

                if (this.IsValid)

                {

                    // No errors, set the success message if it exists.

                    if (String.IsNullOrEmpty(this.SuccessMessage))

                    {

                        foreach (SuccessMessage sm in this.SuccessMessageControls)

                        { sm.BoxTitle = String.Empty; sm.BoxMessage = String.Empty; }

                    }

                    else

                    {

                        foreach (SuccessMessage sm in this.SuccessMessageControls)

                        { sm.BoxTitle = this.SuccessTitle; sm.BoxMessage = this.SuccessMessage; }

                    }

                }

                else

                {

                    // There were errors, set the isError state on each validation label.

                    foreach (ValidationError error in this.PageErrors.Errors)

                    {

                        foreach (ValidationLabel label in this.ValidationLabels.FindAll(n => n.FieldName == error.FieldName))

                        { label.SetError(); }

                    }

                }

            }

        #endregion

 

 

    }

FormPageBase

Now that we’ve defined our validation controls, we need to use them on our ASP.Net page.  Since there are a number of things I want to happen on a data entry/validation container page, I usually create a FormPageBase.  This can then be the base class for any page where I’m entering data.  The FormPageBase implements IValidationContainer and inherits from the PageBase for my application. Notice that FormPageBase requires a MapFieldNames sub, and a delegate to this sub is passed to ValidationBox as a constructor parameter.

    abstract public class FormPageBase : PageBase, IValidationContainer

    {

        // ValidationBox

        private ValidationBox _validationBox;

        public ValidationBox ValidationBox

        {

            get

            {

                if (_validationBox == null) { _validationBox = new ValidationBox( new ValidationBox.FieldMapper(MapFieldNames) ); }

                return _validationBox;

            }

        }

 

        // Constructor - default

        public FormPageBase() : base()

        {

            // Register method to automatically process validation controls.

            this.PreRender += new EventHandler(this.ValidationBox.ProcessValidationControls);

        }

 

        // MapFieldNames

        // Required by the ValidationBox. This method maps BAL Entity field names to

        // the UI Names that are used in error messages. Once fields are mapped, the

        // PageErrors object can automatically generate usable error messages for

        // entity validation errors. The method is passed to the ValidationBox as

        // a delegate. If there is no need to map field names then just create a

        // method with the right signature that does nothing.

        abstract protected void MapFieldNames( PageErrorList ErrorList );

    }

The Payoff – Our Concrete Page

We written a lot of code, but the good part is that it’s all plumbing.  Now that the validation classes and the FormPageBase are written, we never have to touch them again. To create pages that use all of this automated validation code is a simple 3 step process:

  1. Add ValidationLabel and ValidationSummary controls to my markup
  2. Implement a MapFieldNames() method
  3. Bind any errors to my ValidationBox.PageErrors list.

So the framework/plumbing code got a little complex and took some work, but using it is easy.  Below is a listing of the PersonForm page that shows all of the pieces that are directly required for our validation implementation.  I’ve omitted boilerplate code like GetPersonFromForm since I’m sure you’ve seen enough code by now.

public partial class PersonForm : FormPageBase
    {
            //--------------------------------------------------------
            // btnSave_Click
            //--------------------------------------------------------
            protected void btnSave_Click(object sender, EventArgs e)
            {
                BAL.Person person = GetPersonFromForm();
 
                // Run page and entity level validation
                ValidatePage();
                this.ValidationBox.PageErrors.AddList(person.Validate());
 
                // If any errors were found during validation then bail out 
                // and let the validation controls will automatically handle
                // displaying the errors.
                if (this.ValidationBox.PageErrors.Errors.Count != 0) { return; }
 
                // No errors at this point so we'll try to save. If we run into a
                // save-time error we just add it to the PageErrors and bail out.
                try
                {
                    PersonRepository.SavePerson(ref person, true);
                }
                catch (Exception ex)
                { 
                    this.ValidationBox.PageErrors.Add(new ValidationError("Unknown", ex.Message));
                    return;
                }
            }
        
            //--------------------------------------------------------
            // ValidatePage
            //--------------------------------------------------------
            protected void ValidatePage()
            { 

                // Password Confirm Password must match - *** Page Validation ***

                if (FormHelper.ParseString(txtPassword.Text) != FormHelper.ParseString(txtPasswordConfirm.Text))

                {

                    this.ValidationBox.PageErrors.Add(new ValidationError("Page.ConfirmPassword", "Confirm Password and Password don't match"));

                }

            }      
 
 
            //--------------------------------------------------------
            // MapFieldNames
            // Required for PageBase implementation. Method maps full 
            // Entity field names to the UI Names that need to be
            // used in error messages. Once fields are mapped, the 
            // PageErrors object can automatically generate usable 
            // error messages for entity validation errors. If no fields
            // need to be mapped then just create an empty method.
            //--------------------------------------------------------
            override protected void MapFieldNames(PageErrorList ErrorList)
            {
                // Password
                ErrorList.MapField("Person.Password", "Password");
                // ConfirmPassword
                ErrorList.MapField("Page.ConfirmPassword", "Confirm Password");
                // Name
                ErrorList.MapField("Person.Name", "Name");
                // Nickname
                ErrorList.MapField("Person.Nickname", "Nickname");
                // PhoneMobile
                ErrorList.MapField("Person.PhoneMobile", "Mobile Phone");
                // PhoneHome
                ErrorList.MapField("Person.PhoneHome", "Home Phone");
                // Email 
                ErrorList.MapField("Person.Email", "Email");
                // City
                ErrorList.MapField("Person.City", "City");
                // State
                ErrorList.MapField("Person.State", "State");
                // ZipCode
                ErrorList.MapField("Person.ZipCode", "Zip Code");
                // ImAddress
                ErrorList.MapField("Person.ImAddress", "IM Address");
                // ImType
                ErrorList.MapField("Person.ImType", "IM Type");
                // TimeZoneId
                ErrorList.MapField("Person.TimeZoneId", "Time Zone");
                // LanguageId
                ErrorList.MapField("Person.LanguageId", "Language");
            }
    }

 

Summary

So that’s one design for custom validation that allows us to consolidate validation logic in our business objects, but still use some nice features for automated display of error messages and error indicators.  This is probably more work than most people want to do for validation design, but hopefully you’ve gotten some good ideas for how something like this can work.  At some point in the future, I’m going to revisit this topic and show how to use the standard ASP.Net validation controls in combination with the Enterprise Library validation block to provide similar functionality.

2 comments:

  1. How will the following work in WCF scenario, will you expose BO Person or DTO Person,
    And how will you control the creation of a DTO using “PersonRepository” in WCF scenario.

    BAL.Person person = GetPersonFromForm();

    PersonRepository.SavePerson(ref person, true);

    ReplyDelete
  2. great...from where i can download this example for learning

    ReplyDelete