Связывание модели при представлении нескольких форм модели из строго типизированного представления

У меня возникают проблемы с привязкой к форме с несколькими представленными моделями. У меня есть форма жалобы, которая включает в себя информацию о жалобе, а также жалобы "один ко многим". Я пытаюсь отправить форму, но я получаю ошибки в привязке. ModelState.IsValid всегда возвращает false.

Если я отлаживаю и просматриваю ошибки ModelState, я получаю одно высказывание: "EntityCollection уже инициализирован. Метод InitializeRelatedCollection должен быть вызван только для инициализации нового EntityCollection во время десериализации графа объектов".

Кроме того, при отладке я вижу, что модель жалобы заполняется истцами из представления формы, поэтому кажется, что часть работает.

Я не уверен, что то, что я делаю, невозможно с помощью ModelBinder по умолчанию, или если я просто не буду делать это правильно. Я не могу найти конкретных примеров или документации по этому поводу. Очень похожую проблему можно найти в stackoverflow здесь, но, похоже, это не касается сильно типизированных представлений.

Код контроллера:

    public ActionResult Edit(int id)
    {
        var complaint = (from c in _entities.ComplaintSet.Include("Complainants")
                     where c.Id == id
                     select c).FirstOrDefault();

        return View(complaint);
    }

    //
    // POST: /Home/Edit/5
    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(Complaint complaint)
    {
        if (!ModelState.IsValid)
        {
            return View();
        }
        try
        {
            var originalComplaint = (from c in _entities.ComplaintSet.Include("Complainants")
                                     where c.Id == complaint.Id
                                     select c).FirstOrDefault();
            _entities.ApplyPropertyChanges(originalComplaint.EntityKey.EntitySetName, complaint);
            _entities.SaveChanges();
            return RedirectToAction("Index");
        }
        catch
        {
            return View();
        }
    }

Просмотреть код (это частичное представление, которое вызывается Create/Edit Views, которое также строго типизировано с жалобой):

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ProStand.Models.Complaint>" %>

<%= Html.ValidationSummary() %>
<% using (Html.BeginForm()) {%>

<table cellpadding="0" cellspacing="0" class="table">
    <tr>
        <td>
        <label for="DateReceived">Date Received:</label>
            <%= Html.TextBox("DateReceived") %>
            <%= Html.ValidationMessage("DateReceived", "*") %>    
        </td>
        <td>
            <label for="DateEntered">Date Entered:</label>
            <%= Html.TextBox("DateEntered")%>
            <%= Html.ValidationMessage("DateEntered", "*") %>
        </td>
    </tr>
    <tr>
        <td>
            <label for="Concluded">Concluded:</label>
            <%= Html.CheckBox("Concluded")%>
            <%= Html.ValidationMessage("Concluded", "*") %>
            </td>
        <td>
            <label for="IncidentDate">Incident Date:</label>
            <%= Html.TextBox("IncidentDate")%>
            <%= Html.ValidationMessage("IncidentDate", "*") %></td>
    </tr>
</table>
    <hr />
    <table>
    <% if (Model != null) {
           int i = 0;
       foreach (var complainant in Model.Complainants){ %>
       <%= Html.Hidden("Complainants[" + i + "].Id", complainant.Id)%>
    <tr>
        <td>
            <label for="Surname">Surname:</label>

            <%= Html.TextBox("Complainants[" + i + "].Surname", complainant.Surname)%>
            <%= Html.ValidationMessage("Surname", "*")%>
        </td>
        <td>
            <label for="GivenName1">GivenName1:</label>
            <%= Html.TextBox("Complainants[" + i + "].GivenName1", complainant.GivenName1)%>
            <%= Html.ValidationMessage("GivenName1", "*")%>
        </td>
    </tr>
    <% i++; %>
    <% }} %>
    <tr>
        <td colspan=2>
            <input type="submit" value="Submit" />
        </td>
    </tr>
</table>
<% } %>
<div>
    <%=Html.ActionLink("Back to List", "Index") %>
</div>

Ответ 1

Слепое предположение:

изменения:

<%= Html.TextBox("Complainants[" + i + "].Surname", complainant.Surname)%>

с:

<%= Html.TextBox("Complaint.Complainants[" + i + "].Surname",  
complainant.Surname)%>

Соответственно - добавьте "Жалоба". перед "Истцами [..."

ИЗМЕНИТЬ

Это НЕ правильный ответ. Оставленный он был восстановлен только потому, что это может добавить некоторое значение, пока не появится правильный ответ.

EDIT2:

Возможно, я ошибаюсь, но для меня, похоже, проблема с инфраструктурой сущности (или - с тем, как вы ее используете). Я имею в виду - asp.net mvc удается считывать значения из запроса, но не может инициализировать сбор жалоб.

Здесь написано:

Метод InitializeRelatedCollection (TTargetEntity) инициализирует существующий EntityCollection (TEntity), который был создан с использованием конструктора по умолчанию. EntityCollection (TEntity) инициализируется с использованием назначенных отношений и имен целевой роли.

Метод InitializeRelatedCollection (TTargetEntity) используется только при десериализации.

Дополнительная информация:

Исключение:

  • InvalidOperationException

Условия:

  • Когда предоставленный EntityCollection (TEntity) уже инициализирован.
  • Когда менеджер отношений уже привязан к ObjectContext.
  • Когда менеджер отношений уже содержит отношения с этим именем и целевой ролью.

В некоторых случаях InitializeRelatedCollection запускается дважды. К несчастью - у меня нет ярких идей, почему именно. Возможно, это небольшое расследование поможет кому-то другому - более опытен с EF.:)

EDIT3:
Это не решение для этой конкретной проблемы, больше похоже на обходной путь, правильный способ обработки модельной части mvc.

Создайте viewmodel только для целей презентации. Создайте новую модель домена из чистых POCOs (потому что EF будет поддерживать их только в следующей версии). Используйте AutoMapper для сопоставления EFDataContext <= > Model <= > ViewModel.

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

Ответ 2

public ActionResult Edit([Bind(Exclude = "Complainants")]Complaint model)
{
  TryUpdateModel(model.Complainants, "Complainants");
  if (!ModelState.IsValid)
  {
      // return the pre populated model
      return View(model);
  }

}

Это работает для меня!

Я думаю, что когда объект Жалобы создается, его коллекция "Истребителей" инициализируется (из-за автоматической логики сущности), а затем модельное связующее пытается также создать собственную коллекцию, что вызывает ошибку. Но когда мы пытаемся обновить модель вручную, коллекция уже инициализируется, но в качестве связующего не требуется инициализировать ее снова.

Ответ 3

Чтобы заставить это работать без обходных решений, вам необходимо создать собственный метод привязки модели и переопределить метод SetProperty:

public class MyDefaultModelBinder : DefaultModelBinder
{
    protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
    { 
        ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name]; 
        propertyMetadata.Model = value;
        string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyMetadata.PropertyName);

        // Try to set a value into the property unless we know it will fail (read-only 
        // properties and null values with non-nullable types)
        if (!propertyDescriptor.IsReadOnly) { 
        try {
            if (value == null)
            {
            propertyDescriptor.SetValue(bindingContext.Model, value);
            }
            else
            {
            Type valueType = value.GetType();

            if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(EntityCollection<>))
            {
                IListSource ls = (IListSource)propertyDescriptor.GetValue(bindingContext.Model);
                IList list = ls.GetList();

                foreach (var item in (IEnumerable)value)
                {
                list.Add(item);
                }
            }
            else
            {
                propertyDescriptor.SetValue(bindingContext.Model, value);
            }
            }

        }
        catch (Exception ex) {
            // Only add if we're not already invalid
            if (bindingContext.ModelState.IsValidField(modelStateKey)) { 
            bindingContext.ModelState.AddModelError(modelStateKey, ex); 
            }
        } 
        }
    }
}

Не забудьте зарегистрировать ваше связующее в Global.asax:

ModelBinders.Binders.DefaultBinder = new MyDefaultModelBinder();

Ответ 4

Я работал над исключением ModelBinding, выполнив следующее:

// Remove the error from ModelState which will have the same name as the collection.
ModelState.Remove("Complaints"/*EntityCollection*/); 
if (ModelState.IsValid) // Still catches other errors.
{
    entities.SaveChanges(); // Your ObjectContext
}

Основной недостаток заключается в том, что исключение все еще бросается и может быть дорогостоящим во время выполнения. Элегантная работа может заключаться в создании обертки вокруг существующего DefaultBinder и предотвращения повторного создания EntityCollection, что уже сделано EF. Затем привязка этой коллекции к значениям формы (FormCollection).

Имейте в виду, что если вы связываете несколько коллекций, вам нужно будет удалить ошибку для каждой коллекции.

В моем эксперименте коллекция успешно сохранилась, а также корневой объект на графике, частью которого была коллекция.

Надеюсь, что это поможет кому-то другому.

Ответ 5

У меня была идентичная проблема! В итоге вы обнаружите, что структура не может обрабатывать сложные модели.

Я написал небольшой компонент привязки, который может инициализировать сложные привязки в сообщении.

Но в основном то, что вам нужно сделать, это то, что говорит Арнис Л.