Платформа сущностей не обновляет свойство сложной коллекции модели - PullRequest
4 голосов
/ 20 сентября 2011

возможно, этот вопрос задавался во многих формах и раньше, но все же я думаю, что их сценарий не дает четкого решения.

У меня есть следующие классы сущностей.

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для сущности проекта и пользовательского связывателя модели для этой модели представления.

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

Теперь, когда у меня есть проект внутри вида редактирования, я выбираю новые теги для проекта и отправляю параметр действия редактирования формы, использую связующее звено модели и устанавливаемвсе свойства объекта проекта, включая теги.Но когда объект проекта передается методу inserttorupdate grouprepository, все сделанные нами изменения помещаются в базу данных, за исключением свойства коллекции Tags, теперь я действительно разочарован этой вещью.

Пожалуйста, предоставьте мне решение, которое не будет вносить изменений в структуру, было разработано до сих пор.

Ответы [ 2 ]

3 голосов
/ 21 сентября 2011

Нечто подобное для вашего 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

SaveChanges фактически сохранит ранее перезагруженный проект ссписок тегов из-за обнаружения изменений EF.Проект, переданный в метод, даже не привязан к контексту и просто используется для обновления перезагруженного проекта и его списка тегов.

Редактировать

context.Tags.Attach(tag); добавлено вкод, в противном случае SaveChanges создаст новые теги в базе данных.

2 голосов
/ 19 февраля 2013

Я создал помощник для 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);
    }
  }
...