问题
I have been trying to use one View for updating an object and all its child collections (based on one-to-many relationships in an SQL Server database with an Entity Framework model).
It was suggested I should use AutoMapper, and I tried that and got it to work. (see Trying to use AutoMapper for model with child collections, getting null error in Asp.Net MVC 3 ).
But the solution is really hard to maintain. And when I try the simple one I had to begin with, using an entity object directly as the model (a "Consultant" object, the parent of all the child collections), I am able to get all the correct changed data back in the POST, and I can use UpdateModel to get them, including child collections. Simple. Granted, UpdateModel only worked after creating a custom model binder from a tip here at SO:
From my custom model binder:
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);
}
}
}
}
Here's my simple Edit POST method:
[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");
}
But after that UpdateModel worked. The problem is, at the next stage, when trying to call SaveChanges on the context, that fails. I'm getting this error:
The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.
I don't understand what is wrong. I'm seeing all the correct values in the Consultant object posted, I just can't save it to database. The route of AutoMapper in this case (although an interesting tool) is not working well, it's complicating my code immensely and making the application, which should be rather simple, a nightmare to maintain.
Can anyone offer any insight into why I'm getting this error and how to overcome it?
UPDATE:
Reading some posts here, I found one that seemed slightly related: How to update model in the database, from asp.net MVC2, using Entity Framework? . I don't know if it relates to this, but when I inspected the Consultant object after POST it seems this object itself has entitykey, but the individual items in a collection do not (EntityKeySet = null). Each item however does have the correct id. I don't pretend to understand any of this with the EntityKey, so please explain if it has any bearings on my issue, and if so, how to resolve it...
UPDATE 2:
I thought of something that might have something to do with my problems: The View is using a technique described by Steven Sanderson (see http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/ ), and when debugging it seems to me as if UpdateModel has trouble matching the items in a collection in the View with the ones in the actual Consultant object. I'm wondering if this has to do with the indexing in this technique. Here's the helper from that code (I can't follow it very well myself, but it uses a Guid to create indexes, which might be the problem):
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;
}
}
}
But then again, I wouldn't have thought this should be the problem since the hidden input contains the id in the value attribute, and I thought UpdateModel just looked at the name of the field to get Programs (the collection) and Name (the property), and then the value to the the id...? And then again there's seems to be some mismatch during update. Anyway, here's the generated html from FireBug also:
<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>
Anyone know if this is the problem? And if so, how can I work around it to be able to easily update the collections with UpdateModel? (While still being able to add or remove items in the View before POST, which was the purpose of this technique to begin with).
回答1:
It looks like there is a Parent entity that has a one to many relationship with your Consultant entity. When you change an attribute of the Consultant entity that is used as the ForeignKey for that relationship, Entity Framework sets the relevant field in the Parent entity to null to decouple the relationship. When that field is not nullable you'll get this error. Actually that error definition is surprisingly good, I've seen this problem with far more cryptic errors.
So, I recommend that you check the parent entity in the database, and proceed to a remedy from there (if you can change it to nullable all is well, if it is part of a different constraint -pk or suchlike- you'll have to fiddle with your object models). I'd ask you to post your entity models, but the chunk of text is intimidating as it is.
回答2:
I think the error you are getting is related to: EF 4: Removing child object from collection does not delete it - why? You have created an orphan somewhere.
回答3:
Yes it is related to HtmlPrefixScopeExtensions, but only because you are using Mvc Futures model binders. In global.asax.cs comment out the line
Microsoft.Web.Mvc.ModelBinding.ModelBinderConfig.Initialize();
and retry: it will work ok !
The problem happens because the MVC futures model binder does not handle correctly this case. It converts ok the form data into your model when you submit the form, but it has a problem when filling the ModelState object when you use HtmlPrefixScopeExtensions to generate non incremental ids.
The model itself is correctly created from the form data. The problem lies inside ModelState which contains only the last value of the collection instead of all elements of the collection.
The strongly typed helper method - which renders the list - only select items which are in your Model property list AND in the matching ModelState entry which is converted into a list. So because there is only one item in the matching ModelState entry other list items get deselected.
This method called by the strongly typed helper code:
htmlHelper.GetModelStateValue(fullName, typeof(string[]))
returns only the last element of the list, because ModelState["Programs[cabac7d3-855f-45d8-81b8-c31fcaa8bd3d].List"].Value contains only the last element of the list.
This is a bug (or non supported scenario) in MVC3 Futures extensible model binders.
来源:https://stackoverflow.com/questions/5132917/entity-framework-and-mvc-3-the-relationship-could-not-be-changed-because-one-or