Несколько форм в представлении MVC: ModelState применяется ко всем формам

Выполнение некоторых проблем с несколькими формами на одном представлении.

Предположим, что у меня есть следующая модель просмотра:

public class ChangeBankAccountViewModel  
{  
     public IEnumerable<BankInfo> BankInfos { get; set; }  
}

public class BankInfo  
{  
    [Required]  
    public string BankAccount { get; set; }  
    public long Id { get; set; }  
}

В моей модели просмотра я хочу, чтобы все BankInfos отображались под eachother, внутри отдельных форм для каждого.

Чтобы достичь этого, я использую частичный вид _EditBankInfo:

@model BankInfo

@using (Html.BeginForm())
{
   @Html.HiddenFor(m => m.InvoiceStructureId)
   @Html.TextBoxFor(m => m.IBANAccount)

   <button type="submit">Update this stuff</button>
}

Как и мой фактический вид BankInfo:

foreach(var info in Model.BankInfos)
{
    Html.RenderPartial("_EditBankInfo", info);
}

Наконец, вот мои 2 метода действий:

[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();
}

Все это работает hunky dory: валидация работает гладко, размещенная модель распознается и проверяется правильно... Однако, когда перезагрузка страницы возникает, когда возникает проблема. Поскольку я использую одну и ту же форму несколько раз, мой ModelState будет применяться несколько раз. Поэтому при выполнении обновления в одной форме загрузка следующей страницы будет иметь все опубликованные значения.

Есть ли способ легко предотвратить это?

Я пробовал делать это без частичных представлений, но это немного сокращает наименования (они уникальны, но привязка к серверным файлам не распознает их).

Спасибо за любые ответы.

Ответ 1

Это немного сложно. Вот как это можно решить. Начните с перемещения части _EditBankInfo.cshtml в шаблон редактора ~/Views/Shared/EditorTemplates/BankInfo.cshtml, который выглядит так (обратите внимание, что имя и расположение шаблона важно. Оно должно быть помещено внутри ~/Views/Shared/EditorTemplates и названо именем имени, используемого в ваше свойство коллекции IEnumerable<T>, которое в вашем случае 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>

а затем в главном представлении избавиться от цикла foreach и заменить его простым вызовом помощнику EditorFor:

@model ChangeBankAccountViewModel

@Html.EditorFor(x => x.BankInfos)

Теперь для каждого элемента шаблона редактора шаблона BankInfos будет отображаться шаблон. И в отличие от частичного, шаблон редактора учитывает навигационный контекст и генерирует следующую разметку:

<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>

...

Теперь, поскольку у каждого поля есть определенное имя, больше не будет конфликтов при отправке формы. Обратите внимание на скрытое поле с именем model.prefix, которое я явно помещаю внутри каждой формы. Это будет использоваться специальным связующим устройством для типа BankInfo:

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);
    }
}

который будет зарегистрирован в вашем Application_Start:

ModelBinders.Binders.Add(typeof(BankInfo), new BankInfoModelBinder());

Хорошо, теперь нам хорошо идти. Избавьтесь от ModelState.Clear в действии вашего контроллера, поскольку он вам больше не нужен:

[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();
}