ASP.NET Framework MVC 2 Dynamic Validation

Introduction

ASP.NET MVC 2 supports server-side as well as client-side validation, out of the box. This validation is based on the DataAnnotations attributes, which can be easily implemented by decorating the model properties with the validation attributes. ASP.NET MVC 2 will automatically enforce the validation rules, which also helps in maintaining all the validation rules in once place. However, this DataAnnotations based validation provides very limited support for the dynamic validation. Here I will cover how we can extend the out of the box validation to support dynamic validation.

Let’s walkthrough a simple scenario that takes advantage of the ASP.NET MVC 2 Validation. Create a new ASP.NET MVC 2 web application and name it as Mvc2SampleApp. Add a model to the project and name it as Interview as shown below:


public class Interview
       public Interview()
       {
           BillingAddressList = new List<SelectListItem>()
                               {
                                   new SelectListItem(){ Text = “Select …”, Value = “Select”},
                                   new SelectListItem(){ Text = “Mailing”, Value = “Mailing”},
                                   new SelectListItem(){ Text = “Property”, Value = “Property”}
                               };
       }
       [Required(ErrorMessage = “Name is required”)]
       public string Name { get; set; }
       public Address MailingAddress { get; set; }
       public Address PropertyAddress { get; set; }
       [Required(ErrorMessage = “Billing Address Type is required”)]
       public string BillingAddress { get; set; }
       public List<SelectListItem> BillingAddressList { get; set; }
       [Required(ErrorMessage = “Grace is required”)]
       public string Grace { get; set; }
       [Required(ErrorMessage = “HouseType is required”)]
       public string HouseType { get; set; }
   }
   public class Address
   {
       [Required(ErrorMessage = “Street is required”)]
       public string Street { get; set; }
       public string Street2 { get; set; }
       [Required(ErrorMessage = “City is required”)]
       public string City { get; set; }    
       [Required(ErrorMessage = “State is required”)]
       [StringLength(2, ErrorMessage = “Please enter the valid State code”)]
       public string State { get; set; }
       public string County { get; set; }
       [Required(ErrorMessage = “Zip is required”)]
       [StringLength(5, ErrorMessage = “Please enter the valid Zip code”)]
       public string Zip { get; set; }
   }
ASP.NET MVC 2 currently supports below DataAnnotations attributes for validation:
[Required]
[StringLength]
[Range]
[RegularExpression]
Now let’s add a controller to the project and name it as InterviewController.
public class InterviewController : Controller
   {
       [HttpGet]
       public ActionResult Index()
       {
           return View(new Interview());
       }
       [HttpPost]
       public ActionResult Index(Interview interview)
       {
           if (!ModelState.IsValid)
           {
               return View(interview);
           }
           return Content(“Interview saved successfully!”);
       }
   }

Now add the strongly typed view for the model. This view will show the Property/Mailing address fields based on the selected value in the Billing Address dropdown. The final code of the view should look like below:


<%@ Page Title=”” Language=”C#” MasterPageFile=”~/Views/Shared/Site.Master” Inherits=”System.Web.Mvc.ViewPage<Mvc2SampleApp.Models.Interview>” %>
<asp:Content ID=”Content1″ ContentPlaceHolderID=”TitleContent” runat=”server”>
Index
</asp:Content>
<asp:Content ID=”Content2″ ContentPlaceHolderID=”MainContent” runat=”server”>
   <script type =”text/javascript” src=”../../Scripts/jquery-1.4.1.js”></script>
   <script type =”text/javascript” src=”../../Scripts/MicrosoftAjax.js”></script>
   <script type =”text/javascript” src=”../../Scripts/MicrosoftMvcAjax.js”></script>
   <script type =”text/javascript” src=”../../Scripts/MicrosoftMvcValidation.js”></script>
   <script type=”text/javascript”>
       $(document).ready(function() {
           $(‘#dMailingAddress’).children().hide();
           $(‘#dPropertyAddress’).children().hide();
           $(‘#Grace’).attr(‘disabled’, true);
           $(‘input[id^=MailingAddress]’).attr(‘disabled’, true);
           $(‘#HouseType’).attr(‘disabled’, true);
           $(‘input[id^=PropertyAddress]’).attr(‘disabled’, true);
           $(“#BillingAddress”).bind(‘change’, function() {
               var selectedBillingAddress = $(“#BillingAddress option:selected”).text();
               if (selectedBillingAddress == ‘Mailing’) {
                   $(‘#dMailingAddress’).children().show();
                   $(‘#dPropertyAddress’).children().hide();
                   $(‘#Grace’).removeAttr(‘disabled’);
                   $(‘input[id^=MailingAddress]’).removeAttr(‘disabled’);
                   $(‘#HouseType’).attr(‘disabled’, true);
                   $(‘input[id^=PropertyAddress]’).attr(‘disabled’, true);
               }
               else if (selectedBillingAddress == ‘Property’) {
                   $(‘#dPropertyAddress’).children().show();
                   $(‘#dMailingAddress’).children().hide();
                   $(‘#Grace’).attr(‘disabled’, true);
                   $(‘input[id^=MailingAddress]’).attr(‘disabled’, true);
                   $(‘#HouseType’).removeAttr(‘disabled’);
                   $(‘input[id^=PropertyAddress]’).removeAttr(‘disabled’);
               }
           })
       })
   </script>
   <h2>Index</h2>
       <% Html.BeginForm(); %>
   <p>
   <label for=”Name” id=”NameLabel”>Name:</label>
   <%= Html.TextBoxFor(model => model.Name) %>
   <%= Html.ValidationMessageFor(model => model.Name) %>
   </p>
   <p>
   <label for=”BillingAddress” id=”BillingAddressLabel”>Billing Address:</label>
   <%= Html.DropDownListFor(model => model.BillingAddress, Model.BillingAddressList) %>
   <%= Html.ValidationMessageFor(model => model.BillingAddress) %>
   </p>
   <div id = “dMailingAddress”>
   <p>
   <label for=”Grace” id=”GraceLabel”>Grace:</label>
   <%= Html.TextBoxFor(model => model.Grace)%>
   <%= Html.ValidationMessageFor(model => model.Grace)%>
   </p>  
   <p>
   <label for=”MailingAddress” id=”MailingAddressLabel”>Mailing Address:</label>
   <br />
   <br />
   <label for=”MailingAddress_Street” id=”MailingAddress_StreetLabel”>Street:</label>
   <%= Html.TextBoxFor(model => model.MailingAddress.Street)%>
   <%= Html.ValidationMessageFor(model => model.MailingAddress.Street)%>
   </p>  
   <p>
   <label for=”MailingAddress_Street2″ id=”MailingAddress_Street2Label”>Street2:</label>
   <%= Html.TextBoxFor(model => model.MailingAddress.Street2)%>
   <%= Html.ValidationMessageFor(model => model.MailingAddress.Street2)%>
   </p>  
   <p>
   <label for=”MailingAddress_City” id=”MailingAddress_CityLabel”>City:</label>
   <%= Html.TextBoxFor(model => model.MailingAddress.City)%>
   <%= Html.ValidationMessageFor(model => model.MailingAddress.City)%>
   </p>    
   <p>
   <label for=”MailingAddress_State” id=”MailingAddress_StateLabel”>State:</label>
   <%= Html.TextBoxFor(model => model.MailingAddress.State)%>
   <%= Html.ValidationMessageFor(model => model.MailingAddress.State)%>
   </p>    
   <p>
   <label for=”MailingAddress_County” id=”MailingAddress_CountyLabel”>County:</label>
   <%= Html.TextBoxFor(model => model.MailingAddress.County)%>
   <%= Html.ValidationMessageFor(model => model.MailingAddress.County)%>
   </p>    
   <p>
   <label for=”MailingAddress_Zip” id=”MailingAddress_ZipLabel”>Zip:</label>
   <%= Html.TextBoxFor(model => model.MailingAddress.Zip)%>
   <%= Html.ValidationMessageFor(model => model.MailingAddress.Zip)%>
   </p>    
   </div>
   <div id = “dPropertyAddress”>
   <p>
   <label for=”HouseType” id=”HouseTypeLabel”>House Type:</label>
   <%= Html.TextBoxFor(model => model.HouseType)%>
   <%= Html.ValidationMessageFor(model => model.HouseType)%>
   </p>    
   <p>
   <label for=”PropertyAddress” id=”PropertyAddressLabel”>Property Address:</label>
   <br />
   <br />
   <label for=”PropertyAddress_Street” id=”PropertyAddress_StreetLabel”>Street:</label>
   <%= Html.TextBoxFor(model => model.PropertyAddress.Street)%>
   <%= Html.ValidationMessageFor(model => model.PropertyAddress.Street)%>
   </p>   
   <p>    
   <label for=”PropertyAddress_Street2″ id=”PropertyAddress_Street2Label”>Street2:</label>
   <%= Html.TextBoxFor(model => model.PropertyAddress.Street2)%>
   <%= Html.ValidationMessageFor(model => model.PropertyAddress.Street2)%>
   </p>    
   <p>
   <label for=”PropertyAddress_City” id=”PropertyAddress_CityLabel”>City:</label>
   <%= Html.TextBoxFor(model => model.PropertyAddress.City)%>
   <%= Html.ValidationMessageFor(model => model.PropertyAddress.City)%>
   </p>    
   <p>
   <label for=”PropertyAddress_State” id=”PropertyAddress_StateLabel”>State:</label>
   <%= Html.TextBoxFor(model => model.PropertyAddress.State)%>
   <%= Html.ValidationMessageFor(model => model.PropertyAddress.State)%>
   </p>   
   <p>
   <label for=”PropertyAddress_County” id=”PropertyAddress_CountyLabel”>County:</label>
   <%= Html.TextBoxFor(model => model.PropertyAddress.County)%>
   <%= Html.ValidationMessageFor(model => model.PropertyAddress.County)%>
   </p>    
   <p>
   <label for=”PropertyAddress_Zip” id=”PropertyAddress_ZipLabel”>Zip:</label>
   <%= Html.TextBoxFor(model => model.PropertyAddress.Zip)%>
   <%= Html.ValidationMessageFor(model => model.PropertyAddress.Zip)%>
   </p>
   </div>
   <input type=”submit” value = “Save Interview” />
   <% Html.EndForm(); %>
</asp:Content>

Now set the InterviewController as the default controller and run the application. In the view choose Mailing in the Billing Address dropdown, which will make all the Mailing Address related fields visible. Fill all the required fields and click on the Save Interview button. If you put the breakpoint in the post Action method, you will see that validation fails on the server side and view is returned back to the user. This happens because ASP.NET MVC is validating all the fields/Model properties, which includes the fields that were not even shown to the user. In this case, the validation on the server side failed because we haven’t entered values in the Property Address related fields, which were not shown on the UI.

Now let’s extend this model so that it validates only those fields that are applicable in a particular scenario, both on server side as well as on client side. On the client side we are already disabling the hidden fields, in the action filter we will clear out errors of the fields that were not posted to the server.

Add a new Action Filter to the project and override its OnActionExecuting method. In the OnActionExecuting method, clear out all errors of those fields that were not posted by the client.


public class DynamicValidationAttribute : System.Web.Mvc.ActionFilterAttribute   
{
       public override void OnActionExecuting(ActionExecutingContext filterContext)
       {
           var modelState = filterContext.Controller.ViewData.ModelState;
           var valueProvider = filterContext.Controller.ValueProvider;
           var keysWithNoIncomingValue = modelState.Keys.Where(x => !valueProvider.ContainsPrefix(x));
           foreach (var key in keysWithNoIncomingValue)
               modelState[key].Errors.Clear();
       }
   }
Now add this Action Filter to the post Index method of the InterviewController as shown below:
       [HttpPost]
       [DynamicValidationAttribute]
       public ActionResult Index(Interview interview)
       {

        }

Now run the application again, choose Mailing as the Billing Address, fill all the required fields and click on the Save Interview button. Now validation on the server side passes as it validates only the posted fields. So far, so good. We implemented the dynamic validation on the server side but what about the client side validation. To enable the client side validation, add the below statement in the view just above the form.



<% Html.EnableClientValidation(); %>    
<% Html.BeginForm(); %>


If you run the application again, you will observe that the validation fails on the client side; even form is not getting posted to the server. Like server side, client side validation is also validating all the fields in the form including hidden/disabled fields, due to which client side validation is failing. Now there are two ways to solve this problem.


  • A) Override the validate function of the FormContext defined in MicrosoftMvcValidation.js/MicrosoftMvcValidation.debug.js files, in our own JavaScript file and validate only those fields that are enabled.
  • B) Microsoft has written all its JavaScript code in C# programming and generated the JavaScript out of it using Script# tool. So we can get the source code of ASP.NET MVC Framework and modify the Validate method of FormContext class that is written in C# and rebuild the project, which will generate updated MicrosoftMvcValidation.js/MicrosoftMvcValidation.debug.js files with dynamic validation built-in.

If you follow the approach A), it can be easily implemented by including our own JavaScript file in the project but if you use both MicrosoftMvcValidation.js and MicrosoftMvcValidation.debug.js files, then we will have to maintain two JavaScript files separately for each of them and maintain MicrosoftMvcValidation.js file may be a bit hard due to its cryptic nature.

Following approach b) makes the maintenance very easy as we are directly dealing with the C# code but there is an additional overhead of compiling the C# project and adding version control for the same. We will cover both of these approaches here.

Solution A)

If you open the MicrosoftMvcValidation.js file you will see code block assigned to Sys.Mvc.FormContext.prototype, copy this code block to a new JavaScript file and name it DynamicValidation.js. In this block there is a validate function, which validates all the fields in for loop. Add a condition in this loop that will ensure only enabled fields are validated. Updated code of the validate function is shown below.


validate: function(eventName) {
       var $0 = this.fields;
       var $1 = [];
       for (var $2 = 0; $2 < $0.length; $2++) {
           var $3 = $0[$2];
           var fName = ‘#’ + ($3.elements[0].name.replace(“.”, “_”));
           if ($(fName).is(‘:disabled’) == false) {
               var $4 = $3.validate(eventName);
               if ($4) {
                   Array.addRange($1, $4);
               }
           }
       }
       if (this.replaceValidationSummary) {
           this.clearErrors();
           this.addErrors($1);
       }
       return $1;
   }

Similarly in MicrosoftMvcValidation.debug.js file there is a code block assigned to Sys.Mvc.FormContext.prototype. Copy this block to a new JavaScript file and name it is as DynamicValidation.debug.js. Add a condition in the validate function to validate only enabled fields.


validate: function Sys_Mvc_FormContext$validate(eventName) {
       /// <param name=”eventName” type=”String”>
       /// </param>
       /// <returns type=”Array” elementType=”String”></returns>
       var fields = this.fields;
       var errors = [];
       for (var i = 0; i < fields.length; i++) {
           var field = fields[i];
           var fName = ‘#’ + (field.elements[0].name.replace(“.”, “_”));
           if ($(fName).is(‘:disabled’) == false) {
               var thisErrors = field.validate(eventName);
               if (thisErrors) {
                   Array.addRange(errors, thisErrors);
               }
           }
       }
       if (this.replaceValidationSummary) {
           this.clearErrors();
           this.addErrors(errors);
       }
       return errors;
   }


Now add this new JavaScript file to the view after MicrosoftMvcValidation.js/MicrosoftMvcValidation.debug.js file. If you are using the debug files then it should look like below:


<script type =”text/javascript” src=”../../Scripts/ MicrosoftMvcValidation.debug.js”></script>
<script type =”text/javascript” src=”../../Scripts/DynamicValidation.debug.js”></script>

In case you are using release files then script tags should be as below:


<script type =”text/javascript” src=”../../Scripts/ MicrosoftMvcValidation.js”></script>
<script type =”text/javascript” src=”../../Scripts/ DynamicValidation.js”></script>

Now if run the application it will validate only displayed/enabled fields both on client and server side.

Solution B)

Download the ASP.NET MVC 2 source code from the Microsoft web site and open the solution in Microsoft Visual Studio 2008. Open the FormContext class of MicrosoftMvcValidationScript project and add a new condition to check whether field is enabled or disabled.


public string[] Validate(string eventName) {
           FieldContext[] fields = Fields;
           ArrayList errors = new ArrayList();
           for (int i = 0; i < fields.Length; i++) {
               FieldContext field = fields[i];               
               if (!field.Elements[0].Disabled)
               {
                   string[] thisErrors = field.Validate(eventName);
                   if (thisErrors != null)
                   {
                       ArrayList.AddRange(errors, thisErrors);
                   }
               }
           }
           if (ReplaceValidationSummary) {
               ClearErrors();
               AddErrors((string[])errors);
           }
           return (string[])errors;
       }

Build this project, which will also generate MicrosoftMvcValidation.js/MicrosoftMvcValidation.debug.js files. Copy the updated files from the bin folder into your project. Newly generated *.js files will have the conditional validation implemented in them. Run the project and everything should work as expected.

Happy Validating!

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read