Making ASP.NET MVC 2 client side validation work with AJAX loaded forms

Few days ago I wrote a post in this thread on forums.asp.net. In one of the posts vazavi noticed, that client side validation works only until you reload form using AJAX. I decided to take a deeper look at this. After few hours I have found a solution. The solution depends on which client side validation script you are using (the one which works with jQuery or the one which not). First I will go with the one which doesn't use jQuery. To make it work we need to write one function (I have just added it to MicrosoftMvcValidation.js):
Sys.Mvc.FormContext.OnSuccessEnableClientValidation = function (ajaxContext) {
  //Getting the update target container
  var updateTarget = document.getElementById(ajaxContext.$4.id);
  //Getting all script elements in it (script elements injected with innerHtml are not executed)
  var mvcClientValidationMetadataOldScripts = updateTarget.getElementsByTagName('script');
  var mvcClientValidationMetadataNewScripts = [];
  //For every script element
  while (mvcClientValidationMetadataOldScripts.length > 0) {
    //Create a new one
    var mvcClientValidationMetadataNewScript = document.createElement('script');
    mvcClientValidationMetadataNewScript.type = 'text/javascript';
    mvcClientValidationMetadataNewScript.text = mvcClientValidationMetadataOldScripts[0].text;
    //Add it to collection
    mvcClientValidationMetadataNewScripts.push(mvcClientValidationMetadataNewScript);
    //And remove old one
    updateTarget.removeChild(mvcClientValidationMetadataOldScripts[0]);
  }
  //For every new script element
  while (mvcClientValidationMetadataNewScripts.length > 0) {
    //Append it to update target container, this way they will be executed and generate needed metadata
    updateTarget.appendChild(mvcClientValidationMetadataNewScripts.pop());
  }
  //Calling Microsoft validation initialization for new metadata
  Sys.Mvc.FormContext._Application_Load();
}

Now we can use our new function as OnSuccess handler for Ajax.BeginForm:
<% Html.EnableClientValidation(); %>
<% using (Ajax.BeginForm("...", "...", null, new AjaxOptions() { UpdateTargetId = "...", OnSuccess = "Sys.Mvc.FormContext.OnSuccessEnableClientValidation" }, new { id = "..." })) { %>
  ...
<% } %>

Now it should work. Let's do the same for MicrosoftMvcJQueryValidation.js. We will add the following function inside of it:
function __MVC_OnSuccessEnableClientValidation(ajaxContext) {
  //Getting the update target container
  var updateTarget = document.getElementById(ajaxContext.$4.id);
  //Getting all script elements in it (script elements injected with innerHtml are not executed)
  var mvcClientValidationMetadataOldScripts = updateTarget.getElementsByTagName('script');
  var mvcClientValidationMetadataNewScripts = [];
  //For every script element
  while (mvcClientValidationMetadataOldScripts.length > 0) {
    //Create a new one
    var mvcClientValidationMetadataNewScript = document.createElement('script');
    mvcClientValidationMetadataNewScript.type = 'text/javascript';
    mvcClientValidationMetadataNewScript.text = mvcClientValidationMetadataOldScripts[0].text;
    //Add it to collection
    mvcClientValidationMetadataNewScripts.push(mvcClientValidationMetadataNewScript);
    //And remove old one
    updateTarget.removeChild(mvcClientValidationMetadataOldScripts[0]);
  }
  //For every new script element
  while (mvcClientValidationMetadataNewScripts.length > 0) {
    //Append it to update target container, this way they will be executed and generate needed metadata
    updateTarget.appendChild(mvcClientValidationMetadataNewScripts.pop());
  }
  //Getting new metadata
  var allFormOptions = window.mvcClientValidationMetadata;
  if (allFormOptions) {
    //For every form in metadata
    while (allFormOptions.length > 0) {
      //Enable validation for form based on metadata
      var thisFormOptions = allFormOptions.pop();
      __MVC_EnableClientValidation(thisFormOptions);
    }
  }
}

We can use this function in the same way as previous one:
<% Html.EnableClientValidation(); %>
<% using (Ajax.BeginForm("...", "...", null, new AjaxOptions() { UpdateTargetId = "...", OnSuccess = "__MVC_OnSuccessEnableClientValidation" }, new { id = "..." })) { %>
  ...
<% } %>

I can't say that this solution has been tested in every possible scenario, so if you find any problems with it, just let me know.

16 comments:

adammcraven said...

Thanks for this information on the client-side validation, it helped me come up with my solution to the issue: http://adammcraventech.wordpress.com/2010/06/11/asp-net-mvc2-ajax-executing-dynamically-loaded-javascript/

Earn Part Time Jobs said...

Hi. I actually tried the solution posted on the link by "adammcraven".

http://adammcraventech.wordpress.com/2010/06/11/asp-net-mvc2-ajax-executing-dynamically-loaded-javascript/

The javascript here parses the script tags and evaluates them by default on "OnSuccess" event of AJAX call. But it doesn't seem to work.

Is it possible that your function be modified to call itself on OnSuccess event of every Ajax.ActionLink call.

Tomasz Pęczek said...

Hi. I'm currently on vacation, so I'm offline most of the time. I will try to modify my function the way you asked and respond as soon as possible.

ChaosSlave51 said...

i was using
$.load to add elements to my form instead of Ajax.BeginForm

I was able to adapt your solution for MicrosoftMvcValidation.js with a small change

var updateTarget = document.getElementById(ajaxContext.$4.id);
to
var updateTarget = document.getElementById($(this).attr('id'));

Ryan said...

This solution is excellent, but when the submit button is clicked, the data is being validated both client and server side when the client side validation should be preventing the form from posting.

if you stick a breakpoint on the post action and enter invalid data, you'll see what I mean.

Any suggestions on how to resolve this?

I would just use Jquery validation, but I'm really set on using Data Annotation Validators. I just wish MS would document their MvcAjax and MvcValidation files so I could have some idea of what's going on in there.

Tomasz Pęczek said...

To be honest, that wouldn't be the best idea. You can't rely only on client-side validation, because it can be easly tempered with. You should always revalidate your data on server.

Ryan said...

hmmm I need to be more clear in how I ask this...

When using the HTML.BeginForm() helper, if the client-side validation doesn't pass, the form doesn't post. This is expected behavior. Why run server code if we know data is invalid?

When using the AJAX.BeginForm() helper, the form posts whether the Client catches any errors or not. How can we prevent the client from posting if the client catches errors?

If this is still unclear, I'll whip up an example.

Tomasz Pęczek said...

Now I understand what you are asking, but I would rather see it as some kind of JavaScript bug. I will test this case as soon as possible and let you know.

Ryan said...

Right, it's definitely a javascript issue. The html and ajax helpers are just wired up to behave very differently and Microsoft doesn't seem to have been very mindful of ajax applications when they designed their client validation helper.

This whole thing has me considering writing my own client validation library.

Thanks in advance for the help! I've been beating my head against the wall for a few days on this.

Tomasz Pęczek said...

Ryan, I have tested the case you have described, and if the solution described in this psot is used, then there is no problem with client side validation. The form doesn't get POST if it's not valid. Please check if you have implemented aboive solution properly. If you will still have problems, plz contact me on email with peace of your code so I can test that.

Ryan said...

It looks like this was a bug in an older version of MVC. We're still using VS2008 at my work-place. I went home and tried to duplicate the issue in 2010 and everything worked as it should.

I also found out that the code you have in this post can be replaced with:

function InitValidation() {
Sys.Application.remove_load(arguments.callee);
Sys.Mvc.FormContext._Application_Load();
}

Thanks again for the help, and great blog you have here!

Hardy said...

Hi,

I tried your solution, but somehow ValidationSummary and ValidationMesssge does not seem to show up.

Any idea?

Thanks
Hardy

Tomasz Pęczek said...

Hi Hardy,

I would need more information about yours scenario so I can try to reproduce it.

Greetings

Anonymous said...

This is a test.

Shannon Marsh said...

I seem to be getting the same issue in MVC 4 RC, although I am using the jquery.validate.min.js and /jquery.validate.unobtrusive.min.js scripts.

Would I still need to add the additional functions you have described here? or should these issues be resolved in the newer version of MVC?
Thanks.

Tomasz Pęczek said...

The validation framework has changed a little bit since MVC 2. The issue should not be present anymore and if it`s appearing then the reason is most probably different and above solution will not resolve it I'm afraid.