3

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

有人建议我应该使用 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 时,它会失败。我收到此错误:

操作失败:无法更改关系,因为一个或多个外键属性不可为空。当对关系进行更改时,相关的外键属性将设置为空值。如果外键不支持空值,则必须定义新的关系,必须为外键属性分配另一个非空值,或者必须删除不相关的对象。

我不明白出了什么问题。我看到发布的顾问对象中的所有正确值,我只是无法将其保存到数据库中。在这种情况下,AutoMapper 的路线(虽然是一个有趣的工具)运行不佳,它使我的代码变得非常复杂,并使应用程序(应该相当简单)成为维护的噩梦。

任何人都可以提供有关我为什么会收到此错误以及如何克服它的任何见解吗?

更新:

在这里阅读了一些帖子,我发现一个似乎有点相关的帖子:How to update model in the database, from asp.net MVC2, using Entity Framework? . 我不知道它是否与此有关,但是当我在 POST 之后检查顾问对象时,似乎该对象本身具有实体键,但集合中的各个项目没有(EntityKeySet = null)。但是,每个项目都具有正确的 ID。我不会假装用 EntityKey 理解这些,所以请解释它是否对我的问题有任何影响,如果有,如何解决它......

更新 2:

我想到了一些可能与我的问题有关的事情:视图正在使用 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;
            }
        }
    }

但是话又说回来,我不会认为这应该是问题,因为隐藏的输入包含 value 属性中的 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 之前在视图中添加或删除项目,这也是该技术开始的目的)。

4

3 回答 3

1

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

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

然后重试:它会正常工作!

出现问题是因为 MVC 期货模型绑定器无法正确处理这种情况。当您提交表单时,它将表单数据转换为您的模型,但是当您使用 HtmlPrefixScopeExtensions 生成非增量 id 时填充 ModelState 对象时出现问题。

模型本身是根据表单数据正确创建的。问题出在 ModelState 内部,它只包含集合的最后一个值,而不是集合的所有元素。

强类型辅助方法 - 呈现列表 - 仅选择模型属性列表和匹配的模型状态条目中的项目,该条目被转换为列表。因此,因为匹配的 ModelState 条目中只有一项,其他列表项被取消选择。

此方法由强类型助手代码调用:

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

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

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

于 2011-04-25T09:23:46.453 回答
1

看起来有一个父实体与您的顾问实体具有一对多关系。当您更改用作该关系的 ForeignKey 的 Consultant 实体的属性时,实体框架将 Parent 实体中的相关字段设置为 null 以解耦关系。当该字段不可为空时,您将收到此错误。实际上,错误定义出奇的好,我已经看到这个问题有更多神秘的错误。

因此,我建议您检查数据库中的父实体,然后从那里进行补救(如果您可以将其更改为可为空,那么一切都很好,如果它是不同约束的一部分 -pk 或类似的 - 您将拥有摆弄你的对象模型)。我会要求您发布您的实体模型,但大量的文本令人生畏。

于 2011-03-03T10:37:59.267 回答
1

我认为您遇到的错误与:EF 4:从集合中删除子对象不会删除它 - 为什么?您在某处创建了一个孤儿。

于 2011-03-06T16:28:35.083 回答