Friday, February 27, 2009

Multiple Field Custom Validator

I needed to extend the Custom Validator to allow it to have multiple ControlToValidate fields, so that if any of the fields are changed the validator would trigger.

Example:

Credit Card Expiration Date - I had two dropdownlists for the month and year. I wanted a custom validator that instantly triggered a warning if the two fields did not combine into a date that was current or future.

I was able to create the JavaScript on the client side to check for that, but I would have to have 2 custom validators, one for each dropdownlist. So if I selected a new year but the combined was not current or future then the warning would show. But if I then changed the month and the combined was now current or future, it would not know to go back to the previous custom validator and validate it, since it had its own validator.

I know I could have hard coded the JavaScript to do it, but I wanted something more substantial and reusable.

So I decided to create a multi-field custom validator, just extending the .NET Custom Validator.


Here are the steps I took to create this.

1. Start a new class project.

2. Create the new class inheriting from System.Web.UI.WebControls.CustomValidator. Also includes settings for the control.

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Security.Permissions;
using System.ComponentModel;
using System.Drawing;

namespace My.CustomControls
{
   [AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
   [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
   [ToolboxData(@"<{0}:MultipleFieldsValidator runat=server></{0}:MultipleFieldsValidator>")]
   public class MultipleFieldsValidator : System.Web.UI.WebControls.CustomValidator
   {
   }
}

3. Add a Private Property to store the extra controls to validate.
        #region Private Properties

   private string _extraControlsToValidate;

#endregion

4. Add a Public Property to Get/Set the extra controls to validate.

#region Public Properties

   /// <summary>
   /// Comma separated list of control IDs that you want to check
   /// </summary>
   [Browsable(true)]
   [Category("Behavior")]
   [Themeable(false)]
   [DefaultValue("")]
   [Description("Comma separated list of extra control IDs that you want to check")]
   public string ExtraControlsToValidate
   {
     get
     {
       return _extraControlsToValidate;
     }
     set
     {
       _extraControlsToValidate = value;
     }
   }

#endregion

5. Create some helper methods to parse out that field.

#region Helper Methods

private string[] GetControlsToValidateIDs()
{
   try
   {
     string[] controlToValidateIDs = null;
     if (this.ExtraControlsToValidate != null && this.ExtraControlsToValidate.Length > 0)
     {
       string controlsToValidate = this.ExtraControlsToValidate.Replace(" ", "");
       if (controlsToValidate.Length > 0)
       {
         try
         {
           controlToValidateIDs = controlsToValidate.Split(',');
         }
         catch (ArgumentOutOfRangeException ex)
         {
           throw new FormatException(string.Format("The ExtraControlsToValidate property of {0} is not well-formatted.", this.ID), ex);
         }
       }
     }
     return controlToValidateIDs;
   }
   catch (Exception e)
   {
     throw (e);
   }
}

// Only needed if doing the RegisterExpandoAttribute in AddAttributesToRender
private string GenerateClientSideControlsToValidate()
{
   try
   {
     string[] controlToValidateIDs = this.GetControlsToValidateIDs();
     string controlToValidateIDTrimmed;
     string controlRenderIDs = string.Empty;
     foreach (string controlToValidateID in controlToValidateIDs)
     {
       controlToValidateIDTrimmed = controlToValidateID.Trim();
       if (controlToValidateIDTrimmed == string.Empty)
       {
         throw new FormatException(string.Format("The ExtraControlsToValidate property of {0} is not well-formatted.", this.ID));
       }
       controlRenderIDs += "," + base.GetControlRenderID(controlToValidateIDTrimmed);
     }
     controlRenderIDs = controlRenderIDs.Remove(0, 1); // Removing the first ","
     return controlRenderIDs;
   }
   catch (Exception e)
   {
     throw (e);
   }
}

#endregion

6. Override a method (OnPreRender) to attach the validator to the extra controls.

#region Overiden Methods

protected override void OnPreRender(EventArgs e)
{
   try
   {
     base.OnPreRender(e);
     if (base.RenderUplevel)
     {
       if (!Page.ClientScript.IsClientScriptBlockRegistered("MultipleFieldsValidator"))
       {
         if (null != this.ExtraControlsToValidate && this.ExtraControlsToValidate.Length > 0)
         {
           System.Text.StringBuilder sb = new System.Text.StringBuilder();
           sb.Append("<script language='javascript' type='text/javascript'> \n");
           sb.Append("<!-- \n");
           sb.Append("var ctrl=''; \n");
           sb.Append("var vld=''; \n");
           string[] extraControls = GetControlsToValidateIDs();
           foreach (string controlToValidateID in extraControls)
           {
             string controlToValidateIDTrimmed = controlToValidateID.Trim();
             if (controlToValidateIDTrimmed != string.Empty)
             {
               sb.Append("ctrl = document.all ? document.all['" + base.GetControlRenderID(controlToValidateIDTrimmed) + "'] : document.getElementById('" + base.GetControlRenderID(controlToValidateIDTrimmed) + "'); \n");
               sb.Append("vld = document.all ? document.all['" + this.ClientID + "'] : document.getElementById('" + this.ClientID + "'); \n");
               sb.Append("if (typeof(ctrl) != 'undefined') \n");
               sb.Append("{ \n");
               sb.Append(" ValidatorHookupControl(ctrl, vld); \n");
               sb.Append("} \n");
             }
           }
           sb.Append("--> \n");
           sb.Append("</script> \n");
           this.Page.ClientScript.RegisterStartupScript(this.GetType(), "MultipleFieldsValidator", sb.ToString());
         }
       }
     }
   }
   catch (Exception ex1)
   {
     throw (ex1);
   }
}

#endregion

7. (Optional) Override a method (AddAtrributesToRender) to put a Custom (Expando) Attribute on the validator control if the developer wants access to that in the client side function they add to the custom validator.

#region Overiden Methods

protected override void AddAttributesToRender(System.Web.UI.HtmlTextWriter writer)
{
   try
   {
     base.AddAttributesToRender(writer);
     if (this.RenderUplevel)
     {
       string clientID = this.ClientID;
       if (this.ExtraControlsToValidate != null && this.ExtraControlsToValidate.Length > 0)
       {
         Page.ClientScript.RegisterExpandoAttribute(clientID, "extracontrolstovalidate", this.GenerateClientSideControlsToValidate());
       }
     }
   }
   catch (Exception ex1)
   {
     throw (ex1);
   }
}

#endregion

8. (Optional) Add a png file to the project for use in the toolbox. Right click on it, properties, Build Action, select “Embedded Resource”. Then add this setting to the class.

[ToolboxBitmap(typeof(MultipleFieldsValidator), "MultiFieldValidator.png")]
public class MultipleFieldsValidator : System.Web.UI.WebControls.CustomValidator

9. (Optional, but required if putting in the GAC or bin folder for SharePoint) Strongly type the project. Right click on solution name, properties. Signing, Choose a strong name key file, select new, Enter Key file name, uncheck “Protect my key file with a password unless you want to have that.

10. Build the project.

11. Copy DLL to your main project’s bin folder or to the GAC if you have problems with permissions and don’t have time to modify the permission policy.

12. Add a reference to the DLL in your main project.

13. (Optional) While opened to a web page (aspx, ascx, etc), right click on a header in the Toolbox and click on Choose Items, “.NET Framework Components”, Browse. Find the DLL and select it, then OK. This will add the new MultiFieldCustomValidator to your Toolbox.

14. If added to Toolbox, drag and drop onto webpage where you want the control. It will add a Register directive at the top of the page. If you don’t add to the Toolbox, you need to first add the Register directive, then:
<%@ Register Assembly="My.CustomControls" Namespace="My.CustomControls" TagPrefix="cc3" %>

(Optional) If the full assembly is needed like for SharePoint then:
<%@ Register Assembly="My.CustomControls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxxx" Namespace="My.CustomControls" TagPrefix="cc3" %>

15. (Optional) If using SharePoint, then add a “SafeControl” to the web.config so SharePoint trusts this DLL.
<SafeControls>
   <SafeControl Assembly="My.CustomControls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4abcb93e1cb47678" Namespace="My.CustomControls" TypeName="*" Safe="True" />
</SafeControls>

16. Add the properties to the control just like you would with the CustomValidator, but add one extra property, ExtraControlsToValidate, which is a comma delimited list of all the ID’s of the extra controls you want attached to this validator.

Now you have your Multi-Field Custom Validator ready to use in any of your projects.

No comments:

Post a Comment