实体框架不更新模型的复杂集合属性

时间:2011-09-20 17:39:21

标签: asp.net-mvc asp.net-mvc-3 entity-framework entity-framework-4.1 asp.net-mvc-scaffolding

很可能这个问题之前已经有很多种形式的问题,但我认为他们的情景并没有明确的解决方案。

我有以下实体类。

public class Project
{
    public int ProjectId { get; set; }
    [Required(ErrorMessage="please enter name")]
    public string Name { get; set; }
    public string Url { get; set; }
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
    public bool isFeatured { get; set; }
    public bool isDisabled { get; set; }
    public int GroupId { get; set; }
    public virtual Group Group { get; set; }
    [Required(ErrorMessage="Please select atleast one tag")]
    public virtual ICollection<Tag> Tags { get; set; }
}

public class Tag
{
    public int TagId { get; set; }
    public string Name { get; set; }
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
    public virtual ICollection<Project> Projects { get; set; }
}

public class Group
{
    public int GroupId { get; set; }
    public string Name { get; set; }
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
    public virtual ICollection<Project> Projects { get; set; }
}

我有项目实体的viewmodel和这个viewmodel的自定义模型绑定器。

public class NewProjectModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        ProjectNewViewModel model = (ProjectNewViewModel)bindingContext.Model ??
            (ProjectNewViewModel)DependencyResolver.Current.GetService(typeof(ProjectNewViewModel));
        bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);
        string searchPrefix = (hasPrefix) ? bindingContext.ModelName + ".":"";

        //since viewmodel contains custom types like project make sure project is not null and to pass key arround for value providers
        //use Project.Name even if your makrup dont have Project prefix

        model.Project  = model.Project ?? new Project();
        //populate the fields of the model
        if (GetValue(bindingContext, searchPrefix, "Project.ProjectId") !=  null)
        {
            model.Project.ProjectId = int.Parse(GetValue(bindingContext, searchPrefix, "Project.ProjectId"));
        }

        //
        model.Project.Name = GetValue(bindingContext, searchPrefix, "Project.Name");
        model.Project.Url = GetValue(bindingContext, searchPrefix, "Project.Url");
        model.Project.CreatedOn  =  DateTime.Now;
        model.Project.UpdatedOn = DateTime.Now;
        model.Project.isDisabled = GetCheckedValue(bindingContext, searchPrefix, "Project.isDisabled");
        model.Project.isFeatured = GetCheckedValue(bindingContext, searchPrefix, "Project.isFeatured");
        model.Project.GroupId = int.Parse(GetValue(bindingContext, searchPrefix, "Project.GroupId"));
        model.Project.Tags = new List<Tag>();

        foreach (var tagid in GetValue(bindingContext, searchPrefix, "Project.Tags").Split(','))
        {
            var tag = new Tag { TagId = int.Parse(tagid)};
            model.Project.Tags.Add(tag);
        }

        var total = model.Project.Tags.Count;

        return model;
    }

    private string GetValue(ModelBindingContext context, string prefix, string key)
    {
        ValueProviderResult vpr = context.ValueProvider.GetValue(prefix + key);
        return vpr == null ? null : vpr.AttemptedValue;
    }

    private bool GetCheckedValue(ModelBindingContext context, string prefix, string key)
    {
        bool result = false;
        ValueProviderResult vpr = context.ValueProvider.GetValue(prefix + key);
        if (vpr != null)
        {
            result = (bool)vpr.ConvertTo(typeof(bool));
        }

        return result;
    }
}

//My project controller edit action defined as under:
[HttpPost]
[ActionName("Edit")]
public ActionResult EditProject( ProjectNewViewModel ProjectVM)
{
   if (ModelState.IsValid) {
       projectRepository.InsertOrUpdate(ProjectVM.Project);
       projectRepository.Save();
       return RedirectToAction("Index");
   } 
   else {
    ViewBag.PossibleGroups = groupRepository.All;
        return View();
   }
}


//Group Repository
public void InsertOrUpdate(Project project)
    {
        if (project.ProjectId == default(int)) {
            // New entity
            foreach (var tag in project.Tags)
            {
                context.Entry(tag).State = EntityState.Unchanged;
            }
            context.Projects.Add(project);
        } else {               
            context.Entry(project).State = EntityState.Modified;
        }
    }

现在,当我在编辑视图中有一个项目并为项目选择新标签并提交表单编辑操作参数时,使用模型绑定器并设置项目对象的所有属性,包括标签。但是当项目对象传递给grouprepository的insertorupdate方法时,我们所做的所有更改都会在数据库中除了标签集合属性之外,我现在对此事感到非常沮丧。

请为我提供迄今为止尚未开发出结构变化的解决方案。

2 个答案:

答案 0 :(得分:3)

对于else中的InsertOrUpdate案例(我认为if案件很好)可能会有效:

//...
else {
    // Reload project with all tags from DB
    var projectInDb = context.Projects.Include(p => p.Tags)
        .Single(p => p.ProjectId == project.ProjectId);

    // Update scalar properties of the project
    context.Entry(projectInDb).CurrentValues.SetValues(project);

    // Check if tags have been removed, if yes: remove from loaded project tags
    foreach(var tagInDb in projectInDb.Tags.ToList())
    {
        // Check if project.Tags collection contains a tag with TagId
        // equal to tagInDb.TagId. "Any" just asks: Is there an element
        // which meets the condition, yes or no? It's like "Exists".
        if (!project.Tags.Any(t => t.TagId == tagInDb.TagId))
            projectInDb.Tags.Remove(tagInDb);
    }

    // Check if tags have been added, if yes: add to loaded project tags
    foreach(var tag in project.Tags)
    {
        // Check if projectInDb.Tags collection contains a tag with TagId
        // equal to tag.TagId. See comment above.
        if (!projectInDb.Tags.Any(t => t.TagId == tag.TagId))
        {
            // We MUST attach because tag already exists in the DB
            // but it was not assigned to the project yet. Attach tells
            // EF: "I know that it exists, don't insert a new one!!!"
            context.Tags.Attach(tag);
            // Now, we just add a new relationship between projectInDb and tag,
            // not a new tag itself
            projectInDb.Tags.Add(tag);
        }
    }
}

// context.SaveChanges() somewhere later

由于EF更改检测,SaveChanges实际上将使用标记列表保存以前重新加载的项目。传递给该方法的项目甚至没有附加到上下文,只是用于更新重新加载的项目及其标记列表。

修改

context.Tags.Attach(tag);已添加到代码中,否则SaveChanges会在数据库中创建新标记。

答案 1 :(得分:2)

我在DBContext [CodeFirst]

上创建了一个帮助器
    /// <summary>
    /// Reattaches the relationships so that they can be committed in a <see cref="DbContext.SaveChanges()"/>
    /// Determines equality using <see cref="OPSDEV.Utils.EF.KeyEqualityComparer"/>
    /// </summary>
    /// <typeparam name="T">The Model or Entity to Attach</typeparam>
    /// <param name="db">The DbContext to use to do the reattaching</param>
    /// <param name="new">The new list of values to attach</param>
    /// <param name="old">The old or previous values that existed in the database</param>
    /// <returns>The new list to be committed</returns>
    public static ICollection<T> AttachToContext<T>(this DbContext db, ICollection<T> @new, ICollection<T> old) where T : class
    {
      if (@new == null) return null;

      var result = new List<T>();

      var comparer = new KeyEqualityComparer<T>();
      var added = @new.Where(c => !old.Contains(c, comparer)).ToList();
      var existing = old.Where(c => @new.Contains(c, comparer)).ToList();

      foreach (var entity in added)
      {
        db.Entry(entity).State = EntityState.Unchanged;
        result.Add(entity);
      }

      foreach (var entity in existing)
      {
        db.Entry(entity).State = EntityState.Unchanged;
        result.Add(entity);
      }

      return result;
    }

它使用KeyEqualityComparer

  /// <summary>
  /// Uses the Key attribute to determine equality. 
  /// Both keys but have have equal values for the comparer to return true.
  /// Throws "No Key property found" ArgumentException if no key attribute can be found.
  /// </summary>
  /// <typeparam name="T">The Model or Entity type to be compared</typeparam>
  public class KeyEqualityComparer<T> : EqualityComparer<T>
  {
    private PropertyInfo Property { get; set; }
    public KeyEqualityComparer()
    {
      Property = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
      .FirstOrDefault(p => p.GetCustomAttributes(typeof(KeyAttribute), false).Any());

      if (Property == null)
        throw new ArgumentException("No Key property found");
    }

    public override bool Equals(T x, T y)
    {
      return GetValue(x).Equals(GetValue(y));
    }

    public override int GetHashCode(T obj)
    {
      return GetValue(obj).GetHashCode();
    }

    public object GetValue(object obj)
    {
      var value = Property.GetValue(obj, null);
      return  value ?? default(T);
    }
  }