How to read modelstate errors when returned by Json?
Asked Answered
O

6

16

How can I display ModelState errors returned by JSON?

I want to do something like this:

 if (!ValidateLogOn(Name, currentPassword))
    {
        ModelState.AddModelError("_FORM", "Username or password is incorrect.");

        //Return a json object to the javascript
        return Json(new { ModelState });
    }

What must be my code in the view to read the ModelState errors and display them?

My actual code in the view to read the JSON values is as follows:

function createCategoryComplete(e) { 
    var obj = e.get_object(); 
    alert(obj.Values); 
} 
Orrin answered 11/5, 2010 at 5:38 Comment(1)
There is a solution here: #2846352Abshire
A
-10

If you are returning JSON, you cannot use ModelState. Everything that the view needs should be contained inside the JSON string. So instead of adding the error to the ModelState you could add it to the model you are serializing:

public ActionResult Index()
{
    return Json(new 
    {
        errorControl = "_FORM",
        errorMessage = "Username or password is incorrect.",
        someOtherProperty = "some other value"
    });
}
Azores answered 11/5, 2010 at 6:21 Comment(2)
Don't see why ModelState can't be used. E.g. model binders can put errors to ModelState, action filters, etc; if we don't use ModelState user won't even know about them.Zebu
ModelState doesn't serialize. I got a circulate reference error.Ferryman
Z
47

This is draft code but the same idea works for me in production. The main idea here is that Json errors have predefined tag names, that no normal objects will have. For errors validation errors HTML is re-created using JavaScript (both top summary and form elements highlighting).

Server side:

  public static JsonResult JsonValidation(this ModelStateDictionary state)
  {
     return new JsonResult
     {
        Data = new
           {
              Tag = "ValidationError",
              State = from e in state
                      where e.Value.Errors.Count > 0
                      select new
                      {
                         Name = e.Key,
                         Errors = e.Value.Errors.Select(x => x.ErrorMessage)
                            .Concat(e.Value.Errors.Where(x => x.Exception != null).Select(x => x.Exception.Message))
                      }
           }
     };
  }

  in action:
  if (!ModelState.IsValid && Request.IsAjaxRequest())
      return ModelState.JsonValidation();

Client side:

function getValidationSummary() {
   var el = $(".validation-summary-errors");
   if (el.length == 0) {
      $(".title-separator").after("<div><ul class='validation-summary-errors ui-state-error'></ul></div>");
      el = $(".validation-summary-errors");
   }
   return el;
}

function getResponseValidationObject(response) {
   if (response && response.Tag && response.Tag == "ValidationError")
      return response;
   return null;
}

function CheckValidationErrorResponse(response, form, summaryElement) {
   var data = getResponseValidationObject(response);
   if (!data) return;

   var list = summaryElement || getValidationSummary();
   list.html('');
   $.each(data.State, function(i, item) {
      list.append("<li>" + item.Errors.join("</li><li>") + "</li>");
      if (form && item.Name.length > 0)
         $(form).find("*[name='" + item.Name + "']").addClass("ui-state-error");
   });
}

$.ajax(... function(response) { 
   CheckValidationErrorResponse(xhr.responseText); } );
Zebu answered 11/5, 2010 at 7:59 Comment(3)
Nicely done! The code you provide is extremely portable too. The only thing I needed to change was "$(".title-separator")" to match my DOM.Fiendish
what do you set to: summaryElement ?? i don't see that anywhere in your code.Weald
summaryElement is passed to CheckValidationErrorResponse if I want to display errors in non-default location - e.g. in jqGrid edit form. This is a jquery element/selector of the ul/div/etc whose content will be replaced with li tags with errors. In my code above you would pass it CheckValidationErrorResponse in the $.ajax callback.Zebu
I
3

Why not return the original ModelState object to the client, and then use jQuery to read the values. To me it looks much simpler, and uses the common data structure (.net's ModelState)

C#:

return Json(ModelState);

js:

var message = "";
if (e.response.length > 0) {
    $.each(e.response, function(i, fieldItem) {
        $.each(fieldItem.Value.Errors, function(j, errItem) {
            message += errItem.ErrorMessage;
        });
        message += "\n";
    });
    alert(message);
}
Incrust answered 20/9, 2013 at 12:13 Comment(0)
H
2

this is a tiny tweak to queen3's client side code which handles specific validation messages, and creates a similar document to that created by MVC3:

function getValidationSummary() {
   var $el = $(".validation-summary-errors > ul");
   if ($el.length == 0) {
       $el = $("<div class='validation-summary-errors'><ul></ul></div>")
                .hide()
                .insertBefore('fieldset:first')
                .find('ul');
   } 
   return $el;
}
function getResponseValidationObject(response) {
   if (response && response.Tag && response.Tag == "ValidationError")
      return response;
   return null;
}
function isValidationErrorResponse(response, form, summaryElement) {
    var $list,
        data = getResponseValidationObject(response);
    if (!data) return false;
    $list = summaryElement || getValidationSummary();
    $list.html('');
    $.each(data.State, function (i, item) {
        var $val, lblTxt, errorList ="";
        if (item.Name) {
            $val = $(".field-validation-valid,.field-validation-error")
                        .first("[data-valmsg-for=" + item.Name + "]")
                        .removeClass("field-validation-valid")
                        .addClass("field-validation-error");
            $("input[name=" + item.Name + "]").addClass("input-validation-error")
            lblTxt = $("label[for=" + item.Name + "]").text();
            if (lblTxt) { lblTxt += ": "; }
        }
        if ($val.length) {
            $val.text(item.Errors.shift());
            if (!item.Errors.length) { return; }
        }
        $.each(item.Errors, function (c,val) {
            errorList += "<li>" + lblTxt + val + "</li>";
        });
        $list.append(errorList);
    });
    if ($list.find("li:first").length) {$list.closest("div").show(); }
    return true;
}
Hafiz answered 29/8, 2012 at 10:46 Comment(1)
Thanks Brent. Couple of suggestions - $.first() doesn't accept arguments, should it be $.filter()? Then $("input[name=...]") excludes select, textarea, etc - could be changed to $("*[name=...]")?Cretin
D
2

C#

 public class ValidateModelAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            if (actionContext.ModelState.IsValid == false)
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(
                    HttpStatusCode.BadRequest, actionContext.ModelState);
            }
        }
    }

JavaScript

$.ajax({
        type: "GET",
        url: "/api/xxxxx",
        async: 'false',
        error: function (xhr, status, err) {
            if (xhr.status == 400) {
                DisplayModelStateErrors(xhr.responseJSON.ModelState);
            }
        },
....


function DisplayModelStateErrors(modelState) {
    var message = "";
    var propStrings = Object.keys(modelState);

    $.each(propStrings, function (i, propString) {
        var propErrors = modelState[propString];
        $.each(propErrors, function (j, propError) {
            message += propError;
        });
        message += "\n";
    });

    alert(message);
};
Disable answered 6/9, 2016 at 10:0 Comment(0)
C
1

See below for code with a few amendments to Brent's answer. CheckValidationErrorResponse looks for the Validation Summary regardless of whether it's in the valid or invalid state, and inserts it if not found. If validation errors are found in the response, it applies the validation-summary-errors class to the Summary, else it applies validation-summary-valid. It assumes CSS is present to control the visibility of the Summary.

The code clears existing instances of field-validation-error, and reapplies them for errors found in the response.

function getValidationSummary(form) {
    var $summ = $(form).find('*[data-valmsg-summary="true"]');

    if ($summ.length == 0)
    {
    $summ = $('<div class="validation-summary-valid" data-valmsg-summary="true"><ul></ul></div>');
        $summ.appendTo(form);
    }

    return $summ;
}

function getValidationList(summary) {
    var $list = $(summary).children('ul');

    if ($list.length == 0) {
        $list = $('<ul></ul>');
        $list.appendTo(summary);
    }

    return $list;
}

function getResponseValidationErrors(data) {
    if (data && data.ModelErrors && data.ModelErrors.length > 0)
        return data.ModelErrors;
    return null;
}

function CheckValidationErrorResponse(data, form, summaryElement) {
    var errors = getResponseValidationErrors(data);
    var $summ = summaryElement || getValidationSummary(form);
    var $list = getValidationList($summ);

    $list.html('');

    $(form).find(".field-validation-error")
           .removeClass("field-validation-error")
           .addClass("field-validation-valid");

    if (!errors)
    {
        $summ.removeClass('validation-summary-errors').addClass('validation-summary-valid');
        return false;
    }

    $.each(errors, function (i, item) {
        var $val, $input, errorList = "";
        if (item.Name) {
            $val = $(form).find(".field-validation-valid, .field-validation-error")
                          .filter("[data-valmsg-for=" + item.Name + "]")
                          .removeClass("field-validation-valid")
                          .addClass("field-validation-error");

            $input = $(form).find("*[name='" + item.Name + "']");

            if (!$input.is(":hidden") && !$val.length)
            {
                $input.parent().append("<span class='field-validation-error' data-valmsg-for='" + item.Name + "' data-valmsg-replace='false'>*</span>");
            }

            $input.addClass("input-validation-error");
        }

        $.each(item.Errors, function (c, err) {
            errorList += "<li>" + err + "</li>";
        });

        $list.append(errorList);
    });

    $summ.removeClass('validation-summary-valid').addClass('validation-summary-errors');
    return true;
}
Cretin answered 24/5, 2013 at 11:5 Comment(0)
A
-10

If you are returning JSON, you cannot use ModelState. Everything that the view needs should be contained inside the JSON string. So instead of adding the error to the ModelState you could add it to the model you are serializing:

public ActionResult Index()
{
    return Json(new 
    {
        errorControl = "_FORM",
        errorMessage = "Username or password is incorrect.",
        someOtherProperty = "some other value"
    });
}
Azores answered 11/5, 2010 at 6:21 Comment(2)
Don't see why ModelState can't be used. E.g. model binders can put errors to ModelState, action filters, etc; if we don't use ModelState user won't even know about them.Zebu
ModelState doesn't serialize. I got a circulate reference error.Ferryman

© 2022 - 2024 — McMap. All rights reserved.