4

I have a scenario in which I would like to display data to users and allow them to be able to modify it. To do this, I have a form which is preset with the current object values. This form is enclosed as a partial view which is in turn called in a loop depending on how many child objects my parent object has.

I also provide an empty form at the end to allow users to create new instances of this child item, so in short I have something like so:

Edit Form (a partial view):

@model WebPortal.Models.AddressModel

@using (Html.BeginForm("EditAddress", "MerchantDetailsEditor", FormMethod.Post))
{
    @Html.ValidationSummary(true)

    @Html.LabelFor(model => model.StreetNumber, new { id = Model.ID + "_streetnumber_label", Name = Model.ID + "_streetnumber_label", @for = Model.ID + "_streetnumber" })
    @Html.TextBoxFor(address => address.StreetNumber, new { id = Model.ID + "_streetnumber", Name = Model.ID + "_streetnumber"})
    @Html.ValidationMessageFor(address => address.StreetNumber)

Create Form (another partial view)

@model WebPortal.Models.AddressModel
@{
    int counter = 1;
}

@using (Html.BeginForm("AddAddress", "MerchantDetailsEditor", FormMethod.Post))
{
    @Html.ValidationSummary(true)

    @Html.LabelFor(model => model.StreetNumber, new { id = counter + "_streetnumber_label", Name = counter + "_streetnumber_label", @for = counter + "_streetnumber" })
    @Html.TextBoxFor(address => address.StreetNumber, new { id = counter + "_streetnumber", Name = counter + "_streetnumber"})
    @Html.ValidationMessageFor(address => address.StreetNumber)
    counter++;

Which are in turn called like so:

@foreach (WebPortal.Models.AddressModel address in @Model.Addresses)
{
    @Html.Partial("Edit_Form", address)
}
@Html.Partial("Add_Form", new WebPortal.Models.AddressModel())

The problem I am facing is that since they are all sharing the same model, the validation errors are being applied and shown next to all instances I have displayed. So if I where to edit an object, I would get errors because the other form (the add form) was empty.

I have read that this is due to the fact that MVC applies the component ID's at a view level, so since I am using multiple instances of the same view, they are all getting the same ID's.

To go round this I overrode the properties for the ID and Name, but to no avail.

Essentially, I just want the error messages to be shown next to the appropriate form. I am fairly green on this type of approach, so if it is incorrect please do let me know. My objective is to allow users to View/Edit and Create data on a particular screen.

Thanks!

Sankar V
  • 4,794
  • 3
  • 38
  • 56
npinti
  • 51,780
  • 5
  • 72
  • 96

2 Answers2

0

Do not generate the values of name and id attributes yourself. MVC can do that if you either use:

  1. Editor templates or
  2. Passing in HtmlFieldPrefix in to your partial views See this blog for more details

The solution that i'm going to show you is for editor templates.

AddressModel.cshtml (editor template)

@model WebPortal.Models.AddressModel

@Html.LabelFor(model => model.StreetNumber)
@Html.TextBoxFor(address => address.StreetNumber)
@Html.ValidationMessageFor(address => address.StreetNumber)

View

@for (int i = 0; i < Model.Addresses.Count; i++)
{
    using (Html.BeginForm("EditAddress", "MerchantDetailsEditor"))
    {
        @Html.ValidationSummary(true)    
        <input type="hidden" name="Addresses.Index" value="@i" /> //http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/
        @Html.EditorFor(m => m.Addresses[i]) //uses AddressModel.cshtml
        <input type="submit" value="Edit" />
    }
}

@using (Html.BeginForm("AddAddress", "MerchantDetailsEditor"))
{
    @Html.ValidationSummary(true)
    @Html.EditorFor(m => m.AddAddress)
    <input type="submit" value="Add" />
}

Controller

    [HttpPost]
    public ActionResult EditAddress([Bind(Prefix="Addresses")] AddressModel[] i)
    {
         //learn more about BindAttribute here
         http://stackoverflow.com/questions/1317523/how-to-use-bind-prefix
    }

    [HttpPost]
    public ActionResult AddAddress([Bind(Prefix = "AddAddress")] AddressModel i)
    {

    }

Generated HTML

Notice that the name and id attributes has a unique numerical index which is 0 in this sample

<form action="/Home/EditAddress" method="post">
  <input type="hidden" name="Addresses.Index" value="0" />
  <label for="Addresses_0__StreetNumber">Street Number</label>
  <input data-val="true" data-val-required="The Street Number field is required." id="Addresses_0__StreetNumber" name="Addresses[0].StreetNumber" type="text" value="" />
  <span class="field-validation-valid" data-valmsg-for="Addresses[0].StreetNumber" data-valmsg-replace="true"></span>
  <input type="submit" value="Edit" />
</form>
LostInComputer
  • 15,188
  • 4
  • 41
  • 49
  • I was unable to test your approach since I had already fixed most of the problem. That being said, I think it is a good idea to leave your answer here so that it can be of help to any person which will encounter this problem in the future. – npinti Apr 09 '14 at 04:36
0

I eventually managed to get this working. After coming across this previous SO question, I opted to use Actions instead. So the code now looks something like so:

EDIT Form (Still a partial view)

@model WebPortal.Models.AddressModel


@using (Html.BeginForm("EditAddress", "MerchantDetailsEditor", FormMethod.Post))
{
    @Html.ValidationSummary(true)

    @Html.HiddenFor(model => model.ID)

    @Html.LabelFor(model => model.StreetNumber, new { id = Model.ID + "_streetnumber_label" })
    @Html.TextBoxFor(address => address.StreetNumber, new { id = Model.ID + "_streetnumber" })
    @Html.ValidationMessageFor(address => address.StreetNumber)
 ...

Create Form (Still a partial view)

@model WebPortal.Models.AddressModel
@{
    int counter = 1;
}

@using (Html.BeginForm("AddAddress", "MerchantDetailsEditor", FormMethod.Post))
{
    @Html.ValidationSummary(true)

    @Html.LabelFor(model => model.StreetNumber, new { id = counter + "_streetnumber_label" })
    @Html.TextBoxFor(address => address.StreetNumber, new { id = counter + "_streetnumber"})
    @Html.ValidationMessageFor(address => address.StreetNumber)
 ...

The major change is what I did next:

<h2>Addresses</h2>
@foreach (WebPortal.Models.AddressModel address in @Model.Addresses)
{
    @Html.Action("_AddressEdit", address)
}

@if (ViewBag.AddressToAdd != null)
{
    @Html.Action("_AddressAdd", (WebPortal.Models.AddressModel)ViewBag.AddressToAdd)
}
else
{
    @Html.Action("_AddressAddNew")
}

I essentially provided an action per instance of my partial view. This allowed me to go round the problem exposed in the link in my answer.

My controller now looks like so:

 #region Actions
    public ActionResult _AddressEdit(AddressModel addressModel)
    {
        return View("...", addressModel);
    }

    public ActionResult _AddressAdd(AddressModel addressModel)
    {
        return View("...", addressModel);
    }

    public ActionResult _AddressAddNew()
    {
        return View("...");
    }
    #endregion Actions
Community
  • 1
  • 1
npinti
  • 51,780
  • 5
  • 72
  • 96