【问题标题】:Edit on Model - complex types not updated properly编辑模型 - 复杂类型未正确更新
【发布时间】:2012-09-28 15:37:46
【问题描述】:

我有这两个对象 - 杂志和作者(M-M 关系):

public partial class MAGAZINE
    {
        public MAGAZINE()
        {
            this.AUTHORs = new HashSet<AUTHOR>();
        }

        public long REF_ID { get; set; }
        public string NOTES { get; set; }
        public string TITLE { get; set; }

        public virtual REFERENCE REFERENCE { get; set; }
        public virtual ICollection<AUTHOR> AUTHORs { get; set; }
    }

public partial class AUTHOR
{
    public AUTHOR()
    {  
         this.MAGAZINEs = new HashSet<MAGAZINE>();
    }

            public long AUTHOR_ID { get; set; }
            public string FULL_NAME { get; set; }

            public virtual ICollection<MAGAZINE> MAGAZINEs { get; set; }
        }
}

我的问题是我似乎无法根据杂志更新作者的数量,例如如果我有 1 位作者叫“Smith, P”。已经存储在一本杂志上,我可以添加另一个名为“Jones, D.”的文章,但在发回编辑控制器后,作者的数量仍然显示为 1 - 即“Smith, P.H”。

请不要说我已经成功地将作者数量模型绑定回父实体(杂志),它使用自定义模型绑定器来检索作者并绑定到杂志(我认为),但它仍然没有似乎更新正常。

我用于更新模型的代码很简单 - 并显示前后的变量值:

public ActionResult Edit(long id)
    {
        MAGAZINE magazine = db.MAGAZINEs.Find(id);
        return View(magazine);
    }

这里是变量预编辑/更新 -

[HttpPost]
public ActionResult Edit(MAGAZINE magazine)
   {
       if (ModelState.IsValid)
       {
           db.Entry(magazine).State = EntityState.Modified;
           db.SaveChanges();
           return RedirectToAction("Index");
       }

       return View(magazine);
   }

...这是添加新作者后的变量...

我开始怀疑作者实体正在显示,编辑后它没有绑定到任何杂志,我猜这就是为什么它没有被更新回杂志实体 - 但它很令人困惑,因为我实际上是处理同一个杂志实体 - 我想这可能与作者的自定义模型绑定器有关。

有人可以帮忙解决这个问题吗?

为了完整性 - 我也包含了我的 AuthorModelBinder 类 -

public class AuthorModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var values = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (values != null)
            {
                // We have specified asterisk (*) as a token delimiter. So
                // the ids will be separated by *. For example "2*3*5"
                var ids = values.AttemptedValue.Split('*');

                List<int> validIds = new List<int>();
                foreach (string id in ids)
                {
                    int successInt;
                    if (int.TryParse(id, out successInt))
                    {
                        validIds.Add(successInt);
                    }
                    else
                    {
                        //Make a new author
                        AUTHOR author = new AUTHOR();
                        author.FULL_NAME = id.Replace("\'", "").Trim();
                        using (RefmanEntities db = new RefmanEntities())
                        {
                            db.AUTHORs.Add(author);
                            db.SaveChanges();
                            validIds.Add((int)author.AUTHOR_ID);
                        }
                    }
                }

                 //Now that we have the selected ids we could fetch the corresponding
                 //authors from our datasource
                var authors = AuthorController.GetAllAuthors().Where(x => validIds.Contains((int)x.Key)).Select(x => new AUTHOR
                {
                    AUTHOR_ID = x.Key,
                    FULL_NAME = x.Value
                }).ToList();
                return authors;
            }
            return Enumerable.Empty<AUTHOR>();
        }
    }

【问题讨论】:

    标签: asp.net-mvc-3 entity-framework-4 model-binding


    【解决方案1】:

    当我使用 MVC/Nhibernate 开发博客时,我遇到了非常相似的情况,实体是 PostTag

    我也有类似这样的编辑操作,

    public ActionResult Edit(Post post)
    {
      if (ModelState.IsValid)
      {
           repo.EditPost(post);
           ...
      }
      ...
    }
    

    但与您不同的是,我为 Post 而不是 Tag 创建了自定义模型绑定器。在自定义PostModelBinder 中,我正在做与您在那里所做的几乎相同的事情(但我没有像您为Authors 所做的那样创建新的Tags)。基本上,我创建了一个新的 Post 实例,从 POST 表单中填充它的所有属性,并从数据库中获取 ID 的所有 Tags。请注意,我只从数据库中获取了Tags,而不是Post

    我可能会建议您为Magazine 创建一个ModelBinder 并检查一下。此外,最好使用存储库模式,而不是直接从控制器进行调用。

    更新:

    这里是Post模型绑定器的完整源代码

    namespace PrideParrot.Web.Controllers.ModelBinders
    {
      [ValidateInput(false)]
      public class PostBinder : IModelBinder
      {
        private IRepository repo;
    
        public PostBinder(IRepository repo)
        {
          this.repo = repo;
        }
    
        #region IModelBinder Members
    
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
          HttpRequestBase request = controllerContext.HttpContext.Request;
    
          // retrieving the posted values.
          string oper = request.Form.Get("oper"),
                   idStr = request.Form.Get("Id"),
                   heading = request.Form.Get("Heading"),
                   description = request.Form.Get("Description"),
                   tagsStr = request.Form.Get("Tags"),
                   postTypeIdStr = request.Form.Get("PostType"),
                   postedDateStr = request.Form.Get("PostedDate"),
                   isPublishedStr = request.Form.Get("Published"),
                   fileName = request.Form.Get("FileName"),
                   serialNoStr = request.Form.Get("SerialNo"),
                   metaTags = request.Form.Get("MetaTags"),
                   metaDescription = request.Form.Get("MetaDescription"),
                   themeIdStr = request.Form.Get("Theme");
    
          // initializing to default values.
          int id = 0, serialNo = 0;
          DateTime postedDate = DateTime.UtcNow;
          DateTime? modifiedDate = DateTime.UtcNow;
          postedDate.AddMilliseconds(-postedDate.Millisecond);
          modifiedDate.Value.AddMilliseconds(-modifiedDate.Value.Millisecond);
    
          /*if operation is not specified throw exception. 
            operation should be either'add' or 'edit'*/
          if (string.IsNullOrEmpty(oper))
            throw new Exception("Operation not specified");
    
          // if there is no 'id' in edit operation add error to model.
          if (string.IsNullOrEmpty(idStr) || idStr.Equals("_empty"))
          {
            if (oper.Equals("edit"))
              bindingContext.ModelState.AddModelError("Id", "Id is empty");
          }
          else
            id = int.Parse(idStr);
    
          // check if heading is not empty.
          if (string.IsNullOrEmpty(heading))
            bindingContext.ModelState.AddModelError("Heading", "Heading: Field is required");
          else if (heading.Length > 500)
            bindingContext.ModelState.AddModelError("HeadingLength", "Heading: Length should not be greater than 500 characters");
    
          // check if description is not empty.
          if (string.IsNullOrEmpty(description))
            bindingContext.ModelState.AddModelError("Description", "Description: Field is required");
    
          // check if tags is not empty.
          if (string.IsNullOrEmpty(metaTags))
            bindingContext.ModelState.AddModelError("Tags", "Tags: Field is required");
          else if (metaTags.Length > 500)
            bindingContext.ModelState.AddModelError("TagsLength", "Tags: Length should not be greater than 500 characters");
    
          // check if metadescription is not empty.
          if (string.IsNullOrEmpty(metaTags))
            bindingContext.ModelState.AddModelError("MetaDescription", "Meta Description: Field is required");
          else if (metaTags.Length > 500)
            bindingContext.ModelState.AddModelError("MetaDescription", "Meta Description: Length should not be greater than 500 characters");
    
          // check if file name is not empty.
          if (string.IsNullOrEmpty(fileName))
            bindingContext.ModelState.AddModelError("FileName", "File Name: Field is required");
          else if (fileName.Length > 50)
            bindingContext.ModelState.AddModelError("FileNameLength", "FileName: Length should not be greater than 50 characters");
    
          bool isPublished = !string.IsNullOrEmpty(isPublishedStr) ? Convert.ToBoolean(isPublishedStr.ToString()) : false;
    
          //** TAGS
          var tags = new List<PostTag>();
          var tagIds = tagsStr.Split(',');
          foreach (var tagId in tagIds)
          {
            tags.Add(repo.PostTag(int.Parse(tagId)));
          }
          if(tags.Count == 0)
            bindingContext.ModelState.AddModelError("Tags", "Tags: The Post should have atleast one tag");
    
          // retrieving the post type from repository.
          int postTypeId = !string.IsNullOrEmpty(postTypeIdStr) ? int.Parse(postTypeIdStr) : 0;
          var postType = repo.PostType(postTypeId);
          if (postType == null)
            bindingContext.ModelState.AddModelError("PostType", "Post Type is null");
    
          Theme theme = null;
          if (!string.IsNullOrEmpty(themeIdStr))
            theme = repo.Theme(int.Parse(themeIdStr));
    
          // serial no
          if (oper.Equals("edit"))
          {
            if (string.IsNullOrEmpty(serialNoStr))
              bindingContext.ModelState.AddModelError("SerialNo", "Serial No is empty");
            else
              serialNo = int.Parse(serialNoStr);
          }
          else
          {
            serialNo = repo.TotalPosts(false) + 1;
          }
    
          // check if commented date is not empty in edit.
          if (string.IsNullOrEmpty(postedDateStr))
          {
            if (oper.Equals("edit"))
              bindingContext.ModelState.AddModelError("PostedDate", "Posted Date is empty");
          }
          else
            postedDate = Convert.ToDateTime(postedDateStr.ToString());
    
          // CREATE NEW POST INSTANCE
          return new Post
          {
            Id = id,
            Heading = heading,
            Description = description,
            MetaTags = metaTags,
            MetaDescription = metaDescription,
            Tags = tags,
            PostType = postType,
            PostedDate = postedDate,
            ModifiedDate = oper.Equals("edit") ? modifiedDate : null,
            Published = isPublished,
            FileName = fileName,
            SerialNo = serialNo,
            Theme = theme
          };
        }
    
        #endregion
      }
    }
    

    【讨论】:

    • 回复!!万岁,这让马克感到欣慰,因为自从写了这篇文章后,我意识到我需要一个 MagazineModelBinder 类,所以我做了一个。但是,当您发布新帖子时,您会怎么做?您是否删除现有的帖子,然后重新从头开始?在我的情况下,我通过 ID 检索杂志并清除作者,然后应用我的新作者集合(即使以前存在相同),但是唉,它开始创建新作者,我在 Authors 表中得到重复 :( 什么你是在活页夹里做的吗?
    • @Vidar 我附上了 Post ModelBinder 的源代码,你可以看看
    • 如果您还有问题,请告诉我
    • 你知道有什么好的链接可以展示你如何实现存储库模式吗?
    • 斯蒂芬在这里写了博客stephenwalther.com/archive/2009/02/27/…
    【解决方案2】:

    db.Entry(magazine).State = EntityState.Modified; 这一行仅告诉 EF 杂志实体已更改。它没有提到关系。如果您调用Attach,对象图中的所有实体都以Unchanged 状态附加,您必须分别处理它们中的每一个。在多对多关系you must also handle relation itself(以及改变关系in DbContext API is not possible)的情况下更糟。

    我在这个problem and design in disconnected app 上花了很多时间。并且有三种通用方法:

    • 您将向您的实体发送附加信息,以查找已更改和已删除的内容(是的,您还需要跟踪已删除的项目或关系)。然后您将手动设置对象图中每个实体和关系的状态。
    • 您将只使用当前拥有的数据,而不是将它们附加到上下文中,您将加载当前杂志和您需要的每个作者,并在这些加载的实体上重建这些更改。
    • 您根本不会这样做,而是使用轻量级 AJAX 调用来添加或删除每个作者。我发现这在许多复杂的 UI 中都很常见。

    【讨论】:

    • 嗯,我的意思是最好的方式 - “我希望你错了”,因为如果不是,我认为沿着整个 EF 路线走下去是一个巨大的错误,它会给我带来很多工作我几乎不认为这是值得的!
    • 换个角度想一想——如果没有 EF,你会怎么做?在您的操作中接收数据时,您仍然需要检测已更改的内容并在数据库中执行适当的 SQL 命令以添加新记录、删除旧记录和更新现有记录。 EF 对此没有任何影响,只是您不需要手动编写这些 SQL 命令,但您必须准确告诉 EF 它需要对每条记录执行什么操作。
    • 我越来越恼火的是,你看到的每一个关于 MVC 和 EF4 的演示/教程 - 都展示了一个更新简单类型的非常基本的示例 - 没有关于你会发现自己的常见情况例如使用 1-M 或 M-M 关系。我对这一切感到非常生气!
    • 好吧,不是所有的演示,但大部分是的。我知道我在新的 Pluralsight 课程中花了很多时间讨论断开连接图的一些问题。我知道我已经与 EF 讨论了 6 年的问题,但 Ladislav 是对的,它不是 EF 问题,而是断开数据问题。 Rowan 和我在 DbContext 书中也有一整章关于这个主题。我刚刚花了一些时间在处理客户端上的图表上,可以在后台使用 EF 将它们发送到 odata 或 webapi,并为 EF 提供所有正确的状态信息
    • 顺便说一句,Ladislav 确实回答了您的问题,我将其标记为这样。我认为不需要赏金
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-12-22
    • 2017-07-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-17
    • 1970-01-01
    • 2021-12-14
    相关资源
    最近更新 更多