Несколько форм в представлении 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 отображались друг под другом, внутри отдельных форм для каждого.

Для этого я использую частичное представление _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);
}

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

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

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

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

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

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


person Kippie    schedule 07.08.2013    source источник
comment
Не могли бы вы показать действие контроллера, которому отправляется форма? Меня особенно интересует модель, которую он принимает в качестве параметра, и модель, которую он передает в представление.   -  person Darin Dimitrov    schedule 07.08.2013
comment
@DarinDimitrov, добавил их. Знайте, что это упрощенный пример, но базовая настройка должна быть там. Кроме того, я, вероятно, использовал бы здесь какой-то сценарий PRG.   -  person Kippie    schedule 07.08.2013


Ответы (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();
}
person Darin Dimitrov    schedule 07.08.2013
comment
Большое спасибо, Дарин. Замечательный пример, который научил меня нескольким трюкам (кто знал, что EditorFor допускает Enumerable коллекции ??). Просто есть небольшая проблема с выяснением префикса на случай, если мне нужно вручную добавить ошибку модели в мое состояние модели, но я уверен, что я это выясню. - person Kippie; 07.08.2013