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.
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" />
<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.
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.
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:
- Add ValidationLabel and ValidationSummary controls to my markup
- Implement a MapFieldNames() method
- 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.
How will the following work in WCF scenario, will you expose BO Person or DTO Person,
ReplyDeleteAnd how will you control the creation of a DTO using “PersonRepository” in WCF scenario.
BAL.Person person = GetPersonFromForm();
PersonRepository.SavePerson(ref person, true);
great...from where i can download this example for learning
ReplyDelete