Entity Framework и MVC 3: отношение не может быть изменено, поскольку одно или несколько свойств внешнего ключа не могут быть обнуляемыми - PullRequest
3 голосов
/ 27 февраля 2011

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

Было предложено использовать AutoMapper, и я попробовал это и заставил работать. (см. Попытка использовать AutoMapper для модели с дочерними коллекциями, получение ошибки NULL в 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 в контексте происходит сбой. Я получаю эту ошибку:

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

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

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

UPDATE:

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

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

Я думал о том, что может иметь какое-то отношение к моим проблемам: View использует технику, описанную Стивеном Сандерсоном (см. 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;
            }
        }
    }

Но опять же, я бы не подумал, что это должно быть проблемой, поскольку скрытый ввод содержит идентификатор в атрибуте значения, и я подумал, что UpdateModel просто посмотрел на имя поля для получения программ (коллекции) и Имя (свойство), а затем значение идентификатора ...? И, опять же, кажется, что во время обновления наблюдается некоторое несоответствие. В любом случае, вот сгенерированный 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 ]

1 голос
/ 06 марта 2011

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

1 голос
/ 25 апреля 2011

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

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

и повторите попытку: все будет нормально!

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

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

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

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

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

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

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

1 голос
/ 03 марта 2011

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

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

...