ASP.NET MVC 5 Model-Binding Edit View

Jazimov picture Jazimov · Jan 22, 2014 · Viewed 34.8k times · Source

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.

Answer

Mister Epic picture Mister Epic · Jan 22, 2014

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/