4

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

madvora
  • 1,717
  • 7
  • 34
  • 49
  • only thing i can think of would be to serialize the model errors and display them on `success` http://stackoverflow.com/questions/2845852/asp-net-mvc-how-to-convert-modelstate-errors-to-json – JamieD77 Jul 08 '15 at 19:15
  • I have had success in causing the controller action to return a response object that includes the HTML rendering of just the model editor template, and then having the client-side replace the form contents with the new editor HTML. This way, you can leverage MVC's built-in error displaying and editor templates, while still having your page behave in an AJAXy way. However, it is a fair bit of work to get it set up. – StriplingWarrior Jul 08 '15 at 19:20
  • Throwing a 500 response isn't really user friendly and is a bad implementation of error handling in this case. you need to return a message to the user letting them know that a validation error happened and probably include the messages. Check my answer below. – Khalid Jul 08 '15 at 20:58
  • Maybe I'm thinking of this wrong. Is there anything that can cause a model error, other than validation problems (the ones set by data annotation)? I'm already using javascript on the client side to notify the user. I was just thinking of catching any possible errors that could slip through at this point, since there was no else statement set up on the ModeState IsValid before. – madvora Jul 09 '15 at 01:31

6 Answers6

4

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...).

snow_FFFFFF
  • 3,235
  • 17
  • 29
  • Right, you understand the situation perfectly. If you manually call an error like this, then the JQuery AJax "error" function will pick this up and I can do something in there. Not sure if you saw the added part at the end, but is there a way I can get this routed to the error page the same way other errors would be? – madvora Jul 08 '15 at 19:21
  • Well, your controller could return the error view, but that isn't going to work with your jquery call - jquery will get a successful response with the content off the error page. I haven't used the customErrors before, so I could n't be sure without some experimentation. I would want jquery to get the error response and decide what to do - it might just show something on the page, or it could redirect to an error page. By having the controller return the error, you allow the consumer to decide. – snow_FFFFFF Jul 08 '15 at 19:26
  • This is all making sense. As I responded to Khalid above. I wanted to find out if anything other than validation can cause model errors that need to be caught. If it's just validation, then maybe these don't need to be caught this way. I think I need to experiment with something like a divide by zero error and see how that's caught and returned. – madvora Jul 09 '15 at 01:38
  • The modelstate is only going to reflect conflicts with your model (incorrect data types, missing required fields, etc...). If you controller code has any other errors (like your divide by zero) and you do not handle it, an exception will be thrown and you'll likely get a 500 response (this is now a real internal server error). – snow_FFFFFF Jul 09 '15 at 13:03
  • Thanks that helps. What I'm concerned with, (and you an ignore the validation part of this) is how to get any error in that controller to redirect to the error page. If I called that controller/action a non-ajax way, it would go to my View/Shared/Error.cshtml page, but with an AJAX call (as I posted in the image above) that response gets trapped in FireBug (for example) while the main page does nothing. – madvora Jul 09 '15 at 13:15
  • In my opinion, you don't want to return a view from an action that you will call with ajax. The content returned by the controller action will be received by your ajax call (it won't be received by the main browser window). So, your action could return success or failure and your jquery callback can redirect as appropriate. If you want to be able to call it and return a view, you either need two different actions or you need to provide input that directs it how to behave. – snow_FFFFFF Jul 09 '15 at 13:21
2

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.');     
});
tvanfosson
  • 524,688
  • 99
  • 697
  • 795
1

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.

Casey
  • 805
  • 4
  • 10
0

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 () {

}

});
XPD
  • 1,121
  • 1
  • 13
  • 26
0

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
Khalid
  • 225
  • 1
  • 5
0

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.

madvora
  • 1,717
  • 7
  • 34
  • 49