To The Controller And Back. Part 2 – Validation

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 the SitecoreController will render the current page:

  1. It will first run the mvc.buildPageDefinition which will in turn run mvc.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.
  2. The next step is to run mvc.getPageRendering to find the layout document and create a RenderingView wrapped around it

When later the framework calls the Render() method on the returned view it will trigger the mvc.renderRendering and this time it is the whole thing. The layout document will run and trigger mvc.renderPlaceholder for each placeholder. Each placeholder will run mvc.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 the ViewData which is still there. We haven’t crossed the boundaries of the request.

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 it Cacheable unless there are errors to show. How? I will save that for another blog post.

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 the PageContext.Current.Item. Making it believe that the current page is something else is very easy – just set PageContext.Current.Item to be that something else right before you return GetDefaultAction(). 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 – via TempData – but this is not something Html.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() with UpdateTargetId 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 => v.Value.Errors.Count > 0)
                  .Select(x => 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 and 4xx or 5xx will send it down the OnFailure 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 < 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.

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!
Pavel Veller

2 comments on To The Controller And Back. Part 2 – Validation

NateNovember 19, 2015 - Reply

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

Pavel VellerNovember 19, 2015 - Reply

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?

Add a Comment

Your email address will not be published. Required fields are marked *

Or request call back