ASP.NET MVC 5 模型绑定编辑视图

5

我无法想出一个最好用口头和一点代码描述的问题的解决方案。我正在使用VS 2013、MVC 5和EF6 Code-First;我还使用了MvcControllerWithContext脚手架,它生成支持CRUD操作的控制器和视图。

简单地说,我有一个包含CreatedDate值的简单模型:

public class WarrantyModel
{
    [Key]
    public int Id { get; set; }
    public string Description { get; set; }
    DateTime CreatedDate { get; set; }
    DateTime LastModifiedDate { get; set; }
}

包含MVC脚手架使用相同的模型用于其索引、创建、删除、详细信息和编辑视图。我希望在“创建”视图中包含“CreatedDate”,但是我不想在“编辑”视图中包含它,因为当编辑视图被提交到服务器时,我不希望它的值发生变化,也不希望任何人在表单提交期间篡改该值。
理想情况下,我不希望“CreatedDate”出现在编辑视图中。我发现可以在模型的“CreatedDate”属性上放置一些属性(例如[ScaffoldColumn(false)]),以防止它出现在编辑视图中,但是在回发时我会遇到绑定错误,因为“CreatedDate”的值最终会变成1/1/0001 12:00:00 AM。这是因为编辑视图没有将“CreatedDate”字段的值传回控制器。
我不想实现需要进行任何SQL Server更改的解决方案,例如在保存“CreatedDate”值的表上添加触发器。如果我想快速修复,我会在呈现编辑视图之前存储“CreatedDate”(当然是在服务器端),然后在回发时还原“CreatedDate”--这样我就可以将“CreatedDate”作为隐藏表单字段发送,并在回发后在控制器中覆盖其值,但是我没有一个好的策略来存储服务器端的值(我不想使用Session变量或ViewBag)。
我考虑过使用[Bind(Exclude="CreatedDate")],但这并没有帮助。
我的控制器编辑回发函数中的代码如下:
public ActionResult Edit([Bind(Include="Id,Description,CreatedDate,LastModifiedDate")] WarrantyModel warrantymodel)
{
    if (ModelState.IsValid)
    {
        db.Entry(warrantymodel).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(warrantymodel);
}

我想我可能能够在上面的if块中检查db.Entry(warrantymodel)对象,并查看CreatedDate的OriginalValue,但是当我尝试访问该值(如下所示)时,我会收到'System.InvalidOperationException'类型的异常:

var originalCreatedDate = db.Entry(warrantymodel).Property("CreatedDate").OriginalValue;

如果我能够成功地检查原始的CreatedDate值(即已经在数据库中的值),那么我就可以覆盖掉CurrentValue的值。但是由于上面这行代码会生成一个异常,所以我不知道还有什么其他办法。(我考虑过查询数据库来获取该值,但这很愚蠢,因为在渲染编辑视图之前数据库已经查询了该值)。
另一个我想到的办法是将CreatedDate值的IsModified值更改为false,但是当我进行调试时,我发现它已经在我的前面显示的'if'块中被设置为false了。
bool createdDateIsModified = db.Entry(warrantymodel).Property("CreatedDate").IsModified;

我对如何处理这个看似简单的问题已经无计可施了。总之,我不想将模型字段传递给编辑视图,并且我希望该字段(例如CreatedDate)在其他编辑视图字段被提交并使用db.SaveChanges()持久化到数据库时,仍保持原始值。
非常感谢任何帮助/想法。
谢谢。
6个回答

14

你应该利用ViewModel:

public class WarrantyModelCreateViewModel
{
    public int Id { get; set; }
    public string Description { get; set; }
    DateTime CreatedDate { get; set; }
    DateTime LastModifiedDate { get; set; }
}

public class WarrantyModelEditViewModel
{
    public int Id { get; set; }
    public string Description { get; set; }
    DateTime LastModifiedDate { get; set; }
}

ViewModel的意图有点不同于领域模型。它提供给视图足够的信息以正确地呈现。

ViewModel还可以保留与您的领域完全无关的信息。它可以持有表格中的排序属性或搜索过滤器的引用。这些肯定不能放在您的领域模型上!

现在,在您的控制器中,您将从ViewModels映射属性到您的领域模型并保存更改:

public ActionResult Edit(WarrantyModelEditViewModel vm)
{
    if (ModelState.IsValid)
    {
        var warrant = db.Warranties.Find(vm.Id);
        warrant.Description = vm.Description;
        warrant.LastModifiedDate = vm.LastModifiedDate;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(warrantymodel);
}

此外,ViewModel非常适合汇总多个模型的数据。假设您有一个有关保修的详细视图,但您还想查看在该保修期内进行的所有维修记录,那么您可以简单地使用以下ViewModel:
public class WarrantyModelDetailsViewModel
{
    public int Id { get; set; }
    public string Description { get; set; }
    DateTime CreatedDate { get; set; }
    DateTime LastModifiedDate { get; set; }
    List<Services> Services { get; set; }
}

ViewModel是简单、灵活且非常流行的使用方式。以下是一个对其的良好解释:http://lostechies.com/jimmybogard/2009/06/30/how-we-do-mvc-view-models/

你将最终需要编写大量的映射代码。Automapper非常棒,能完成大部分繁重的工作:http://automapper.codeplex.com/


我将使用您建议的自定义编辑视图模型。但是,我希望在 post-back 时避免重新查询数据库。Microsoft 在其函数签名中使用 Bind 表达式: public ActionResult Edit([Bind(Include="Id,Description,CreatedDate,LastModifiedDate")] WarrantyModel warrantymodel) 您的 db.Warranties.Find(vm.Id) 和映射是否执行相同的操作? - Jazimov
嗨,我认为使用相同的视图模型更容易,然后使用Automapper并配置它跳过空值,以便现有数据仍存在于域模型中。请参见下面的答案。 - stenlytw

3
这并不是回答问题的答案,但对于使用Bind()并遇到不同问题的人来说可能非常关键。当我搜索“为什么Bind()会清除所有预先存在但未绑定的值”时,我发现了这个:

(在HttpPost Edit()中)脚手架生成了一个Bind属性,并将模型绑定器创建的实体添加到具有Modified标志的实体集中。该代码不再推荐使用,因为Bind属性会清除Include参数中未列出字段中的任何预先存在的数据。将来,MVC控制器脚手架将进行更新,以便它不会为Edit方法生成Bind属性。

来自官方页面(上次更新于2015年3月): http://www.asp.net/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application#overpost 根据主题:
  1. 不建议使用Bind,将来将从自动生成的代码中删除。

  2. TryUpdateModel()现在是官方解决方案。

您可以在主题中搜索“TryUpdateModel”以获取详细信息。

2
它可能解决你的问题。
在模型中:使用?
public class WarrantyModel
{
    [Key]
    public int Id { get; set; }
    public string Description { get; set; }
    DateTime? CreatedDate { get; set; }
    DateTime? LastModifiedDate { get; set; }
}

表单提交后:

public ActionResult Edit([Bind(Include = "Id,Description,CreatedDate,LastModifiedDate")] WarrantyModel warrantymodel)
{
    if (ModelState.IsValid)
    {
        db.Entry(warrantymodel).State = EntityState.Modified;
        db.Entry(warrantymodel).Property("CreatedDate").IsModified=false
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(warrantymodel);
}

它确实解决了我的问题:在更新记录后,创建日期总是消失了。使用@moynul-haque-biswas的db.Entry(model).Property("field").IsModified=false,任何选择的字段都可以保持不变。效果非常好! - Lave Loos
这个提示解决了我的问题。非常感谢。不应更新的属性必须在实体状态标记为“已修改”之后放置。 - macmuri

1

+1 对cheny的回答表示赞同。使用TryUpdateModel而不是Bind。

public ActionResult Edit(int id)
{
    var warrantymodel = db.Warranties.Find(id);
    if (TryUpdateModel(warrantymodel, "", new string[] { "Id", "Description", "LastModifiedDate" }))
    {
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(warrantymodel);
}

如果您想使用View Model,可以使用Automapper并将其配置为跳过null值,以便现有数据仍存在于域模型中。
示例:
模型:
public class WarrantyModel
{
    public int Id { get; set; }
    public string Description { get; set; }
    DateTime CreatedDate { get; set; }
    DateTime? LastModifiedDate { get; set; }
}

视图模型:

public class WarrantyViewModel
{
    public int Id { get; set; }
    public string Description { get; set; }
    DateTime? CreatedDate { get; set; }
    DateTime? LastModifiedDate { get; set; }
}

控制器:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include="Id,Description,LastModifiedDate")] WarrantyViewModel warrantyViewModel)
{
    var warrantyModel = db.Warranties.Find(warrantyViewModel.Id);
    Mapper.Map(warrantyViewModel, warrantyModel);
    if (ModelState.IsValid)
    {
        db.Entry(warrantyModel).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(warrantyModel);
}

Automapper:
Mapper.CreateMap<WarrantyViewModel, WarrantyModel>()
    .ForAllMembers(opt => opt.Condition(srs => !srs.IsSourceValueNull));

0
尝试在编辑视图中删除“创建日期”提示文本框。在我的应用程序中,脚手架生成的编辑和创建视图包含在数据库中生成的主键。

这不是一个解决方案。思路是始终使用脚手架生成视图,而不是在生成后手动编辑视图。你的方法问题在于,如果你需要重新生成视图,你需要记住哪些视图是你手动修改过的,以便让应用程序按照你的需求工作。 - Jazimov
谢谢您的反馈,Jazimov。确实如此。但是我该如何让它识别在数据库中生成的主键呢?因为它是自动生成的,我不需要在编辑中拥有它。 - Mubarak
我并不完全理解您的问题,但是一般来说,您不应该向视图传递任何视图不需要的信息。在您的情况下,主键就是这样--不要将其发送到视图...实现此行为的一种方法是利用视图模型,正如回答中所讨论的那样。这能解决您的问题吗? - Jazimov

-1

控制器:

...
warrantymodel.CreatedDate = DateTime.Parse(Request.Form["CreatedDate"]);
...

1
我认为这个回答有点简短了,为了帮助任何发现你的答案的用户,最好加上一些背景和/或解释。 - M.T
从请求对象中获取值的建议表明完全不理解原始问题的概念,因为它既没有遵循视图模型的概念,也没有考虑到脚手架的存在。 - Jazimov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接