I’m working on an MVC2 application that makes extensive use of forms being sucked into the current page using ajax like this, which issues the request and gets back html representing the form which is then presented in a jQuery UI modal dialog;
type: postType,
url: url,
data: data,
dataType: "html",
async: true,
cache: false,
success: function (data, text)
{
unlockPage();
dialogContent(title, data, width);
},
error: function (request, textStatus, errorThrown)
{
unlockPage();
handleStandardErrors(null, request);
}
});
I wanted to use the new out of the box validation toolset with data annotations, which on the face of it looks pretty cool, so I followed the guide on getting this working using jQuery validator. Namely, I got myself the MicrosoftMvcJQueryValidation.js from the MvcFutures project and then added data annotations to my view model, eg;
public string Email{ get; set; }
That’s it to get server side validation working, which works a treat, but to get client side working, I then added the following script includes to my master page;
<script type="text/javascript" src="/Scripts/MicrosoftMvcJQueryValidation.js"></script>
Enabled client validation in my form and added some validation messages;
<%using( Html.BeginForm("AddUser", "Users", FormMethod.Post, null)){ %>
<%=Html.LabelFor( m => m.Email) %>
<%=Html.EditorFor( m => m.Email )%>
<%=Html.ValidationMessageFor( m => m.Email) %>
Ran the app, and…..nothing… nada, not a thing. So I started digging and tracing through the MVC source. All appeared to be working as it should. EnableClientValidation was setting a flag in the form context to tell the framework to output validation code. The dispose method of MvcForm (which is instantiated with the BeginForm using) was invoking the code to output some javascript structured describing what to validate and how, but it didn’t seem to be using this anywhere. I soon worked out why…
This little snippet of code is in MicrosoftMvcJQueryValidation.js, which remember we included in our master page (which is rendered in the host page, NOT our partial form view we’re getting using ajax).
var allFormOptions = window.mvcClientValidationMetadata;
if (allFormOptions) {
while (allFormOptions.length > 0) {
var thisFormOptions = allFormOptions.pop();
__MVC_EnableClientValidation(thisFormOptions);
}
}
});
That won’t be fired so to get the validation working, we just need to do the same thing right? Not quite. I added the above code to a jQuery startup function in my partial view, it gets called successfully but….nothing, it still didn’t work because window.mvcClientValidationMetadata was undefined. Now the reason is something different – the jQuery startup function is actually invoked before the inline <script></script> block that sets window.mvcClientValidationMetadata!
The way the window.mvcClientValidationMetadata is used can help us though – the inline script pushes the latest validation data for the form onto this variable and the code above pops it back off. As such, we can just interrogate the length of the array when we start up and if there is no data there yet, retry after a short delay. If we keep doing that until it’s been processed all should be well with the world. So, my modified startup script is as follows;
{
initContentLoaded();
setupMvcValidation();
});
function setupMvcValidation()
{
alert(window.mvcClientValidationMetadata);
if (window.mvcClientValidationMetadata == undefined || window.mvcClientValidationMetadata.length < 1)
setTimeout(setupMvcValidation, 100);
var allFormOptions = window.mvcClientValidationMetadata;
if (allFormOptions)
{
while (allFormOptions.length > 0)
{
var thisFormOptions = allFormOptions.pop();
__MVC_EnableClientValidation(thisFormOptions);
}
}
}
and all is indeed well with the world. I think this should even cover the edge cases where you have multiple forms in your partial view, each with their own validation, but I’ve yet to test it any more thoroughly.
Tony,
ReplyDeleteVery interesting solution. I am working through the same problem, and haven't yet solved it. It seems to me that in your solution using Html.BeginForm would cause an actual form postback. Granted, the client-side validation would prevent that in the case of errors, but for the sake of argument suppose the client-side javascript validation didn't fire. If the user posted the form, and the server caught an error, the page would reload into the main content area rather than the modal. Is this the case? If so, do you think Ajax.BeginForm might solve this?
Hi Nat.Net,
ReplyDeleteI use Html.BeginForm, but it doesn't post back as I have a jQuery handler that intercepts the form and posts it using a jQuery ajax request.
If it was the case that the client validation wasn't firing then you've got to ask why. There are only 3 reasons I can think of.
1. Your scripts contain errors and therefore aren't registering correctly. This is something that just needs to be well tested and fixed before shipping your product.
2. User intentionally turns off scripts for a malicious reason. In this case, server side validation is still going to fire and prevent the malicious attack and to be honest, if someone want's to intentionally mess with my system, I don't care what their user experience is like :)
3. User accidentally turns off scripts or has scripts turned off in their browser completely. In this case, the application as a whole isn't going to work and they can't get to the modal screen anyway as it itself is script based.
So, in the unlikely event that scripts didn't fire, I wouldn't be too worried, but there are some other things you could do if you are worried about this.
You could use an empty action in your BeginForm and wire the URL up in your submit code, or you could skip the use of the html helper completely and just write a manual form tag. To do the latter you would need a manual call to ViewContext.OutputClientValidation() after you close your form. It's possible that the Ajax.BeginForm might also help, but I've not tried it myself.
Hope this helps.
Tony
Tony,
ReplyDeleteThanks for the response. Just to be clear the call to __MVC_EnableClientValidation is what binds the form post interception code? I didn't make that connection earlier. Well done. I will use this solution. Thanks for the help.
Tony,
ReplyDeleteI am still missing a vital piece here. How do you get the form contained within Html.BeginForm to post via an Ajax call rather than a form POST? I see your jquery code to load the initial modal, but how do you force that to be used when posting the form? In my case, I have some validations that cannot be run on the client (i.e., looking for existing account in the system). So, I have to rely on Server validation. All of the client-side validation works great using your solution, but if the server validation fails, then I get the form reloading in the main content area rather than being targeted in the Modal popup.
Thanks again.
That's right, the __MVC_EnableClientValidation is the thing that does the binding. Normally this is invoked on document.ready by the MicrosoftMvcJQueryValidation.js, but as that's included in the host page, not your html fragment, you need to call it yourself when your fragment has loaded and after the rules structure has been processed on the page (hence the timer).
ReplyDeleteFor the posting mechanism, that's a whole separate issue - this post really was just about getting the validation to run.
To post via an ajax post, I hook a standard jQuery $.ajax call into my form submit event, the controller then returns a json structure as to whether it succeeded or not and any error information I might need on the client and as part of the success handler of the .ajax call I close the dialog and refresh the display etc as necessary.
You could also do it using a .ajax call which returns the view, then simply replace the dialog content with your view, thus rendering your form again from your model, but with any server side validation errors shown. (I've just gone quite a bit further with my framework to use json messages and process all kinds of things coming back out of my controller in a standard way, but none of that is really relevant)
:)
Let me know if you need more info - feel free to send me a cut down sample project and I'll get it working for you and send it back :)
Cheers
Tony,
ReplyDeleteFinal note: I got this working using the jquery.form plugin which allows me to hijack all of the requests and make them Ajax and specify an object target on the page. I believe this is more lightweight than using ASP.NET Ajax which is why I chose it. Thanks again.
Yep, I don't use any asp.net ajax stuff or the microsoft ajax framework. jQuery all the way for me.
ReplyDeleteI think the jquery.form plugin will be doing pretty much the same, using the $.ajax call. Glad it's all working for you - enjoy.
Cheers
Tony
The following might also be useful - it's an end to end sample demonstrating all the validation principles along with jQuery code to load the form dynamically with ajax and post it back too.
ReplyDeletehttp://www.deepcode.co.uk/2010/09/mvc2-validation-samples.html
Hi, nice article! But still I am having trouble getting it to work.
ReplyDeleteWe are having the form loaded via jQueryUI dialog. When the form model is in error, the page simply redirects instead of returning via the .ajax call. What could be the issue? I am using Html.BeginForm and latest jQuery along with jQuery.validate.js and microsoftmvcjqueryvalidation.js from futures project.
Thanks!
GM
Hi GM,
ReplyDeleteIt sounds to me like the you've not hooked your form up to post it's data using $.ajax() and instead it's just posting straight back with a normal request.
The standard HTML form doesn't know to post it's data using jquery by default remember, and it will instead send the form and refresh the entire page with the result.
Take a look at this post: http://www.deepcode.co.uk/2010/09/mvc2-validation-samples.html
Which links to some other guidance on the subject and a sample project I've put on codeplex that does what you are describing.
Hope this helps,
Tony
Hi Tony, got to work!!
ReplyDeleteWe have submit button click events wired up to js function via jQuery .live mechanism. This was not working when the two js files named in my earlier post were included. I got to work by including our common.js file within the view itself rather than in the master. This is actually strange since the view is based on the master, still is not able to find some functions defined in the common.js file.
And your sample helped me a lot!
Thanks for your quick response and the complete sample!
I would also love to see the sample include forms loaded via jQuery-UI.
Cheers!
GM
Looks like my joy was still shortlived. This query logged by someone is exactly what I want to achieve. http://osdir.com/ml/jquery-ui/2009-02/msg01006.html
ReplyDeleteHowever, I cannot find a complete sample showing using jQuery UI instead of just ajax within page.
Issues I know in jQuery UI case are that javascript in the form loaded is not executed. And content loaded via .html() above in your example does not hold onto the events hooked up dynamically. Are these right?
Can you show me an example using jquery ui if possible? I would really appreciate!
Thanks,
GM
Hi GM,
ReplyDeleteI've extended the sample to do exactly this, but I'm in North Wales at the moment with little connectivity and tethering through my phone isn't working.
I'll upload in a couple of days when I get home. I'm not aware of any of the problems you describe, in fact I'm doing exactly this for several projects without any issues.
Cheers
Tony
Hi Tony,
ReplyDeleteThanks for the sample, I am waiting to check it out!
(The Issues I referred to, I had read about them on some other forum, while researching on this issue.)
Meanwhile, I was able to get the serverside validations working but still client side is not working.
I am executing these statements in master like below:
$(document).ready(function () {
_mvcValidationHookCounter = 0; //declared in another js file loaded before this
setupMvcValidation();
});
AND after I get the response from the ajax call like below:
//in submit click event
if (!$(form).valid()) {
return false;
}
$.ajax({
type: "POST",
url: form.action,
data: $(form).serialize(),
dataType: "html",
async: true,
cache: false,
success: function (data, text) {
if (data != undefined && data != null) {
try {
//.....
}
}
catch (err) { //try for json data to be returned
//.....
}
}
//I do the below when there are server side validation errors and the dialog cannot be closed
_mvcValidationHookCounter = 0; //declared in another js file loaded before this
setupMvcValidation();
}
//....
}
In Firebug I can see that it enters setupMVCValidation and calls it 30 times (I increased in a hope to get it valid) but still is unable to the mvcClientValidationMetadata value to be valid.)
I have included both the js files and the EnableClientValidation as well. I have a custom validator for which I do not yet have a adaptor written. Can this be causing the validations to be not setup? (Server side valiation of this custom validator also fires up properly.)
Thanks for taking the time out for modifying the sample!
Cheers,
GM
Hi GM,
ReplyDeleteI think the hookup to the validator is in the wrong place in your sample.
You are hooking up the client validators on a successful ajax response, but at that point, the response data (the HTML with the form etc) will not yet have been added to the DOM and so no metadata about the validation will have been output, hence why your call executes 30 times.
If you move that code to the actual HTML fragment being returned you should find it works as it will be executed when the content is added to the DOM correctly.
I've checked in the latest source to the project on code plex with a sample that demonstrates all this as requested.
Let me know how you get on.
Cheers,
Tony
Hi Tony,
ReplyDeleteSorry, I did not paste complete code earlier but I was doing this in the response checking
$(dialog).html(divHtml);
where divHTML was a Div tag picked out of the response html. So the validator hook up location seemed right. Also, I tried adding the calls to document.ready function in the head section of page being loaded in modal. But that too did not help.
Thanks for the updated sample! I checked it out and found only two differences.
1. The dialog load in your case does load the entire page whereas in our case we are picking out a specific div out of the page and loading only that.
2. The dialog in your case is a user control whereas in our case it is a page.
So I went ahead and changed your sample by adding a div around the form and changed the control to a page and boom, the client side validation started failing in that too!
I then did change my dialog "pages" to "user controls" and loaded them in entirety rather than picking out a div! And all client validation started working!!
So lessons learnt, have dialogs implemented as user controls and do not load a section out of the page for modal dialogs (atleast) or you loose out on client validation!!
Thanks a lot for your samples, I really appreciate your pulling out time for helping me out!! With the sample, I could really figure out the difference and sort out the issue!!
Cheer!
GM
Any time :)
ReplyDeleteCheers
Tony