Handling MVC5 errors with JQuery Ajax
Let's say I have a standard form with a ViewModel and validation.
ViewModel
public class EditEventViewModel
{
public int EventID { get; set; }
[StringLength(10)]
public string EventName { get; set; }
}
Form in 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 make a regular form and enter an EventName with a line length of more than 10, the model error will be triggered and I will be notified in a validation message in the view.
But I prefer to submit my forms using JQuery AJax like this.
$.ajax({
type: "POST",
url: "/EditEvent/Edit",
data: $('#editEventForm').serialize(),
success: function () {
},
error: function () {
}
});
So I add client side javascript for validation before submitting, but I still like the data annotations in the ViewModel as a fallback. When it reaches the controller, it is still checked with if (ModelState.IsValid)
. If this is not valid, the data is not written to the database in the same way as intended.
Now I want to know what can I do if ModelState is not , when posting with JQuery. This will not be checked regularly, 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 more information
I already have custom errors set in Web.config
<customErrors mode="On">
This directs errors to the Views / Shared / Error.cshtml file, where I output information about the error that was requested. Anyway, can a model state error (or any error) be sent here in the controller?
@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 snippets of all answers. In my controller, I put this in an else statement throw new HttpException(500, "ModelState Invalid");
(else - this means ModelState is invalid)
This is causing my custom errors in the Web.config to submit to Views / Shared / Error.cshtml (view), but it only appears in FireBug like this. The actual page isn't going anywhere. Any idea how I get this on the parent page? If that doesn't make sense, I used the setup described here here to send it to the custom error page. The fact that this is an AJAX call makes it work a little differently.
source to share
For my unique situation, I decided to take a different approach. Since I have custom errors set in the Web.config, I can automatically handle any non-Ajax request errors and pass that information to Views / Shared / Error.cshtml. A suggested reading for this is here .
I can also handle application errors outside of MVC using the methods described here .
I also installed ELMAH to log errors in my SQL database. Information about this is here and here .
My original intention was to catch errors in controllers in the same way when I submit an Ajax request, which I think you cannot do.
When I submit a form in my application, the form is first validated client-side with JavaScript validation. I know which fields I don't want to be empty, how many characters and what type of data to accept. If I find a field that does not match these criteria, I post the error information to the JQuery dialog to display this information to the user. If all of my criteria are met, I finally submit the form to the controller and action.
For this reason, I decided to throw an exception to the controller if an error occurs. If the client side validation detects no data transfer issues and the controller is still not working, then I definitely want this error to be logged. Keep in mind that I am also setting data annotations in the ViewModel as a fallback for client side validation. Throwing an exception will call the JQuery error function and call ELMAH to log this error.
My controller will 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 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 all posts. Thanks everyone for the help.
source to share
At the moment, your controller is just swallowing any errors - if the model is invalid, it just doesn't save and never gives feedback to the caller. You can fix this and return the error to it in jQuery, effectively returning an error:
return new HttpStatusCodeResult(HttpStatusCode.BadRequest, "Some meaningful message");
Lots of options about what you want to process and how many details to return, but most importantly, your controller must provide a response that matches the action it took.
UPDATE
In response to your "Update Again" section - I wouldn't return 500 - it means "internal server error". If you are going to return an error, 400 (bad query) is probably more appropriate. Whatever the problem with your ajax call is that it is receiving an error response from the web server (not your main browser window). If I had to guess, the error is handled on the server side and you jquery gets the html response from your custom error.
If you are going to leave automatic error handling in place, you should probably only use it for unhandled errors. So in your controller, you will be handling an invalid model by returning an error-free response indicating this state (I think someone else mentioned the json response). Then all responses will be successful, but the content will tell your application how to behave (redirect accordingly, etc.).
source to share
Since you want to return the error content, I would suggest returning a JSON response (alternative is a partial view, but that would mean your JS uses delegated handlers, reset form validation, etc.). In this case, you will need to detect and return JSON if POST is AJAX and return normal view / redirect otherwise. If all checks have to be done client-side, and it's okay for no error text to appear, you could probably return the result of the exception and use an error handler to call.ajax()
to refresh the page. I found that browser support for getting error response text is inconsistent, so if you want actual errors, you need to return a 200 OK response with messages in JSON. My choice will probably depend on the specific use case - for example, if there were multiple errors that I could only detect on the server side, I would probably use an OK response with the content of the error. If only a few or all of the errors need to be handled on the client side, then I would post an exception route.
A custom error handler should not be used or needed to do so.
MVC with result status
[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);
}
JS example with result status
$.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);
}
JS example 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.');
});
source to share
What you need is a small change in your code. Since you are using ajax communication to post data to the server, you do not need to use form posting. Instead, you can change the return type of your action method to JsonResult and use the Json () method to send the data processing result.
[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});
}
}
You can now use this action method to process data.
$.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 () {
}
});
source to share
You can submit an error message in the view bag 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
source to share