Entity Framework和MVC 3:由于一个或多个外键属性是非空的,关系无法更改。

3

我一直在尝试使用一个视图来更新一个对象及其所有子集合(基于SQL Server数据库中的一个Entity Framework模型中的一对多关系)。

有人建议我使用AutoMapper,我尝试了一下并成功了。(见Trying to use AutoMapper for model with child collections, getting null error in 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值。如果外键不支持null值,则必须定义新的关系,将外键属性分配给另一个非null值,或删除不相关的对象。
我不明白哪里出错了。我在发布的Consultant对象中看到所有正确的值,只是无法将其保存到数据库中。在这种情况下AutoMapper的路由(虽然是一个有趣的工具)没有很好地工作,它使我的代码变得极其复杂,并使这个应用程序,本应该很简单,成为了一个维护的噩梦。有人能提供任何关于我为什么会得到这个错误以及如何克服它的见解吗?
更新:
在这里阅读一些帖子,我发现其中一个似乎与此略有关联: 如何使用Entity Framework从asp.net MVC2更新数据库中的模型?。我不知道它是否与此相关,但当我检查了Consultant对象POST后,它本身似乎具有entitykey,但集合中的各个项目却没有(EntityKeySet = null)。然而,每个项目都有正确的id。我不打算理解任何与EntityKey有关的内容,请解释一下它是否对我的问题有任何影响,如果有,如何解决...
我想到了一些可能与我的问题有关的事情:View使用了Steven Sanderson描述的一种技术(参见http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/),在调试时,似乎UpdateModel无法将View中集合中的项目与实际Consultant对象中的项目匹配。我想知道这是否与该技术中的索引有关。以下是来自该代码的帮助程序(我自己也无法很好地理解它,但它使用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;
            }
        }
    }

但是,我认为这不应该是问题,因为隐藏输入框包含了id的值属性,而我认为UpdateModel只是查看字段的名称来获取Programs(集合)和Name(属性),然后将值设置为id...?但是在更新过程中似乎存在一些不匹配。无论如何,这里也是来自FireBug生成的HTML:

<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之前仍然能够添加或删除视图中的项目,这正是这种技术的目的所在)。


在更新期间,您是否从子集合中删除了任何项? - Ladislav Mrnka
不,就是这样,即使我在视图中没有进行任何更改,尝试保存时仍会出现这种情况... - Anders
3个回答

1

是的,它与HtmlPrefixScopeExtensions有关,但这仅因为您正在使用Mvc Futures模型绑定器。 在global.asax.cs中注释掉该行即可。

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

重试一下:它会正常工作的!

问题出现在MVC futures模型绑定器无法正确处理此情况。当您提交表单时,它可以将表单数据转换为您的模型,但是在使用HtmlPrefixScopeExtensions生成非增量ID时填充ModelState对象时存在问题。

模型本身从表单数据中正确创建。问题在于ModelState中仅包含集合的最后一个值,而不是集合的所有元素。

强类型帮助程序方法(用于呈现列表)仅选择在您的Model属性列表中且与匹配的ModelState条目匹配的项目,该条目被转换为列表。因此,由于匹配的ModelState条目中只有一个项目,其他列表项将被取消选择。

这个方法由强类型帮助程序代码调用:

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

返回列表中仅有的最后一个元素,因为ModelState["Programs[cabac7d3-855f-45d8-81b8-c31fcaa8bd3d].List"].Value仅包含列表的最后一个元素。

这是MVC3 Futures可扩展模型绑定器中的一个错误(或不支持的场景)。


好的,非常感谢您的建议。实际上,我已经想出了一个解决方法并继续前进,所以我没有任何立即可测试的代码,但我一有机会就会试一下! - Anders

1

好的,但我需要更多信息,我不知道那个孤儿会在哪里。 - Anders
我的猜测是,在自定义模型绑定器中,当您删除子记录时,将其设置为null而不是从数据库中删除它。 - Wim

1

看起来有一个父实体与您的顾问实体存在一对多的关系。当您更改用作该关系外键的顾问实体的属性时,实体框架会将父实体中的相关字段设置为null以解除关系。当该字段不可为空时,您将收到此错误。实际上,这个错误定义非常好,我曾经遇到过更加神秘的错误。

因此,我建议您检查数据库中的父实体,并从那里进行修复(如果可以将其更改为可空,则一切都很好,如果它是其他约束的一部分 -pk或类似的-则必须调整对象模型)。我想要求您发布实体模型,但是这段文本已经令人生畏了。


不,那对我来说似乎不正确,因为我没有顾问对象的父对象... - Anders
这是一个子实体,通过一个非空字段引用顾问。机制是相同的,EF试图将该字段设置为null以删除子级中的关系,但它无法这样做。 - mcyalcin
好的,我得研究一下。由于这个赏金很快就要到期了,我会尝试一下。如果有任何进一步的提示可以帮助我找到错误,那将不胜感激! - Anders
我遇到了相似的问题(在这里描述:http://stackoverflow.com/questions/10170101/foreign-key-constraint-ef-with-collection-of-childobjects/10186594),并且有一个解决方案。不过,现在似乎我不能使用这个解决方案来添加到我的集合中。我还在寻找,并会在找到后分享。 - Stefan Bergfeldt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接