ASP.NET MVC 5 Model-Binding Edit View

2019-04-28 15:48发布

I cannot come up with a solution to a problem that's best described verbally and with a little code. I am using VS 2013, MVC 5, and EF6 code-first; I am also using the MvcControllerWithContext scaffold, which generates a controller and views that support CRUD operations.

Simply, I have a simple model that contains a CreatedDate value:

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

The included MVC scaffold uses the same model for its index, create, delete, details, and edit views. I want the CreatedDate in the 'create' view; I do not want it in the 'edit' view because I do not want its value to change when the edit view is posted back to the server and I don't want anyone to be able to tamper with the value during a form-post.

Ideally, I don't want the CreatedDate to ever get to the Edit view. I have found a few attributes I can place on the model's CreatedDate property (for example, [ScaffoldColumn(false)]) that prevent it from appearing on the Edit view, but then I'm getting binding errors on postback because the CreatedDate ends up with a value of 1/1/0001 12:00:00 AM. That's because the Edit view is not passing a value back to the controller for the CreatedDate field.

I don't want to implement a solution that requires any SQL Server changes, such as adding a trigger on the table that holds the CreatedDate value. If I wanted to do a quick-fix, I would store the CreatedDate (server-side, of course) before the Edit view is presented and then restore the CreatedDate on postback--that would let me change the 1/1/0001 date to the CreatedDate EF6 pulled from the database before rendering the view. That way, I could send CreatedDate as a hidden form field and then overwrite its value in the controller after postback, but I don't have a good strategy for storing server-side values (I don't want to use Session variable or the ViewBag).

I looked at using [Bind(Exclude="CreatedDate")], but that doesn't help.

The code in my controller's Edit post-back function looks like this:

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

I thought I might be able to examine the db.Entry(warrantymodel) object within the if block above and examine at the OriginalValue for CreatedDate, but when I try to access that value (as shown next), I get an exception of type 'System.InvalidOperationException':

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

If I could successfully examine the original CreatedDate value (i.e., the one that is already in the database) I could just overwrite whatever the CurrentValue is. But since the above line of code generates an exception, I don't know what else to do. (I thought about querying the database for the value but that's just silly since the database was already queried for the value before the Edit view was rendered).

Another idea I had was to change the IsModified value to false for the CreatedDate value but when I debug then I discover that it is already is set to false in my 'if' block shown earlier:

bool createdDateIsModified = db.Entry(warrantymodel).Property("CreatedDate").IsModified;

I am out of ideas on how to handle this seemingly simple problem. In summary, I do not want to pass a model field to an Edit view and I want that field (CreatedDate, in this example) to maintain its original value when the other Edit fields from the view are posted back and persisted to the database using db.SaveChanges().

Any help/thoughts would be most appreciated.

Thank you.

6条回答
何必那么认真
2楼-- · 2019-04-28 16:22

try to remove the Create date prompt text box in the Edit view. In my application, the scaffold generated Edit and Create Views contain the Primary key which is generated in the database.

查看更多
劳资没心,怎么记你
3楼-- · 2019-04-28 16:24

This is not an answer for the questions, but it might be critical for those who is using Bind() and facing different problems. When I was searching "why Bind() clears out all pre-existing but not-bound values", I found this:

(in the HttpPost Edit()) The scaffolder generated a Bind attribute and added the entity created by the model binder to the entity set with a Modified flag. That code is no longer recommended because the Bind attribute clears out any pre-existing data in fields not listed in the Include parameter. In the future, the MVC controller scaffolder will be updated so that it doesn't generate Bind attributes for Edit methods.

from a official page (last updated in 2015, March): 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

According to the topic:

  1. Bind is not recommended and will be removed in the future from the auto-generated codes.

  2. TryUpdateModel() is now the official solution.

You can search "TryUpdateModel" in the topic for details.

查看更多
男人必须洒脱
4楼-- · 2019-04-28 16:31

Controller:

...
warrantymodel.CreatedDate = DateTime.Parse(Request.Form["CreatedDate"]);
...
查看更多
叛逆
5楼-- · 2019-04-28 16:34

You should leverage ViewModels:

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

The intention of a ViewModel is a bit different than that of a domain model. It provides the view with just enough information it needs to render properly.

ViewModels can also retain information that doesn't pertain to your domain at all. It could hold a reference to the sorting property on a table, or a search filter. Those certainly wouldn't make sense to put on your domain model!

Now, in your controllers, you map properties from the ViewModels to your domain models and persist your changes:

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

Furthermore, ViewModels are great for amalgamating data from multiple models. What if you had a details view for your warranties, but you also wanted to see all servicing done under that warranty? You could simply use a ViewModel like this:

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

ViewModels are simple, flexible, and very popular to use. Here is a good explantion of them: http://lostechies.com/jimmybogard/2009/06/30/how-we-do-mvc-view-models/

You're going to end up writing a lot of mapping code. Automapper is awesome and will do most of the heavy lifting: http://automapper.codeplex.com/

查看更多
不美不萌又怎样
6楼-- · 2019-04-28 16:40

It may solve your problem

In Model: Use ?

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

After form submit:

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);
}
查看更多
够拽才男人
7楼-- · 2019-04-28 16:44

+1 for cheny's answer. Use TryUpdateModel instead of 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);
}

If you want to use View Model, you can use Automapper and configure it to skip null values so the existing data still exists in the domain model.

Example:

Model:

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

ViewModel:

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

Controller:

[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));
查看更多
登录 后发表回答