In part 1 I looked at how Sitecore routes controllers that the forms POST to. Let’s see how you can go about validating your forms. I will show you traditional POST as well as AJAX forms with HTML fragments and JSON data.
Test It
Do you test your server side validation? How often did you use (or see someone else use) a well remembered and well recognizable pattern:
if (!ModelState.IsValid) { return View(form); }without actually testing it? ASP.NET MVC will generate equivalent client-side validation for things like length and regex match, and most of the times your server code will not have an invalid model POSTed to it. And when it happents, the above pattern will just work, right? Before we get into the weeds of why it will not work in Sitecore let’s make sure we have a plan to test it.
Turn Client Side Validation Off: #1
To let your server actually run whatever it is you put into the curly braces for
!ModelState.IsValid
you need to turn off your client side validation. One way of achieving it would be to disable it in your razor view that renders the form:Html.EnableClientValidation(false);I don’t like this approach. You’re tinkering with your code which means you have to remember to put it back where it was, plus you can’t really do it on your integration or quality assurance environment. And even if you can, your QA engineers can’t.
Turn Client Side Validation Off: #2
The same result can be achieved by running the following in your browser’s JS console:
$('form[name="<your form name>"]').data('validator').settings.rules = {}I like it better. Very elegant and not intrusive, don’t you think?
Sitecore
Now when we can test our server side validation we can talk about what to put into the curly braces. You can’t really do
return View(model)
in Sitecore. Even if there is a matching view to render it’s probably not what you need. Let’s take a closer look.Traditional POST
With traditional POST you probably want to re-render the page with the form if the model failed to validate. You can then display your error messages and have your user try again. When you say
return View(model)
you’re asking the engine to run the matching razor view and send the result back to the browser. A view file in Sitecore is only one piece of the puzzle that is a page. It could be a rendering, could be a layout, but it’s not the whole thing. You need the whole thing, right?Same Page / URL / Item
Here’s the most straightforward way, assuming you POST-ed to a controller that works in context of the page that has a form on it (read part 1 if you’re not sure how to do that):
public class ContactUsController : SitecoreController { [HttpPost] public ActionResult SubmitContactUsForm(ContactUsForm form) { if (!ModelState.IsValid) { return GetDefaultAction(); // or base.Index() } //... } }The
GetDefaultAction()
in theSitecoreController
will render the current page:
- It will first run the
mvc.buildPageDefinition
which will in turn runmvc.getXmlBasedLayoutDefinition
to parse the layout (aka Renderings) field of the current page (PageContext.Current.Item
) and build the page definition (PageContext.Current.PageDefinition
) for all pipelines that are about to run.- The next step is to run
mvc.getPageRendering
to find the layout document and create aRenderingView
wrapped around itWhen later the framework calls the
Render()
method on the returned view it will trigger themvc.renderRendering
and this time it is the whole thing. The layout document will run and triggermvc.renderPlaceholder
for each placeholder. Each placeholder will runmvc.renderRendering
for each component in it and down the recursion spiral it goes. With the page definition built, every piece of the puzzle will know what to do and will find its way. Your rendering with a form will re-render it with the error messages (you are using@Html.ValidationMessageFor()
, right?)It’s worth mentioning that your models will be created anew. The
ModelState
, however, is detached from your models and it lives on theViewData
which is still there. We haven’t crossed the boundaries of the request.[su_note note_color=”#fafafa”]Last but not least, pay attention to your caching settings for the component that generates the form. If you have it configured as
Cacheable
you are not likely to see your server-side error messages. And you probably want itCacheable
unless there are errors to show. How? I will save that for another blog post.[/su_note]Different Page / URL / Item
The “same page” solution works great if the controller is set to work in context of the page you POSTed from. What if it wasn’t? What if you needed it to run in context of a different item (via your own route and
scItemPath
)? Can it use the same technique to re-render the page you POSTed from?Technically it can but I am not sure you’d want it to. Not in context of a traditional POST. The
GetDefaultAction()
is predicated on thePageContext.Current.Item
. Making it believe that the current page is something else is very easy – just setPageContext.Current.Item
to be that something else right before you returnGetDefaultAction()
. It will render that page.The biggest issue with this approach is the URL. Say your form is on
/contact-us
and your controller’s route is/contact-us/submit/{*scItemPath}
. The form will post to/contact-us/submit/<GUID>
and that’s what your users will see in their browsers if validation fails and the page re-renders. You wouldn’t want that. You would want/contact-us
. With the “same page” approach it was transparent as you posted to the same URL you rendered the form with.Your only option is to redirect them back to the page with the form but then you’d loose the
ViewData
and the validation errors. There is a way to pas them in – viaTempData
– but this is not somethingHtml.ValidationMessageFor()
can work with… We’re all smart developers and I am sure we can come up with a solution to work around it but it won’t feel natural. That said, there is a place for the “different page” technique in our toolbox.AJAX with HTML
If you’re using
Ajax.BeginRouteForm()
withUpdateTargetId
your controller shouldn’t re-render the whole page. You will get the mirror against mirror effect if you do. You are probably just rendering partials:[HttpPost] public ActionResult SubmitMyForm(MyForm form) { if (!ModelState.IsValid) { return View("_Errors.cshtml", form); } // ... }And then in your errors view you’re drilling down into the validation errors using
ViewData.ModelState
.You can make it more Sitecore-friendly if you architect your fragments / partials to be page items. With AJAX it’s ok to use the “different page” technique. The URL you POST to is an implementation detail hidden from your users. Your fragments will be Page Editor enabled and your code will be just a tiny bit more verbose:
public class ContactUsController : SitecoreController { [HttpPost] public ActionResult SubmitContactUsForm(ContactUsForm form) { if (!ModelState.IsValid) { PageContext.Current.Item = errorFragmentItem; return GetDefaultAction(); // or base.Index() } //... } }AJAX with JSON
You’re in for a treat if you got this far.
I personally don’t like sending HTML fragments in response to an AJAX request. There are, of course, perfectly valid reasons to use that technique but my first option is JSON. Leaner, cleaner, and your form’s target becomes an API endpoint that you can reuse across channels (e.g. mobile app).
You would render your AJAX form like this:
using (Ajax.BeginRouteForm("myformroute", new { scItemPath = Model.Item.ID }, new AjaxOptions { OnFailure = "MySite.Forms.MyForm.PostFailure", OnSuccess = "MySite.Forms.MyForm.PostSuccess" })) { // ... }Sending a JSON structure with errors is pretty straightforward:
if (!ModelState.IsValid) { Response.StatusCode = (int) HttpStatusCode.BadRequest; var errors = ModelState.Where(v =&amp;amp;amp;amp;gt; v.Value.Errors.Count &amp;amp;amp;amp;gt; 0) .Select(x =&amp;amp;amp;amp;gt; new { Name = x.Key, Message = x.Value.Errors[0].ErrorMessage }); return new JsonResult() { Data = errors }; }Two things here:
- First, I’m sending the errors back with the
400
status code. It doesn’t feel right to handle errors in the success handler and4xx
or5xx
will send it down theOnFailure
route. We deemed the form not valid so telling the client they sent a bad request feels like exactly the thing to do. - Second, we’re sending a single error message for each field with errors. It makes the example less cluttered and easier to read. The client side is doing something along the lines of
$label.html(message)
to display the errors so you would need to concatenate those messages to show them all.
With the error data in the client, can we plug it into the validation framework and show messages in context of the forms fields? Here goes the promised treat – an almost hassle-free way to do so:
MySite.Forms.MyForm.PostFailure = function(xhr, status, error) { // Note: you probably need to handle different statusCode errors differently var data = $.parseJSON(xhr.responseText); var $form = $('.my-form-container').find('form'); var errors = {}; for (var i = 0; i &amp;amp;amp;amp;lt; data.length; i++) { errors[data[i].Name] = data[i].Message; } $form.data('validator').showErrors(errors); }That’s it. Your server generated validation errors will display right where you need them.
[su_note note_color=”#fafafa”]Next time I will talk about dependency injection and how to do it as much multi-tenant friendly and as much Sitecore out-of-the-box as possible. Stay tuned![/su_note]
I followed your example for returning a JsonResult and it does route back to the main page but the validator is not showing the error message.
Any idea what is wrong? Thanks
Not without looking at the code, no. I assume you defined the function similar to MySite.Forms.MyForm.PostFailure in my example and wired it in via AjaxOptoins and OnFailure. correct?