Entity Framework и MVC 3: отношение не может быть изменено, так как одно или несколько свойств внешнего ключа не могут иметь значение NULL

Я пытался использовать одно представление для обновления объекта и всех его дочерних коллекций (на основе отношений "один ко многим" в базе данных SQL Server с моделью Entity Framework).

Было предложено, чтобы я использовал AutoMapper, и я попробовал это и заставил это работать. (см. Попытка использовать AutoMapper для модели с дочерними коллекциями, получая нулевую ошибку в Asp.Net MVC 3).

Но решение действительно трудно поддерживать. И когда я попробую простую модель, с которой мне пришлось начать, используя объект-сущность непосредственно в качестве модели (объект "Консультант", родитель всех дочерних коллекций), я могу получить все правильные измененные данные обратно в POST, и я могу использовать UpdateModel, чтобы получить их, включая дочерние коллекции. Просто. Конечно, UpdateModel работал только после создания пользовательского связующего элемента модели из подсказки здесь, в SO:

Из моей пользовательской модели переплета:

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            bindingContext.ModelMetadata.ConvertEmptyStringToNull = false;

            return base.BindModel(controllerContext, bindingContext);
        }

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

Вот мой простой метод редактирования POST:

    [HttpPost]
    [ValidateInput(false)] //To allow HTML in description box
    public ActionResult Edit(int id, FormCollection collection)
    {

        Consultant consultant = _repository.GetConsultant(id);
        UpdateModel(consultant);
        _repository.Save();

        return RedirectToAction("Index");
    }

Но после этого UpdateModel работал. Проблема в том, что на следующем этапе при попытке вызвать SaveChanges для контекста происходит сбой. Я получаю эту ошибку:

Операция не выполнена: отношение не может быть изменено, так как одно или несколько свойств внешнего ключа не могут иметь значение NULL. Когда в отношение вносится изменение, для соответствующего свойства внешнего ключа устанавливается нулевое значение. Если внешний ключ не поддерживает нулевые значения, необходимо определить новое отношение, свойству внешнего ключа должно быть назначено другое ненулевое значение или несвязанный объект должен быть удален.

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

Кто-нибудь может подсказать, почему я получаю эту ошибку и как ее преодолеть?

ОБНОВИТЬ:

Прочитав несколько постов здесь, я нашел один, который казался слегка связанным: Как обновить модель в базе данных, из asp.net MVC2, используя Entity Framework?, Я не знаю, относится ли это к этому, но когда я проверял объект Консультанта после POST, кажется, что сам объект имеет сущность, но отдельные элементы в коллекции не имеют (EntityKeySet = null). Однако каждый элемент имеет правильный идентификатор. Я не претендую на то, чтобы понять что-либо из этого с EntityKey, поэтому, пожалуйста, объясните, имеет ли это какое-либо отношение к моей проблеме, и если да, то как ее решить...

ОБНОВЛЕНИЕ 2:

Я подумал о том, что может иметь какое-то отношение к моим проблемам: "Вид" использует методику, описанную Стивеном Сандерсоном (см. http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/), и при отладке мне кажется, что UpdateModel испытывает проблемы с сопоставлением элементов в коллекции в представлении с элементами в реальном объекте Консультанта. Мне интересно, имеет ли это отношение к индексации в этой технике. Вот помощник из этого кода (я сам не очень хорошо им следую, но он использует Guid для создания индексов, что может быть проблемой):

public static class HtmlPrefixScopeExtensions
    {
        private const string idsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_";

        public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName)
        {
            var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
            string itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString();

            // autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync.
            html.ViewContext.Writer.WriteLine(string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />", collectionName, html.Encode(itemIndex)));

            return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex));
        }

        public static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix)
        {
            return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);
        }

        private static Queue<string> GetIdsToReuse(HttpContextBase httpContext, string collectionName)
        {
            // We need to use the same sequence of IDs following a server-side validation failure,  
            // otherwise the framework won't render the validation error messages next to each item.
            string key = idsToReuseKey + collectionName;
            var queue = (Queue<string>)httpContext.Items[key];
            if (queue == null)
            {
                httpContext.Items[key] = queue = new Queue<string>();
                var previouslyUsedIds = httpContext.Request[collectionName + ".index"];
                if (!string.IsNullOrEmpty(previouslyUsedIds))
                    foreach (string previouslyUsedId in previouslyUsedIds.Split(','))
                        queue.Enqueue(previouslyUsedId);
            }
            return queue;
        }

        private class HtmlFieldPrefixScope : IDisposable
        {
            private readonly TemplateInfo templateInfo;
            private readonly string previousHtmlFieldPrefix;

            public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix)
            {
                this.templateInfo = templateInfo;

                previousHtmlFieldPrefix = templateInfo.HtmlFieldPrefix;
                templateInfo.HtmlFieldPrefix = htmlFieldPrefix;
            }

            public void Dispose()
            {
                templateInfo.HtmlFieldPrefix = previousHtmlFieldPrefix;
            }
        }
    }

Но опять же, я бы не подумал, что это должно быть проблемой, так как скрытый ввод содержит идентификатор в атрибуте value, и я подумал, что UpdateModel просто посмотрел на имя поля, чтобы получить Programs (коллекция) и Name (свойство), а затем значение идентификатора...? И, опять же, кажется, что во время обновления наблюдается некоторое несоответствие. В любом случае, вот сгенерированный html из FireBug также:

<td>
            <input type="hidden" value="1" name="Programs[cabac7d3-855f-45d8-81b8-c31fcaa8bd3d].Id" id="Programs_cabac7d3-855f-45d8-81b8-c31fcaa8bd3d__Id" data-val-required="The Id field is required." data-val-number="The field Id must be a number." data-val="true"> 
            <input type="text" value="Visual Studio" name="Programs[cabac7d3-855f-45d8-81b8-c31fcaa8bd3d].Name" id="Programs_cabac7d3-855f-45d8-81b8-c31fcaa8bd3d__Name">
            <span data-valmsg-replace="true" data-valmsg-for="Programs[cabac7d3-855f-45d8-81b8-c31fcaa8bd3d].Name" class="field-validation-valid"></span>
        </td>

Кто-нибудь знает, в этом ли проблема? И если так, как я могу обойти это, чтобы иметь возможность легко обновлять коллекции с UpdateModel? (Хотя все еще можно добавлять или удалять элементы в представлении до POST, что и было целью этой техники для начала).

3 ответа

Решение

Похоже, что есть родительский объект, который имеет отношения один-ко-многим с вашим консультантом. Когда вы изменяете атрибут сущности Консультант, который используется в качестве ForeignKey для этой взаимосвязи, Entity Framework устанавливает для соответствующего поля в родительской сущности значение null, чтобы отделить взаимосвязь. Когда это поле не обнуляется, вы получите эту ошибку. На самом деле это определение ошибки на удивление хорошо, я видел эту проблему с гораздо более загадочными ошибками.

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

Я думаю, что ошибка, которую вы получаете, связана с: EF 4: удаление дочернего объекта из коллекции не удаляет его - почему? Вы где-то создали сироту.

Да, это связано с HtmlPrefixScopeExtensions, но только потому, что вы используете связыватели модели Mvc Futures. В global.asax.cs закомментируйте строку

Microsoft.Web.Mvc.ModelBinding.ModelBinderConfig.Initialize(); 

и повторите попытку: все будет хорошо!

Проблема возникает из-за того, что связыватель модели фьючерсов MVC неправильно обрабатывает этот случай. Он хорошо преобразует данные формы в вашу модель при отправке формы, но имеет проблему при заполнении объекта ModelState, когда вы используете HtmlPrefixScopeExtensions для генерации неинкрементных идентификаторов.

Сама модель правильно создана из данных формы. Проблема заключается в ModelState, который содержит только последнее значение коллекции вместо всех элементов коллекции.

Строго типизированный вспомогательный метод, который отображает список, выбирает только те элементы, которые находятся в списке свойств модели И в соответствующей записи ModelState, которая преобразуется в список. Таким образом, поскольку в соответствующей записи ModelState есть только один элемент, другие элементы списка отменяются.

Этот метод вызывается строго типизированным вспомогательным кодом:

htmlHelper.GetModelStateValue(fullName, typeof(string[]))

возвращает только последний элемент списка, потому что ModelState["Programs[cabac7d3-855f-45d8-81b8-c31fcaa8bd3d].List"]. Значение содержит только последний элемент списка.

Это ошибка (или неподдерживаемый сценарий) в связывателях расширяемой модели MVC3 Futures.

Другие вопросы по тегам