MVC 4 Validation with a partial view

2019-02-14 07:16发布

问题:

I'm using MVC 4 and Entity Framework to develop a web app. I'm working with partial views which are loaded with javascript. One of them is a create view which includes validation. And that's my problem : the validation. I have a custom validation logic and, for example, if a user enters some numbers into a field such as "Name", it displays an error.

Here, with the partial views, it redirects me on my partial with the errors displayed but what I wanted to do is to stay on my main view (Index view) and keep my partial view which displays the errors.

EDIT :

Here is my partial view :

@model BuSIMaterial.Models.Person

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>Person</legend>

        <div class="editor-label">
            First name : 
        </div>
        <div class="editor-field">
            @Html.TextBoxFor(model => model.FirstName, new { maxlength = 50 })
            @Html.ValidationMessageFor(model => model.FirstName)
        </div>

        <div class="editor-label">
            Last name : 
        </div>
        <div class="editor-field">
            @Html.TextBoxFor(model => model.LastName, new { maxlength = 50 })
            @Html.ValidationMessageFor(model => model.LastName)
        </div>

        <div class="editor-label">
            National number : 
        </div>
        <div class="editor-field">
            @Html.TextBoxFor(model => model.NumNat, new { maxlength = 11 })
            @Html.ValidationMessageFor(model => model.NumNat)
        </div>

        <div class="editor-label">
            Start date : 
        </div>
        <div class="editor-field">
            @Html.TextBoxFor(model => model.StartDate, new {@class = "datepicker", @placeholder="yyyy/mm/dd"})
            @Html.ValidationMessageFor(model => model.StartDate)
        </div>

        <div class="editor-label">
            End date : 
        </div>
        <div class="editor-field">
            @Html.TextBoxFor(model => model.EndDate, new { @class = "datepicker", @placeholder = "yyyy/mm/dd" })
            @Html.ValidationMessageFor(model => model.EndDate)
        </div>

        <div class="editor-label">
            Distance House - Work (km) : 
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.HouseToWorkKilometers)
            @Html.ValidationMessageFor(model => model.HouseToWorkKilometers)
        </div>

        <div class="editor-label">
            Category : 
        </div>
        <div class="editor-field">
            @Html.DropDownList("Id_ProductPackageCategory", "Choose one ...")
            @Html.ValidationMessageFor(model => model.Id_ProductPackageCategory) <a href = "../ProductPackageCategory/Create">Add a new category?</a>
        </div>

        <div class="editor-label">
            Upgrade? : 
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Upgrade)
            @Html.ValidationMessageFor(model => model.Upgrade)
        </div>

        <br />

        <div class="form-actions">
          <button type="submit" class="btn btn-primary">Create</button>
        </div>
    </fieldset>
}


@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
    @Scripts.Render("~/bundles/jqueryui")
    @Styles.Render("~/Content/themes/base/css")
}

In my view Index, I have this :

<div class="form-actions"><button type="button" id="create" class="btn btn-primary">Create</button> </div>
<div id ="create_person"></div>

And the way I load my Partial View :

            $("#create").click(function () {
                var form = $("#create_person").closest("form");
                form.removeData('validator');
                form.removeData('unobtrusiveValidation');
                $.validator.unobtrusive.parse(form);

                $.ajax({
                    url: "/Person/CreateOrUpdate",
                    type: "POST",
                    data: $("#create_person").serialize(),
                    cache: false
                });

//                var url = '/Person/CreatePerson';
//                $("#create_person").load(url);

            });

The actions :

[HttpGet]
        public ActionResult CreateOrUpdate()
        {
            ViewBag.Id_ProductPackageCategory = new SelectList(db.ProductPackageCategories, "Id_ProductPackageCategory", "Name");
            return View();
        }


        [HttpPost]
        public JsonResult CreateOrUpdate(Person person)
        {
            ViewBag.Id_ProductPackageCategory = new SelectList(db.ProductPackageCategories, "Id_ProductPackageCategory", "Name", person.Id_ProductPackageCategory);

            try
            {
                if (!ModelState.IsValid)
                {
                    string messages = string.Join("; ", ModelState.Values
                                        .SelectMany(x => x.Errors)
                                        .Select(x => x.ErrorMessage));
                    throw new Exception("Please correct the following errors: " + Environment.NewLine + messages);
                }

                db.Persons.AddObject(person);
                db.SaveChanges();

                return Json(new { Result = "OK" });
            }
            catch (Exception ex)
            {
                return Json(new { Result = "ERROR", Message = ex.Message });
            }
        }

回答1:

If you post the page it will not come back to the dynamically loaded partial view. Try to make a ajax call to /Person/CreatePerson. Your CreatePerson will look similar to

[HttpPost]
    public JsonResult CreatePerson(Person person)
    {
        ViewBag.Id_ProductPackageCategory = new SelectList(db.ProductPackageCategories, "Id_ProductPackageCategory", "Name", person.Id_ProductPackageCategory);

    try
    {
        if (!ModelState.IsValid)
        {
            string messages = string.Join("; ", ModelState.Values
                                .SelectMany(x => x.Errors)
                                .Select(x => x.ErrorMessage));
            throw new Exception("Please correct the following errors: " + Environment.NewLine + messages);
        }

        db.Persons.AddObject(person);
        db.SaveChanges();

        return Json(new { Result = "OK" });
    }
    catch (Exception ex)
    {
        return Json(new { Result = "ERROR", Message = ex.Message });
    }
}                                                                                                    `

The ajax call to /Person/CreatePerson will look similar to

`

$.ajax({
                url: '/Person/CreatePerson',
                type: "POST",
                data: $("#form").serialize(),
                success: function (responce) {
                    alert(responce.Message);
                },
                error: function (xhr, textStatus) {
                    alert(xhr.status + " " + xhr.statusText);
                }
            });

Besides unobtrusive validation will not work easily with dynamic content. check the link unobtrusive validation on dynamically added partial view (not working)



回答2:

I came across this scenario whereby my main View uses Ajax posts to retrieve PartialViews and delivers the markup of the PartialView into my View. I use the jQuery validation framework to implement client side unobtrusive validation. When I tried to validate my form that had been loaded into the page via an Ajax call, the validation did not appear to be working, my form simply posted. As it turns out, the unobtrusive jQuery validator library parses the markup of a page after it has finished loading for all the validation rules on the page. The result is, jQuery doesn’t know that the dynamically added markup has any validation rules resulting in no validation errors being thrown on post.

Solution

Traffy there is a method that is publicly accessible in the unobtrusive jQuery library which you can call, which will force the parser to scan the markup you just loaded. To do this you need to add the following code to the PartialView (or other dynamically loaded content)

//this code goes in your partial view
$(function(){ 
  //allow the validation framework to re-prase the DOM
  jQuery.validator.unobtrusive.parse(); 

  //or to give the parser some context, supply it with a selector
  //jQuery validator will parse all child elements (deep) starting
  //from the selector element supplied
  jQuery.validator.unobtrusive.parse("#formId");
});

Then in your parent page you can handle the form submission and check the form is valid like this;

//then in your parent page handle the form submission
$(function(){
  $("#SubmitButton").click(function(){ 
    if (!$("#Form1").valid()){ 
      return false; 
    } 
  });
});

And that is it!! (Credit: Dave's coding)



回答3:

I've developed a decent workaround for this. The partial page won't show the server errors on postback. First of all, we get the errors, send them back to the page, then create them in javascript & revalidate the page. Hopefully, hey presto!

In your controller:

        if (ModelState.IsValid)
        {
            //... whatever code you need in here
        }            
        var list = ModelStateHelper.AllErrors(ModelState);
        TempData["shepherdErrors"] = list;

I put it in TempData so it can be retrieve easily from the partial. Yes, I called it shepherdErrors, it's my idea so I can call the concept whatever silly name I want! Shepherd the error codes to where they should be or something being the general idea.

In a helper class:

public class ModelStateHelper
{
    public static IEnumerable<KeyValuePair<string, string>> 
         AllErrors(ModelStateDictionary modelState)
    {
        var result = new List<KeyValuePair<string, string>>();
        var erroneousFields = modelState.Where(ms => ms.Value.Errors.Any())
                                        .Select(x => new { x.Key, x.Value.Errors });

        foreach (var erroneousField in erroneousFields)
        {
            var fieldKey = erroneousField.Key;
            var fieldErrors = erroneousField.Errors
                               .Select(error => new KeyValuePair<string, string>(fieldKey, error.ErrorMessage)); //Error(fieldKey, error.ErrorMessage));
            result.AddRange(fieldErrors);
        }

        return result;
    }
}

Then on the html page somewhere after jquery being loaded:

function displayShepherdErrors() {
    var shepherdErrors = JSON.parse('@(Newtonsoft.Json.JsonConvert.SerializeObject(TempData["shepherdErrors"]))'.replace(/&quot;/g, '"'));
    var frm;
    var isShepherdErrors = (shepherdErrors && shepherdErrors.length > 0);
    if (isShepherdErrors) {
        errObj = {};
        for (var i = 0; i < shepherdErrors.length; i++) {
            var errorKey = shepherdErrors[i].Key;   //also the id of the field
            var errorMsg = shepherdErrors[i].Value;
            var reg = new RegExp('^' + errorKey + '$', 'gi');
            //find the selector - we use filter so we can find it case insensitive
            var control = $('input').filter(function () {
                if ($(this).attr('id'))
                    return $(this).attr('id').match(reg);
            });

            if (control && control.length) {
                control = control[0];
                var controlId = $(control).attr('name');
                errObj[controlId] = errorMsg;

                //get the containing form of the first input element
                if (!frm)
                    frm = control.form;
            }
        }
        var validator = $(frm).validate();
        validator.showErrors(errObj);
    }
    return isShepherdErrors;
}

var isShepherdErrors = displayShepherdErrors();

This should work out of the box with general MVC development provided text boxes that are generated are based on the variable names - this is default behaviour of MVC.