我一直在尝试使用一个视图来更新一个对象及其所有子集合(基于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之前仍然能够添加或删除视图中的项目,这正是这种技术的目的所在)。