Showing posts with label Custom Validation. Show all posts
Showing posts with label Custom Validation. Show all posts

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.

Friday, January 30, 2009

Custom Validation Step 3: Simple Page Validation

In my previous post Custom Validation Step 2: Business Object Validaiton, I showed how the custom validation design that I’m working with could be applied to a Person entity (business object).  The key piece of the puzzle is that my Person class contains a ValidationErrors member which is just a generic list of ValidationError objects.  This handy list is populated with all of the validation errors for my entity whenever I call the Validate() method.  Now it’s time for the payoff.  In this post I’ll walk through a basic implementation of the custom validation design on an ASP.Net page.  Next week I’ll show a more complex implementation that uses self registering ErrorLabel and ValidationSummary controls.

The PersonForm.aspx Page

We’re going to create a PersonForm.aspx page that will be used to create or edit a person entity. This is a simple data input form with labels and input controls.  It looks like this.

image

The PageErrorList Class

We’re going to create a new member on our page called PageErrors that will contain a list of  ValidationError objects, just like the ValidationErrors list on our Person class. This allows us to implement a design where the page’s PageErrors list can contain the ValidationErrors list from the Person object.  This is really the key to making the whole design work.  We use the exact same mechanism to store page level errors as we use to store validation errors in our business objects.  That makes it possible for us to run Validate() on a business object, then pass the resulting ValidationErrors() list to our page, the page can then just append those errors to it’s own PageErrors list. 

You may have noticed that I said PageErrors contains a List<ValidationError> just now. That’s because we need PageErrors to do some additional things that a plain generic list isn’t going to handle for us, like appending lists of ValidationError objects, and setting UIFieldNames when needed.  So, we’re going to create a wrapper class called PageErrorList.   

There are 3 main parts to PageErrorList.  First it contains our generic list of ValidationErrors. Second, it contains a FieldMappings dictionary that can be used to map entity FieldNames to the UIFieldNames used by our page (we want error messages to use “Mobile Phone” not “PhoneMobile”). Third, it contains a collection of helper methods that do things like adding a single ValidationError to the Errors list, adding lists of ValidationErrors, and generating a summary of error messages.  Here’s the code:

public class PageErrorList

    {

        // Errors

        private List<ValidationError> _errors;

        public List<ValidationError> Errors

        {

            get

            {

               if (_errors == null) { _errors = new List<ValidationError>(); }

               return _errors;

            }

            set { _errors = value; }

        }

 

        // FieldMappings

        // Dictionary that contains mappings between the FieldNames and UIFieldNames.

        // FieldName is the key, UIFieldName is the value.

        private Dictionary<string, string> _fieldMappings;

        public Dictionary<string, string> FieldMappings

        {

            get

            {

              if (_fieldMappings == null)

              {

                _fieldMappings = new Dictionary<string, string>();

              }

              return _fieldMappings;

            }

            set { _fieldMappings = value; }

        }

 

        // MapField

        // Helper method to create items in FieldMappings. Main purpose is

        // to make clear which item is FieldName and which is UIFieldName.

        public void MapField(string FieldName, string UIFieldName)

        {

            FieldMappings.Add(FieldName, UIFieldName);

        }

 

        // Add

        // Add a single ValidationError to Errors, and as we add it

        // check the FieldMappings to see if we have a UIFieldName

        // for this FieldName and set it if we do.

        public void Add(ValidationError e)

        {

            string uiFieldName;

            if (FieldMappings.TryGetValue(e.FieldName, out uiFieldName))

            {

              e.UIFieldName = uiFieldName;

            }

            Errors.Add(e);

        }

 

        // AddList

        // Same as Add() but this method takes a list of ValidationErrors.

        public void AddList(List<ValidationError> list)

        {

            foreach (ValidationError e in list) { Add(e); }

        }

 

        // GetErrorMessages

        // Returns a string that lists all errors and is meant

        // to be used for the error summary.

        public string GetErrorMessages(string ErrorBullet)

        {

            System.Text.StringBuilder messages = new System.Text.StringBuilder(512);

            foreach (ValidationError e in Errors)

            {

                messages.Append(ErrorBullet + e.ErrorMessage + "<br />");

            }

            return messages.ToString();

        }

    }

Now that we have our new PageErrorList class, we just add a PageErrors property to our PersonForm page. This property contains an object of type PageErrorList as shown below.   

// PageErrors

private PageErrorList _pageErrors;

public PageErrorList PageErrors

{

    get

    {

      if (_pageErrors == null)

      {

        _pageErrors = new PageErrorList();

      }

      return _pageErrors;

    }

    set { _pageErrors = value; }

}

 

Bringing It All Together

Now we have a PageErrors member that’s ready to be our container for all errors related to the page.  But how do we use it?  There are two main sources of errors on a page like this; entity validation errors, and page level errors.  Entity validation errors are exactly what you would think, the errors that we get back when validating an entity. Page level errors are errors that result from UI requirements. What does that mean?  Scroll up a bit and look at our UI page.  See the Password and Confirm Password fields?  Password is a member of our Person entity, so making sure we have a valid password is an example of entity level validation.  However, the entity doesn’t have anything to do with Confirm Password.  Making sure that the Confirm Password value matches the Password value is purely a requirement of the UI. That’s an example of page level validation.

So, our workflow on this page is simple.  A user clicks the save button, we need to run both page level validation and entity validation, if the page passes all validation we save the person, if not we give an error summary.  We need to make a ValidatePage() method to handle page level validation.  The method just runs the page level validation rules and adds an errors to our PageErrors member.  Remember that this method contains only validation rules not covered by the entity(s).

// ValidatePage

protected void ValidatePage()

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

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

    {

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

    }

You may have noticed the FormHelper class used in the code above.  Whenever I’m getting values from a form I always have repetitive code that does things like null check on a DropDownList.SelectedValue or trim TextBox.Text, so I stick that code in static methods contained in a static FormHelper class. This approach also has the benefit of codifying best practices for getting data from a form.  If everyone is using the FormHelper, you don’t have to worry about a junior developer (or me) creating code that will blow up because he isn’t doing something like a null check on ddl.SelectedValue.

Next is the GetPesonFromForm() method.  Before we can validate or save a Person object we need to create it from our submitted form data.  GetPersonFromForm() does that for us.  It also demonstrates how easy this type of thing can be when you use a FormHelper class. When you look at the code below, keep in mind our business object architecture.  We use PersonDTO objects to move data between layers of our app.  The DTO (Data Transfer Object) is just data container, it’s contains properties for all of the person data fields.  The Peson class that we’re creating below is a full fledged business object. It contains both data and behavior.  It’s data is stored in a very simple way, it just has a single Data property of type PersonDTO.  For more detail take a look at Custom Validation Step 2: Business Object Validation.

// GetPersonFromForm

private BAL.Person GetPersonFromForm()

{

  BAL.Person person = PersonRepository.GetNewPerson();

  if (this.PersonGuid != CommonBase.Guid_NullValue)

  {

    person.Data.PersonGuid = this.PersonGuid;

  }

  person.Data.Name = FormHelper.ParseString(txtName.Text);

  person.Data.Email = FormHelper.ParseString(txtEmail.Text);

  person.Data.Password = FormHelper.ParseString(txtPassword.Text);

  person.Data.TimeZoneId = FormHelper.ParseInt(ddlTimeZone.SelectedValue);

  person.Data.City = FormHelper.ParseString(txtCity.Text);

  person.Data.State = FormHelper.ParseString(ddlState.SelectedValue);

  person.Data.ZipCode = FormHelper.ParseInt(txtZipCode.Text);

  person.Data.PhoneHome = FormHelper.ParseString(txtPhoneHome.Text);

  person.Data.PhoneMobile = FormHelper.ParseString(txtPhoneMobile.Text);

  person.Data.ImAddress = FormHelper.ParseString(txtImAddress.Text);

  person.Data.ImType = FormHelper.ParseInt(ddlImType.SelectedValue);

  return person;

}

All right.  Now we’re ready to create our button click event.  After all of the plumbing we’ve created, our event code is very simple. To recap, we just create a person object from our form data, run both page level and entity level validation, then check to see if we have errors, if so show the error summary, if not save the person. Here it is:

            // btnSave_Click

            protected void btnSave_Click(object sender, EventArgs e)

            {

                BAL.Person person = GetPersonFromForm();

 

                // Run page and entity level validation

                ValidatePage();

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

                // If any errors were found during page validation or the domain

                // object validation then show an error summary.

                if (this.PageErrors.Errors.Count != 0)

                {

                  lblErrorSummary.Text = this.PageErrors.GetErrorMessages(“* ”);

                }

                else

                {

                    PersonRepository.SavePerson(ref person, true);

                }

            }

So that’s the meat of it but there is one last thing I should cover.  Remember when we created our PageErrorList class and we included a dictionary that could be used to store the mappings between entity FieldNames (like PhoneHome) and UIFieldNames used by the page (like Phone Number).  If we want to get error messages that match the names we use in our UI page, then we need to set up those mappings.  Fortunately, thanks to the plumbing we’ve created, it’s pretty easy.   The method below takes a reference to the PageErrorList object as a parameter and then creates the mappings using our MapField() helper method.  Note the careful “fully qualified” naming convention used for the first parameter, FieldName.  Data members that belong to the Person object are named “Person.FieldName”.  Data members that don’t belong to an entity but do exist on the page (like ConfirmPassword) are named “Page.FieldName”.  The main thing to remember is that the FieldName we use here in the mappings must match the FieldName we used when we created the ValidationError object back in the entity level Validate() method or in the page level ValidatePage() method.  With these mappings in place, our ValidationError objects will know to replace the <FieldName> pseudo tags in our error messages with UI specific names that will work with our Asp.Net page and will make sense to our users.  One more thing, for MapFieldNames() to work it needs to be called from some place in the page event lifecycle.  I put my call to MapFieldNames in the Page_PreInit() in my FormPageBase class (the base class that I use for all of my data entry style pages).

            // MapFieldNames

                 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");

            }

So that’s our implementation of custom validation on an Asp.Net page, the simple version.  Looking back it seems like a lot until you realize that most of what we’ve done is just plumbing that can be written once and then used across every page in your application.  The only validation code that you need to create for each individual page is the ValidatePage() method and the MapFieldNames() method.  Stuff like GetPersonFromForm() would have had to be written whether you use this model or not. So once initial development is done, your implementation for additional pages is almost trivial, and the benefit of consolidating your business logic inside your business objects is definitely worth it.  Remember that we’ve been focusing on validation like is this a valid email, but your validation should also implement rules like is this person allowed to add this feature to their service plan if they don’t have an email saved in the system and no other person in their customer group has an email saved to the system.  The latter is the type of validation rule that very clearly does not belong in your UI.  Consolidating your validation in one place, using a mechanism that can be understood and used by your UI can really pay big dividends when it comes to extending and maintaining your code.

Next time I’m going to extend this model even further with a new FormBasePage that is an IValidationContainer, and self registering ValidationLabel, ErrorSummary, and MessageSummary controls.  These additions will automate a lot of the things that I want to happen in my UI forms like creating a summary whenever there are page errors and giving a visual indicator next to lines where there was an error. 

For people who want to see the full PersonForm page class, here is a more or less complete implementation of the things I covered in this post:

 

    public partial class PersonForm : FormPageBase

    {

        #region "PROPERTIES"

            // PageMode

            protected enum enumPageMode

            {

                NULL = 0,

                NEW= 1,

                EDIT = 3

            }

            private enumPageMode _pageMode=enumPageMode.NULL;

            protected enumPageMode PageMode

            {

                get

                {

                    // If _pageMode hasn't been set yet then we need to

                    // pull it from the querystring().  If a known page

                    // mode isn't found, throw an error.

                    if (_pageMode == enumPageMode.NULL)

                    {

                        string pageMode = Request.QueryString["PAGEMODE"];

                        if (String.IsNullOrEmpty(pageMode))

                        {

                           throw new Exception("Unhandled PageMode");

                        }

                        switch (pageMode)

                        {

                            case "NEW":

                                _pageMode = enumPageMode.NEW;

                                break;

                            case "EDIT":

                                _pageMode = enumPageMode.EDIT;

                                break;

                        }

                    }

                    return _pageMode;

                } 

            }

            // PersonGuid

            private Guid _personGuid = CommonBase.Guid_NullValue;

            protected Guid PersonGuid

            {

                get

                {

                    if (_personGuid == CommonBase.Guid_NullValue)

                    {

                        if (this.ViewState["__PersonGuid"] != null)

                        {

                            _personGuid=(Guid)ViewState["__PersonGuid"];

                        }

                    }

                    return _personGuid;

                }

                set

                {

                    _personGuid = value;

                    this.ViewState["__PersonGuid"] = value;

                }

            }

        #endregion

 

 

 

 

 

        #region "PAGE EVENTS"

            //--------------------------------------------------------

            // Page_Load

            //--------------------------------------------------------

            protected void Page_Load(object sender, EventArgs e)

            {

                if (!this.IsInitialized) { InitializePage(); }

            }

        #endregion

 

 

 

 

 

        #region "OTHER EVENTS"

            //--------------------------------------------------------

            // btnSave_Click

            //--------------------------------------------------------

            protected void btnSave_Click(object sender, EventArgs e)

            {

                BAL.Person person = GetPersonFromForm();

 

                // Run page and entity level validation

                ValidatePage();

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

                // If any errors were found during page validation or the domain

                // object validation then show an error summary.

                if (this.PageErrors.Errors.Count != 0)

                {

                  lblErrorSummary.Text = this.PageErrors.GetErrorMessages(“* ”);

                }

                else

                {

                    PersonRepository.SavePerson(ref person, true);

                }

            }

        #endregion

 

 

 

 

 

        #region "CLASS METHODS"

            //--------------------------------------------------------

            // GetPersonFromForm

            //--------------------------------------------------------

            private BAL.Person GetPersonFromForm()

            {

                BAL.Person person = PersonRepository.GetNewPerson();

                if (this.PersonGuid != CommonBase.Guid_NullValue)

                {

                    person.Data.PersonGuid = this.PersonGuid;

                }

                person.Data.Name = FormHelper.ParseString(txtName.Text);

                person.Data.Email = FormHelper.ParseString(txtEmail.Text);

                person.Data.Password = FormHelper.ParseString(txtPassword.Text);

                person.Data.TimeZoneId = FormHelper.ParseInt(ddlTimeZone.SelectedValue);

                person.Data.City = FormHelper.ParseString(txtCity.Text);

                person.Data.State = FormHelper.ParseString(ddlState.SelectedValue);

                person.Data.ZipCode = FormHelper.ParseInt(txtZipCode.Text);

                person.Data.PhoneHome = FormHelper.ParseString(txtPhoneHome.Text);

                person.Data.PhoneMobile = FormHelper.ParseString(txtPhoneMobile.Text);

                person.Data.ImAddress = FormHelper.ParseString(txtImAddress.Text);

                person.Data.ImType = FormHelper.ParseInt(ddlImType.SelectedValue);

                return person;

            }

 

 

            //--------------------------------------------------------

            // InitializePage

            // Sets the initial state of the page. 

            //--------------------------------------------------------

            override protected void InitializePage()

            {

                // Bind all ddl items.

                BindDdlTimeZone();

                BindDdlState();

                BindDdlImType();

 

                // Populate form values for the state we're in

                switch (PageMode)

                {

                    case enumPageMode.EDIT:

                        {

                            // Existing person.

                            BAL.Person thisPerson = PersonRepository.GetPersonByPersonGuid(this.PersonGuid);

                            PopulatePage(ref thisPerson);

                            break;

                        }

                    case enumPageMode.NEW:

                        {

                            // New person.

                            SetPageDefaults();

                            break;

                        }

                }

                // Set the initialized flag

                this.IsInitialized = true;

            }

 

 

            //--------------------------------------------------------

            // SetPageDefaults

            //--------------------------------------------------------

            protected void SetPageDefaults()

            {

                // do nothing for now.

            }

 

 

            //--------------------------------------------------------

            // PopulatePage

            //--------------------------------------------------------

            protected void PopulatePage(ref BAL.Person thisPerson)

            {

                // Set all data values. Be sure to check for null values against

                // the null defaults defined in CommonBase.

                txtName.Text = thisPerson.Data.Name == CommonBase.String_NullValue ? String.Empty : thisPerson.Data.Name;

                txtEmail.Text = thisPerson.Data.Email == CommonBase.String_NullValue ? String.Empty : thisPerson.Data.Email;

                txtPassword.Text = thisPerson.Data.Password == CommonBase.String_NullValue ? String.Empty : thisPerson.Data.Password;

                txtPasswordConfirm.Text = txtPasswordConfirm.Text;

            }

 

 

            //--------------------------------------------------------

            // BindDdlTimeZone

            //--------------------------------------------------------

            protected void BindDdlTimeZone()

            {

                ddlTimeZone.DataSource = TimeZoneRepository.GetAllUS();

                ddlTimeZone.DataTextField = "MicrosoftId";

                ddlTimeZone.DataValueField = "TimeZoneId";

                ddlTimeZone.DataBind();

                ListItem selectOne = new ListItem("Select One", "");

                ddlTimeZone.Items.Insert(0, selectOne);

            }

 

            //--------------------------------------------------------

            // BindDdlState

            //--------------------------------------------------------

            protected void BindDdlState()

            {

                ddlState.DataSource = StateRepository.GetAll();

                ddlState.DataTextField = "StateName";

                ddlState.DataValueField = "StateCode";

                ddlState.DataBind();

                ListItem selectOne = new ListItem("Select One", "");

                ddlState.Items.Insert(0, selectOne);

            }

 

            //--------------------------------------------------------

            // BindDdlImType

            //--------------------------------------------------------

            protected void BindDdlImType()

            {

                ddlImType.DataSource = ImTypeRepository.GetAll();

                ddlImType.DataTextField = "ImName";

                ddlImType.DataValueField = "ImId";

                ddlImType.DataBind();

                ListItem selectOne = new ListItem("Select One", "");

                ddlImType.Items.Insert(0, selectOne);

            }

 

            //--------------------------------------------------------

            // 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 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");

            }

 

 

            //--------------------------------------------------------

            // 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"));

                }

                }            

        #endregion

 

    }