Multiple forms in MVC view: ModelState applied to

2019-03-12 23:38发布

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.

1条回答
【Aperson】
2楼-- · 2019-03-12 23:57

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();
}
查看更多
登录 后发表回答