5

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 });
            }
        }
Traffy
  • 2,803
  • 15
  • 52
  • 78
  • This should be in the partial view replacing the
    – Faisal Ahmed Apr 16 '13 at 09:01
  • So I have to load my Partial View as I did (with .load) and make an ajax call when I click on the create button of my partial view? – Traffy Apr 16 '13 at 09:04
  • @Traffy- yes . u r right. By the way are u using jQuery dialog? – Faisal Ahmed Apr 16 '13 at 09:08
  • Thanks a lot. A last question while I'm testing it : the ajax call should be placed in my partial view or the main view? No, i'm not using jQuery dialog, I think about it but it seems really complicated and I'm new with these technologies. – Traffy Apr 16 '13 at 09:09
  • @Traffy- put the ajax call in a java script file and load the js file when the index view is loaded. – Faisal Ahmed Apr 16 '13 at 09:22

2 Answers2

2

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)

Community
  • 1
  • 1
Faisal Ahmed
  • 208
  • 1
  • 10
0

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.

ShrapNull
  • 1,090
  • 1
  • 9
  • 15