Running into some trouble with multiple forms on a single view.
Suppose I have the following viewmodel:
public class ChangeBankAccountViewModel
{
public IEnumerable<BankInfo> BankInfos { get; set; }
}
public class BankInfo
{
[Required]
public string BankAccount { get; set; }
public long Id { get; set; }
}
In my viewmodel, I want all BankInfos to be displayed underneath eachother, inside separate forms for each.
To achieve this, I'm using a partial view _EditBankInfo:
@model BankInfo
@using (Html.BeginForm())
{
@Html.HiddenFor(m => m.InvoiceStructureId)
@Html.TextBoxFor(m => m.IBANAccount)
<button type="submit">Update this stuff</button>
}
As well as my actual view BankInfo:
foreach(var info in Model.BankInfos)
{
Html.RenderPartial("_EditBankInfo", info);
}
Last, here are my 2 Action Methods:
[HttpGet]
public ActionResult BankInfo()
{
return View(new ChangeBankAccountViewModel{BankInfos = new [] {new BankInfo...});
}
[HttpPost]
public ActionResult BankInfo(BankInfo model)
{
if(ModelState.IsValid)
ModelState.Clear();
return BankInfo();
}
All of this is working hunky dory: Validation works smooth, posted model gets recognized and validated correctly... However, when the page reloads is when the problem arises. Because I'm using the same form multiple times, my ModelState will be applied multiple times. So when performing an update on one form, the next page load all of them will have the posted values.
Is there any way to easily prevent this from happening?
I've tried doing it without the partial views, but that screws up the naming a bit (they're unique, but serverside modelbinding won't recognize them).
Thanks for any answers.
This is a bit tricky. Here's how it can be solved. Start by moving your _EditBankInfo.cshtml
partial into an editor template ~/Views/Shared/EditorTemplates/BankInfo.cshtml
that looks like this (notice that the name and location of the template is important. It should be placed inside ~/Views/Shared/EditorTemplates
and named as the name of the typed used in your IEnumerable<T>
collection property, which in your case is BankInfo.cshtml
):
@model BankInfo
<div>
@using (Html.BeginForm())
{
<input type="hidden" name="model.prefix" value="@ViewData.TemplateInfo.HtmlFieldPrefix" />
@Html.HiddenFor(m => m.Id)
@Html.TextBoxFor(m => m.BankAccount)
<button type="submit">Update this stuff</button>
}
</div>
and then in your main view get rid of the foreach
loop and replace it with a simple call to the EditorFor
helper:
@model ChangeBankAccountViewModel
@Html.EditorFor(x => x.BankInfos)
Now for each element of the BankInfos
collection custom editor template will be rendered. And contrary to the partial, the editor template respects the navigational context and will generate the following markup:
<div>
<form action="/" method="post">
<input type="hidden" name="model.prefix" value="BankInfos[0]" />
<input data-val="true" data-val-number="The field Id must be a number." data-val-required="The Id field is required." id="BankInfos_0__Id" name="BankInfos[0].Id" type="hidden" value="1" />
<input data-val="true" data-val-required="The BankAccount field is required." id="BankInfos_0__BankAccount" name="BankInfos[0].BankAccount" type="text" value="account 1" />
<button type="submit">Update this stuff</button>
</form>
</div>
<div>
<form action="/" method="post">
<input type="hidden" name="model.prefix" value="BankInfos[1]" />
<input data-val="true" data-val-number="The field Id must be a number." data-val-required="The Id field is required." id="BankInfos_1__Id" name="BankInfos[1].Id" type="hidden" value="2" />
<input data-val="true" data-val-required="The BankAccount field is required." id="BankInfos_1__BankAccount" name="BankInfos[1].BankAccount" type="text" value="account 2" />
<button type="submit">Update this stuff</button>
</form>
</div>
...
Now since every field has a specific name there will no longer be any conflicts when posting the form. Notice the hidden field named model.prefix
that I explicitly placed inside each form. This will be used by a custom model binder for the BankInfo
type:
public class BankInfoModelBinder: DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
bindingContext.ModelName = controllerContext.HttpContext.Request.Form["model.prefix"];
return base.BindModel(controllerContext, bindingContext);
}
}
which will be registered in your Application_Start
:
ModelBinders.Binders.Add(typeof(BankInfo), new BankInfoModelBinder());
Alright, now we are good to go. Get rid of the ModelState.Clear
in your controller action as you no longer need it:
[HttpGet]
public ActionResult BankInfo()
{
var model = new ChangeBankAccountViewModel
{
// This is probably populated from some data store
BankInfos = new [] { new BankInfo... },
}
return View(model);
}
[HttpPost]
public ActionResult BankInfo(BankInfo model)
{
if(ModelState.IsValid)
{
// TODO: the model is valid => update its value into your data store
// DO NOT CALL ModelState.Clear anymore.
}
return BankInfo();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With