Entity Framework and MVC 3: the relationship cannot be changed because one or more properties of the foreign key are not null

I am trying to use one view to update an object and all its child collections (based on a one-to-many relationship in a SQL Server database with an Entity Framework model).

It was suggested to use AutoMapper, and I tried this and made it work. (see Attempting to use AutoMapper for a model with child collections, getting a zero error in Asp.Net MVC 3 ).

But the solution is really hard to maintain. And when I try the simple one that I had to start with, using the entity object directly as a model (the "Consultant" object, the parent of all child collections), I can return all the correct changed data back to POST, and I can use the UpdateModel for them receipt, including child collections. Just. Of course, UpdateModel only worked after creating a custom mediation from the tooltip here in SO:

From my custom mediation:

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 is my simple POST editing 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 next step, the problem is trying to call SaveChanges in a context that fails. I get this error:

The operation failed: the relation cannot be changed because one or more properties of the foreign key are nonzero. When a relationship change occurs, the related foreign-key property is null. If the foreign key does not support null values, a new relationship must be defined, a foreign key property must be assigned another non-zero value, or an unrelated object must be deleted.

I do not understand what is wrong. I see all the correct values ​​in the "Consultant" object, I just can’t save it in the database. The AutoMapper route in this case (although an interesting tool) does not work very well, which greatly complicates my code and makes the application, which should be quite simple, a nightmare to support.

Can anyone make it clear why I get this error and how to overcome it?

UPDATE:

Reading some posts here, I found one that seemed slightly related: How to update a model in a database from asp.net MVC2 using Entity Framework? . I do not know if this is related to this, but when I checked the consultant object after POST, it seems that this object itself has an entitykey, but the individual elements in the collection are not (EntityKeySet = null). However, each item has a valid identifier. I do not pretend to understand this with EntityKey, so please explain if it has anything to do with my problem, and if so, how to resolve it ...

UPDATE 2:

I thought of something that might be relevant to my problems: The View uses the technique described by Stephen 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 that UpdateModel cannot match the elements in the collection in the view with those in the "Consultant" object. I am wondering if this is related to indexing in this technique. Here is the helper from this code (I can't do it myself, but it uses Guid to create indexes, which can be a 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 would not think that this should be a problem, since the hidden input contains the identifier in the value attribute, and I thought that UpdateModel just looked at the field name to get the program (collection) and Name (property), and then the value for id ...? And again, during the update, there seems to be an incorrectness. In any case, html from FireBug is generated here:

 <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> 

Does anyone know if this is a problem? And if so, how can I get around this to be able to easily update collections using UpdateModel? (Although it is still possible to add or remove elements in the view before POST, that was the purpose of this technique to start with).

+4
source share
3 answers

It looks like there is a parent that has a one-to-many relationship with your consultant organization. When you change the attribute of a Consultant object that is used as a ForeignKey for this relationship, the Entity Framework sets the corresponding field in the parent object to null to decouple the relationship. If this field is not null, you will receive this error. Actually, this definition of error is surprisingly good; I saw this problem with much more mysterious errors.

So, I recommend that you check the parent object in the database and go to the fix from there (if you can change it to nullable, everything will be fine if it is part of another -pk restriction or you have to bother with object models). I would ask you to publish your entity models, but a piece of text is scary as it is.

+1
source

I think the error you get is related to: EF 4: Removing a child from the collection does not delete it - why? You created an orphan somewhere.

+1
source

Yes, this is due to HtmlPrefixScopeExtensions, but only because you are using Mvc Futures model bindings. In global.asax.cs, comment out the line

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

and try again: it will work fine!

The problem arises because the futures MVC middleware model does not handle this case correctly. It converts the form data into your model when the form is submitted, but it has a problem populating the ModelState when using HtmlPrefixScopeExtensions to generate non-incremental identifiers.

The model itself is correctly created from the form data. The problem lies inside the ModelState, which contains only the last value of the collection, and not all elements of the collection.

A strongly typed helper method - which displays a list - selects only those elements that are in the model’s property list AND in the corresponding ModelState record, which is converted to a list. So, since there is only one item in the corresponding ModelState, the other items in the list will be canceled.

This method is called by 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"]. The value contains only the last element of the list.

This is a bug (or an unsupported scenario) in MVC3 Futures extensible model bindings.

+1
source

Source: https://habr.com/ru/post/1341510/


All Articles