madvora
madvora

Reputation: 1747

MVC5 Error Handling with JQuery Ajax

Let's say I have a standard form setup with a ViewModel and validation like this.

ViewModel

public class EditEventViewModel
{
    public int EventID { get; set; }
    [StringLength(10)]
    public string EventName { get; set; }
}

Form in the View

@using (Html.BeginForm(null, null, FormMethod.Post, new {id="editEventForm"}))
{
    @Html.AntiForgeryToken()

    @Html.LabelFor(model => model.EventName)
    @Html.EditorFor(model => model.EventName)
    @Html.ValidationMessageFor(model => model.EventName)
}

Controller

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "EventName")] EventViewModel model)
{
    //Get the specific record to be updated
    var eventRecord = (from e in db.Event
                       where e.EventID == model.EventID
                       select e).SingleOrDefault();

    //Update the data
    if (ModelState.IsValid)
    {
        eventRecord.EventName = model.EventName;
        db.SaveChanges();            
    }

    return RedirectToAction("Index");
}

Now, if I do a regular form submit and enter an EventName with a string length over 10, the model error will be triggered and I'll be notified in the validation message in the view.

But, I prefer to submit my forms with JQuery AJax like this.

$.ajax({
    type: "POST",
    url: "/EditEvent/Edit",
    data: $('#editEventForm').serialize(),
    success: function () {

    },
    error: function () {

    }
});

With this way, I add some javascript on the client side to validate before the submit, but I'd still like the data annotations in the ViewModel as a backup. When this reaches the controller, it's still checked with if (ModelState.IsValid). If it's not valid, then the data is not written to the database, just as designed.

Now, I want to know what I can do if the ModelState is not valid, when posting with JQuery. This will not trigger the regular validation, so what can I do to send back information that an error has occurred?

if (ModelState.IsValid)
{
    eventRecord.EventName = model.EventName;
    db.SaveChanges();            
}
else
{
    //What can I do here to signify an error?
}

Update With Further Information

I already have custom errors set up in Web.config

<customErrors mode="On">

That routes errors to the Views/Shared/Error.cshtml file, where I'm outputting information about the error that got the request there. Is there anyway my model state error (or any error) in the controller could be sent here?

@model System.Web.Mvc.HandleErrorInfo

@{
    Layout = null;
    ViewBag.Title = "Error";
}

<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>

<p>
    Controller: @Model.ControllerName   <br />
    Action: @Model.ActionName           <br />
    Exception: @Model.Exception.Message
</p>

UPDATE again

Here's another update working with pieces of all of the responses. In my controller, I put this in the else statement throw new HttpException(500, "ModelState Invalid"); (else - meaning that the ModelState is not valid)

That triggers my custom errors in Web.config to send to the Views/Shared/Error.cshtml, (kind of) but this only shows up in FireBug like this. The actual page doesn't go anywhere. Any idea how I get this on the parent page? If this doesn't make sense, I've been using the setup described here, to send to my custom error page. The fact that this is an AJAX call is making this work a little differently.

enter image description here

Upvotes: 4

Views: 9903

Answers (6)

madvora
madvora

Reputation: 1747

For my unique situation, I decided to take a different approach. Since I have custom errors set up in Web.config, I'm able to handle any non-Ajax request errors automatically and have that information sent to Views/Shared/Error.cshtml. Suggested reading for that here.

I'm also able to handle application errors outside of MVC using the methods described here.

I have also installed ELMAH to log any errors in my SQL database. Information about that here and here.

My original intention was to catch errors in the controllers the same way when I send an Ajax request, which I guess you can't do.

When I post a form in my application, the form is first checked on the client side with javascript validation. I know which fields I don't want to be blank and how many characters and what type of data to accept. If I find a field not matching that criteria, I send information about that error to a JQuery dialog to display that information to the user. If all of my criteria is met, then I finally submit the form to the controller and action.

For this reason, I've decided to throw an exception in the controller if an error occurs. If the client-side validation finds no problem with the data being submitted and the controller still errors out, then I definitely want that error logged. Keep in mind that I'm also setting the data annotations on the ViewModel as a backup to the client-side validation. Throwing an exception will trigger the JQuery error function and will trigger ELMAH to log this error.

My controller would look like this.

// POST: EditEvent/Edit
//Method set to void because it does not return a value
[HttpPost]
[ValidateAntiForgeryToken]
public void Edit([Bind(Include = "EventID, EventName")] EditEventViewModel model)
{
    //Get the specific record to be updated
    var eventRecord = (from e in db.Event
                       where e.EventID == model.EventID
                       select e).SingleOrDefault();

    //******************************************************************//
    //*** Not really sure if exceptions like this below need to be    **//
    //*** manually called because they can really bog down the code   **//
    //*** if you're checking every query.  Errors caused from         **//
    //*** this being null will still trigger the Ajax error functiton **//
    //*** and get caught by the ELMAH logger, so I'm not sure if I    **//
    //*** should waste my time putting all of these in.  Thoughts?    **//
    //******************************************************************//


    //If the record isn't returned from the query
    if (eventRecord == null)
    {
        //Triggers JQuery Ajax Error function
        //Triggers ELMAH to log the error
        throw new HttpException(400, "Event Record Not Found");
    }

    //If there's something wrong with the ModelState
    if (!ModelState.IsValid)
    {
        //Triggers JQuery Ajax Error function
        //Triggers ELMAH to log the error
        throw new HttpException(400, "ModelState Invalid");
    }

    //Update the data if there are no exceptions
    eventRecord.EventName = model.EventName;
    db.SaveChanges();
}

The ViewModel with data annotations looks like this.

public class EditEventViewModel
{
    public int EventID { get; set; } 
    [Required]
    [StringLength(10)]
    public string EventName { get; set; }
}

The JQuery Ajax call looks like this

$.ajax({
    type: "POST",
    url: "/EditEvent/Edit",
    data: $('#editEventForm').serialize(),
    success: function () {
        //Triggered if everything is fine and data was written
    },
    error: function () {
        //Triggered with the thrown exceptions in the controller
        //I will probably just notify the user on screen here that something happened.
        //Specific details are stored in the ELMAH log, the user doesn't need that information.
    }
});

I used information from everyone's posts. Thanks to everyone for helping.

Upvotes: 0

tvanfosson
tvanfosson

Reputation: 532765

Because you want the error content back, I would suggest returning a JSON response (the alternative is a partial view, but that would mean making your JS use delegated handlers, resetting the form validation, etc.). In this case you'll want to detect and return JSON if the POST is AJAX and return a normal view/redirect otherwise. If all validation should be done client-side and it's ok to not have the error text, you could probably return an exception result and use the error handler for the .ajax() call to update the page. I've found that browser support for getting the response text on errors is inconsistent, though, so if you want the actual errors, you'll want to return a 200 OK response with the messages in JSON. My choice would probably depend on the exact use case - for example if there were several errors that I could only detect server-side I'd probably use an OK response with error content. If there were only a few or all errors should be handled client-side, then I'd go the exception route.

The custom error handler shouldn't be used or needed for this.

MVC with status result

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "EventName")] EventViewModel model)
{
    //Get the specific record to be updated
    var eventRecord = (from e in db.Event
                       where e.EventID == model.EventID
                       select e).SingleOrDefault();

    if (eventRecord == null)
    {
        if (Request.IsAjaxRequest())
        {
            return new HttpStatusCodeResult(HttpStatusCode.NotFound, "Event not found.");
        }

        ModelState.AddModelError("EventID", "Event not found.");
    }

    //Update the data
    if (ModelState.IsValid)
    {
        eventRecord.EventName = model.EventName;
        db.SaveChanges();            

        if (Request.IsAjaxRequest())
        {
            return Json(new { Url = Url.Action("Index") });
        }

        return RedirectToAction("Index");
    }

    if (Request.IsAjaxRequest())
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest, /* ... collate error messages ... */ "" );
    }

    return View(model);
}

Example JS with status result

$.ajax({
   type: "POST",
   url: "/EditEvent/Edit",
   data: $('#editEventForm').serialize(),
})
.done(function(result) {
     window.location = result.Url;
})
.fail(function(xhr) {
    switch (xhr.status) {  // examples, extend as needed
       case 400:
          alert('some data was invalid. please check for errors and resubmit.');
          break;
       case 404:
          alert('Could not find event to update.');
          break;
    }     
});

MVC with error content

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "EventName")] EventViewModel model)
{
    //Get the specific record to be updated
    var eventRecord = (from e in db.Event
                       where e.EventID == model.EventID
                       select e).SingleOrDefault();

    if (eventRecord == null)
    {
        if (Request.IsAjaxRequest())
        {
            return Json(new { Status = false, Message = "Event not found." });
        }

        ModelState.AddModelError("EventID", "Event not found.");
    }

    //Update the data
    if (ModelState.IsValid)
    {
        eventRecord.EventName = model.EventName;
        db.SaveChanges();            

        if (Request.IsAjaxRequest())
        {
            return Json(new { Status = true, Url = Url.Action("Index") });
        }

        return RedirectToAction("Index");
    }

    if (Request.IsAjaxRequest())
    {
        return Json(new 
        {
            Status = false,
            Message = "Invalid data",
            Errors = ModelState.Where((k,v) => v.Errors.Any())
                               .Select((k,v) => new
                               {
                                   Property = k,
                                   Messages = v.Select(e => e.ErrorMessage)
                                               .ToList()
                               })
                               .ToList()
        });
    }

    return View(model);
}

Example JS with error content

$.ajax({
   type: "POST",
   url: "/EditEvent/Edit",
   data: $('#editEventForm').serialize(),
})
.done(function(result) {
     if (result.Status)
     {
         window.Location = result.Url;
     }
     // otherwise loop through errors and show the corresponding validation messages
})
.fail(function(xhr) {
    alert('A server error occurred.  Please try again later.');     
});

Upvotes: 2

snow_FFFFFF
snow_FFFFFF

Reputation: 3311

Right now, your controller just swallows any errors - if the model isn't valid, it just doesn't save and never gives any feedback to the caller. You could fix this and have it return an error to jQuery by actually returning an error:

return new HttpStatusCodeResult(HttpStatusCode.BadRequest, "Some meaningful message");

Lots of options about what you want to handle and how much detail to return, but the main point is your controller should provide a response that is appropriate for the actual action it performed.

UPDATE

In response to your "Update Again" section - I wouldn't return a 500 - this means "internal server error". If you are going to return an error, a 400 (bad request) is probably more appropriate. Regardless, the problem you have with your ajax call is that it is receiving the error response from the web server (not your main browser window). If I had to guess, the error is being handled server-side and you are jquery is receiving the html response from your custom error.

If you are going to leave the automatic error handling in place, you should probably only use it for unhandled errors. Therefore, in your controller, you would handle the invalid model by returning an non-error response indicating this state (I think someone else mentioned a json response). Then, all responses would be successful, but the content would tell your application how to behave (redirect appropriately, etc...).

Upvotes: 4

Khalid
Khalid

Reputation: 235

You can send back an error message in the viewbag with a message about the items causing the error:

foreach (ModelState modelState in ViewData.ModelState.Values)
            {
                foreach (ModelError error in modelState.Errors)
                {
                //Add the error.Message to a local variable
                }
            }

ViewBag.ValidationErrorMessage = //the error message

Upvotes: 0

XPD
XPD

Reputation: 1215

What you need is a little change to your code. Because you are using ajax communication to send the data to the server you don't need to use form posting. Instead you can change the return type of your action method to JsonResult and use Json() method to send the result of the data processing.

[HttpPost]
[ValidateAntiForgeryToken]
public JsonResult Edit([Bind(Include = "EventName")] EventViewModel model)
{
    //Get the specific record to be updated
    var eventRecord = (from e in db.Event
                   where e.EventID == model.EventID
                   select e).SingleOrDefault();

    //Update the data
    if (ModelState.IsValid)
    {
        eventRecord.EventName = model.EventName;
        db.SaveChanges();  
        return Json(new {Result=true});          
    } 
    else
    {
        return Json(new {Result=false});
    }
}

Now you can use this action method for data processing.

$.ajax({
type: "POST",
url: "/EditEvent/Edit",
data: $('#editEventForm').serialize(),
success: function (d) {
   var r = JSON.parse(d);
   if(r.Result == 'true'){
       //Wohoo its valid data and processed. 
       alert('success');
   }
   else{
       alert('not success');
       location.href = 'Index';
   }
},
error: function () {

}

});

Upvotes: 0

Casey
Casey

Reputation: 805

If you're creating a RESTful service you should return an appropriate http code that indicates the data wasn't saved. Probably a 400.

Otherwise I would say return whatever makes sense to you and check for that value on the client side to determine if the call failed.

Upvotes: 1

Related Questions