views:

1139

answers:

6

Does anyone have a elegant way of dealing with errors in ASP.Net MVC? I constantly run into issues when dealing with requests to controller actions where the Action can be used for both normal requests and AJAX requests. The problem I have is finding an elegant way of dealing with these issues.

For example, how could I handle validation errors? Ideally I would like to submit the form to a server via AJAX and then return any errors the action threw and display them on the page, but for the same to work via a normal postback when the client has JavaScript turned off. I know I can use the jQuery Validation Plugin as they type but it's isn't the same, nor is it ideal considering the restrictions on the data to be validated would be specified in two places (my nHibernate Validation Mappings and in the JavaScript file).

How about when the user requests a non-existent record? Should I redirect to a 404 page? What if the request was made via Ajax (say, to load in a dialogue box).

So:

How do you handle errors thrown by controller actions when they were called using Ajax? Especially Model State errors (i.e validation). Can I send it via JSON?

Do you have tips on how to make controller actions which work well when called normally and via Ajax? This is a annoying problem when writing action methods. Because of the return type I may want a different result depending on the caller. Is there anyway to do this without having two Action Methods?

What is your general strategy for handling errors in actions on MVC? Do you redirect to error pages a lot? Do you redirect to another page?

Edit: I think part of the problem is I want different things to happen, so if there is an error I would want to stop any progress until it's fix and therefore send a error back. Otherwise I may want to update different areas of the page. But if I had one return how would I know it's a success or failure without wrapping it an object that has a property indicting so (thus making it more difficult to use partial views)

Thanks

+3  A: 

The AJAX call is not a page refresh so I would definitely not redirect to a 403, 404 etc. I would display a client side dialog explaining the unexpected result of the request appropriately. Your controller can return the results of failed validation to the AJAX call in a similar fashion to how it would return success data so your dialog can also display whatever is required in this scenario.

AdamRalph
In this case the Dialog would close on success but anyway, What would you do if the data returned was different between a success and a error
Damien
If success, close the dialog, if failure, display an appropriate message?
AdamRalph
What AdamRalph suggested is exactly what we do. I built a custom view "_NotificationView" that the modals return, on ajax return I check to see if the html is blank, if it is, I close the dialog, otherwise I display the message to the user and keep the dialog open.
Tom Anderson
A: 

We never redirect (why make the user repeatedly press the 'back' button in case they don't understand what needs to be inputted into a particular field?), we display errors on the spot, be it AJAX or not (the location of the 'spot' on the page is entirely up to you, for all AJAX requests we just show a coloured bar top of the page, just like stackoverflow does for first-comers, only ours does not push the rest of the content down).

As for form validation, you can do both server-side and client-side. We always try to show server-side errors on top of the form in a distinctive container, client-side - right next to the field in question on submit. Once the page comes back from the server with server-side validation errors, user will only see the server-side ones initially, so no duplication there.

For data specified in two places, not sure if I understand, since I've never dealt with nHibernate Validation Mappings.

dalbaeb
He doesn't want to have to define his validation logic twice: he already defines it using his nHibernate validation mapping attributes (applied to class properties) and doesnt want to have to do it again in his JQuery client-side javascript. I assume he wants to be able to create the client-side javascript validation from the validation in his nHibernate mappings.
Merritt
Yes, I am aware this support will exist in MVC 2 but until then.
Damien
Hmm.. So nHibernate would be considered server-side then? Still, in my understanding, client-side validation should always complement server-side validation to prevent incorrect data from trickling into the DB, mostly because it can be really tedious to fix later, as well as for security purposes.
dalbaeb
Yeah, Server side is done. This isn't so much an issue really.
Damien
A: 

Keep two error views, one for normal (complete) display of the page and the other which just renders the errors in XML or JSON.

Never validate client-side as the user can easily bypass it. For real-time validation, just run an AJAX request to the server for validation, that way you're writing the validation code once.

sirlancelot
+1  A: 

Disclaimer: I haven't done much ASP.NET MVC Programming.

That said, I've used a number of MVC frameworks in other languages, and have completed quite a few django projects. Over time, I've kinda evolved a pattern to deal with this sort of thing in django, using template inclusion.

Essentially, I create a partial template containing the form (and any validation errors), which gets included in the main template. The view (or Controller, in your case) then selects between these two templates: AJAX requests get the partial template, and regular requests get the full template. In the main template, I use the jQuery form plugin to submit the form via AJAX, and simply replace the current form with the data from the server: if validation fails, I get a form with highlighted fields, and a list of errors on top; if the POST succeeds, the form is replaced with a success message, or whatever is more appropriate for your situation.

I guess in the ASP.NET MVC world, UserControls could be the equivalent of partials? This article shows how to acheive something similar to what I've described (though, the article seems rather dated, and things could've changed).

To answer your second question, I don't think you should be doing a redirect on "page not found" -- one returns a 302, the other should return a 404; one isn't an error condition, the other is. If you lose the status code, your javascript gets more complicated (since you'll have to somehow test the actual returned data, to figure out what happened). These two posts should give you some ideas on how to implement HTTP-friendly error pages. Django does something similar (raise a Http404 exception, return a 404 status code and optionally render a 404.html template).

elo80ka
A: 

I have used xVal in the past, along with some homegrown reflection-based JS rule generators. The idea is that you define the rule once (in your case, via nHibernate), and an HTML helper reflects over your properties and generates client-side validation code on the fly (using the jQuery.validation plugin). Exactly the right way to have a responsive client-side UI, while still enforcing server-side validation.

Unfortunately, this method doesn't work for AJAX-posted forms.

For AJAX rules, it would be as simple as adding a Errors array to your returned JSON objects. Anywhere you're using AJAX, simply check the length of Errors (that's all ModelState.IsValid does) and display an error. You can use the IsAjaxRequest method to detect an AJAX call:

public ActionResult PostForm(MyModel thing)
{
  UpdateModel(thing);

  if (this.Request.IsAjaxRequest() == false)
  {
    return View();
  }
  else
  {
    foreach(var error in ModelState.Errors)
    {
      MyJsonObject.Errors.Add(error.Message); 
    }
    return JsonResult(MyJsonObject);
  }
}
Peter J
+1  A: 

Do you have tips on how to make controller actions which work well when called normally and via Ajax? This is a annoying problem when writing action methods.

Yes, yes I do. We also worked through a similar problem--we wanted the app to have a bunch of forms which would be called generally via ajax but could get hit normally. Moreover, we didn't want a whole bunch of duplicate logic in javascript floating around. Anyhow, the technique we came up with was to use a pair of ActionFilterAttributes to intercept the forms, then a little javascript to wire up the form for ajax handling.

First, for the ajax request, we just wanted to swap out master pages:

    private readonly string masterToReplace;

    /// <summary>
    /// Initializes an Ajax Master Page Switcharoo
    /// </summary>
    /// <param name="ajaxMaster">Master page for ajax requests</param>
    public AjaxMasterPageInjectorAttribute(string ajaxMaster)
    {
        this.masterToReplace = ajaxMaster;
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        if (!filterContext.HttpContext.Request.IsAjaxRequest() || !(filterContext.Result is ViewResult)) return;
        ViewResult vr = (ViewResult) filterContext.Result;
        vr.MasterName = masterToReplace;
    }
}

On the return side, we use xVal to validate client-side, so one shouldn't get much in the way of invalid data, but one can still get some. To that end, we just use normal validation and have the aciton method return the form with validation messages. Successful posts are rewarded with redirects in general. In any case, we do a little json injection for the success case:

/// <summary>
/// Intercepts the response and stuffs in Json commands if the request is ajax and the request returns a RedirectToRoute result.
/// </summary>
public class JsonUpdateInterceptorAttribute : ActionFilterAttribute 
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAjaxRequest())
        {
            JsonResult jr = new JsonResult();
            if (filterContext.Result is RedirectResult)
            {
                RedirectResult rr = (RedirectResult) filterContext.Result;
                jr.Data = new {command = "redirect", content = rr.Url};
            }
            if (filterContext.Result is RedirectToRouteResult)
            {
                RedirectToRouteResult rrr = (RedirectToRouteResult) filterContext.Result;
                VirtualPathData vpd = RouteTable.Routes.GetVirtualPath(filterContext.RequestContext, rrr.RouteValues);
                jr.Data = new {command = "redirect", content = vpd.VirtualPath};
            }
            if (jr.Data != null)
            {
                filterContext.Result = jr;
            }
        }
    }
}

The final trick is using a little javascript object to tie everything together:

function AjaxFormSwitcher(form, outputTarget, doValidation) {
    this.doValidation = doValidation;
    this.outputTarget = outputTarget;
    this.targetForm = form;
}

AjaxFormSwitcher.prototype.switchFormToAjax = function() {
    var afs = this;
    var opts = {
        beforeSubmit: this.doValidation ? afs.checkValidation : null,
        complete: function(xmlHttp, status){ afs.processResult(afs, xmlHttp, status); },
        clearForm: false
    };
    this.targetForm.ajaxForm(opts);
}

AjaxFormSwitcher.prototype.checkValidation = function(formData, jqForm, options) {
    jqForm.validate();
    return jqForm.valid();
}

AjaxFormSwitcher.prototype.processResult = function(afs, xmlHttp, status) {
    if (xmlHttp == null) return;
    var r = xmlHttp;
    var c = r.getResponseHeader("content-type");
    if (c.match("json") != null) {
        var json = eval("(" + r.responseText + ")");
        afs.processJsonRedirect(json);
    }
    if (c.match("html") != null) {
        afs.outputTarget.html(r.responseText);
    }
}

AjaxFormSwitcher.prototype.processJsonRedirect = function(data) {
    if (data!=null) {
        switch (data.command) {
            case 'redirect':
                window.location.href = data.content;
                break;
        }
    }
}

That little script handles stuff like wiring up the form to do ajax and processing the result (either json command or html which gets displayed in the target). I admittedly suck at writing javascript, so there is probably a much more graceful way to write that.

Wyatt Barnett
nice post, some useful ideas there.
Damien
"Mom! Phineas and Ferb are answering questions on Stack Overflow!"
Charlie Flowers
???. Or who are Phineas and Ferb and who's mom are you talking about?
Wyatt Barnett
It's a kids' show on Disney (yes, I have a kid), and they have a running gag in which they say, "Yes, yes I do".
Charlie Flowers