3

I'm having trouble grasping the proper way to create view models and save that info back to the database using Entity Framework, and I can't seem to find the info I'm looking for, so please forgive me if I have overlooked it.

I came across this post here and he seems to be asking the same question but doesn't get an answer.

My main questions are,

For editing purposes, If I have a ProductModel model that has a Warranty model relationship, should I be using virtual property Warranty in the view model or should I be using int WarrantyId?

If I should be using a virtual property, why doesn't this code save the Warranty properly?

Do I need to explicitly flag or populate the Warranty for update?

Please not this does populate my edit view and select lists as intended.

My (simplified) code is setup as follows:

Model:

    public int ModelId{ get; set; }

    public int ModelNumber { get; set; }

    public virtual Warranty Warranty { get; set;}

View Model:

    public int ModelId { get; set; }

    [Required(ErrorMessage = "Model Number required")]
    [StringLength(25, ErrorMessage = "Must be under 25 characters")]
    [Display(Name="Model Number")]
    public string ModelNumber { get; set; }

    //related objects and necesary properties
    public virtual Warranty Warranty { get; set; }

    public IEnumerable<SelectListItem> WarrantySelectListItems { get; set; }

Controller (GET):

public ActionResult Edit(int? id)
    {
        //check the id
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }

        //get the model and make sure the object is populated
        var model = _modelService.GetModel(id.Value);
        if (model == null)
        {
            return HttpNotFound();
        }

        //pass our entity (db) model to our view model
        var editModelModel = new EditModelModel();
        editModelModel.InjectFrom(model);

        //warranty select list
        editModelModel.WarrantySelectListItems = WarrantySelectList(editModelModel.Warranty.WarrantyId);

        //option multi select list
        editModelModel.OptionSelectListItems = OptionSelectList();

        return View(editModelModel);
    }

Controller (POST) (work in progress):

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit(EditModelModel editModelModel)
    {
        if (!ModelState.IsValid)
        {
            return View(editModelModel);
        }

        var modelEntity = new Model();
        modelEntity.InjectFrom(editModelModel);

        _modelService.Update(modelEntity);
        _unitOfWork.Save();

        return RedirectToAction("Index");
    }

View (simplified):

<div class="form-group">
        @Html.Label("Warranty", new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.DropDownListFor(x => x.Warranty.WarrantyId, Model.WarrantySelectListItems, "--Select--")
            @Html.ValidationMessageFor(model => model.Warranty.WarrantyId)
        </div>
    </div>

Again, I just want to know the proper/best way to set up these viewmodels and models so the EF is doing as much of the work as possible. I feel like if I have to create a WarrantyId field, I'm doing something wrong, but maybe that isn't the case.

Thanks in advance. Any insight/help is greatly appreciated.

Community
  • 1
  • 1
Mike McCoy
  • 797
  • 2
  • 10
  • 19

5 Answers5

7

For editing purposes, If I have a ProductModel model that has a Warranty model relationship, should I be using virtual property Warranty in the view model or should I be using int WarrantyId?

You don't use virtual keyword for the property of your ViewModel, because the ViewModel has nothing to do with Entity Framework. The reason to use the virtual keyword is to allow lazy loading in Entity Framework. In your case, if you add the virtual keyword for the Warranty navigation property in the Product POCO class, you can access the Warranty property like below:

Model.Warranty.WarrantyId

And the reason it didn't save the Warranty information into your Database is because you need to define a Warranty foreign key property in the Product class.

In your case, if you're using code first approach and Product is your POCO class, just keep it simply like below:

    public class Product
    {
        public int ModelId { get; set; }
        public int ModelNumber { get; set; }
        public int WarrantyId {get;set;}

        [ForeignKey("WarrantyId ")]
        public virtual Warranty Warranty { get; set; }
    }

Then your ViewModel :

    public class MyViewModel 
    {
        public Product Product { get; set; }
        public IEnumerable<SelectListItem> WarrantySelectListItems { get; set; }
    }

Finally your view

  @model MyViewModel

  @Html.DropDownList("Product.Warranty.WarrantyId", Model.WarrantySelectListItems, "--Select--")
  @Html.ValidationMessageFor("Product.Warranty.WarrantyId")

Of course, you need to change your action methods to meet the ViewModel.

noelicus
  • 14,468
  • 3
  • 92
  • 111
Lin
  • 15,078
  • 4
  • 47
  • 49
  • Although a couple other answers were very good, I'm accepting this as the answer to this question because it answers the questions directly asked and gives examples of how to solve my model-viewmodel question within the given bounds of the original question. Also, it has the most up-votes. – Mike McCoy Jan 20 '14 at 14:00
2

For editing purposes, If I have a ProductModel model that has a Warranty model relationship, should I be using virtual property Warranty in the view model or should I be using int WarrantyId?

You shouldn't be using virtual properties in your view models. A view model simply represents the slice of data that is necessary to display a view. As you are mapping to that view model from your entities, you don't need to mark anything as virtual. See this answer, if you want to know what virtual is doing with regards to the Entity Framework.

Also, you should only be including the information necessary to render that view. So if you just need the WarrantyId in the view, then only include that.

As you're also model-binding back to the same view model in your POST action, you should be very specific about what you want your view model to represent, otherwise you leave yourself open to an over-posting attack.

I feel like if I have to create a WarrantyId field, I'm doing something wrong, but maybe that isn't the case.

It isn't the case. Each of your views should be self-contained. When you first starting using view models, one-per-view, your initial reaction is that of violating DRY. However, each view has different requirements. In terms of view models themselves, the most obvious distinction is validation. If you use entities in your views, all of those views are tied to the validation rules you've applied to your entities. (You'd also be vulnerable to over-posting if you don't want the user to be able to edit the entire entity.)

However, by having separate view models for your views, and applying the validation rules on the view models themselves, you can now have different validation requirements in your views. For example:

public class ViewAViewModel
{
    [Required]
    public int WarrantyId { get; set; }
}

public class ViewBViewModel
{
    // No longer required.
    public int WarrantyId { get; set; }
}

If you'd have included Warranty directly in both of these views, you'd have been stuck with one set of validation rules.

That aside, I'm wondering why you have this on your model (which I assume is an entity):

public IEnumerable<SelectListItem> WarrantySelectListItems { get; set; }

That doesn't belong here. This is a presentation detail, and it should not exist in your business objects. It should exist on your view model.

Community
  • 1
  • 1
John H
  • 14,422
  • 4
  • 41
  • 74
1

What you're dealing with are definitely navigation properties (the virtual properties on your model classes), and this does a good job of explaining them:

http://msdn.microsoft.com/en-us/data/jj713564.aspx

The tricky parts in defining these are really in how you set up your DbContext for the database. The official doc on this is here:

http://msdn.microsoft.com/en-us/data/jj591620

Simple parent-child relationships are pretty easy to handle, and there are additional situations (trickier) where you can define a number of models that physically come from the same row in a table, but I don't think you're dealing with that here.

The MVC part is a separate concern, and ideally you should treat it as such. Controller code should only delegate real "work" to other classes. The unit of work pattern, if you choose to use it, isn't really necessary until you get into situations where you've got a big lump of stuff to persist/edit across many tables or entity sets, with the idea that you may want it all to fail or succeed as a whole. If you're just handling simple persistence of single objects, don't even complicate it with the unit of work pattern.

The other thing to keep in mind with EF, or any ORM framework, is that it needs to track changes or compare to existing records, so the key values become super important as you work through this.

Jeff Putz
  • 14,504
  • 11
  • 41
  • 52
1

A ViewModel is a simplified view of your data that is UI aware and includes only information you need for UI rendering and User Input.

It might seem wrong to do more work - why not use the model directly? But with complex systems you end up with a lot of complexity and often you need to change the Model to accomodate the UI and it's a mess.

Also, ViewModels allow you to test the UI without having a database present and without that complexity. You really decouple UI issues and Data Modeling issues.

I usually end up NEVER using Models on the UI at all, always through ViewModel that simplifies my life in the end even if it's more work first.

So let's do a couple of changes.

View Model (Renamed to EditViewModel for clarity):

public int ModelId { get; set; }

// Removed for clarity, include needed properties in the UI

public int WarrantyId { get; set; }

public IEnumerable<SelectListItem> WarrantySelectListItems { get; set; }

Controller (GET):

public ActionResult Edit(int? id)
{
    //check the id
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }

    //get the model and make sure the object is populated
    var model = _modelService.GetModel(id.Value);
    if (model == null)
    {
        return HttpNotFound();
    }

    //pass our entity (db) model to our view model
    var editViewModel = new EditViewModel();
    editViewModel.InjectFrom(model);

    // You could instead create a custom injection like FlatLoopValueInjection
    // That would flatten and remove duplicates from 
    // Model.Warranty.WarrantyId to ViewModel.WarrantyId
    editViewModel.WarrantyId = model.Warranty.Id;

    //warranty select list
    editViewModel.WarrantySelectListItems = WarrantySelectList(editViewModel.WarrantyId);

    return View(editViewModel);
}

Custom Injection Flatten - FlatLoopValueInjection:

http://valueinjecter.codeplex.com/wikipage?title=flattening&referringTitle=Home

Controller (POST):

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(EditViewModel editViewModel)
{
    if (!ModelState.IsValid)
    {
        return View(editViewModel);
    }

    // You need to reconstruct the model itself, there are faster ways but I wanted
    // to showcase the logic behind it
    // I didn't do any null check or anything to simplify

    // Load the model used from the database
    var modelEntity = _modelService.GetModel(editViewModel.ModelId);

    // You can do an InjectFrom for the other properties you need
    // with custom Injection to unflatten
    modelEntity.InjectFrom(editViewModel);

    // Load the selected warranty from the database
    var warrantyEntity = _warrantyService.GetWarranty(editViewModel.WarrantyId);

    // Update the warranty of the model with the one loaded
    modelEntity.Warranty = warrantyEntity;

    _modelService.Update(modelEntity);

    _unitOfWork.Save();

    return RedirectToAction("Index");
}

Now in your view:

<div class="form-group">
    @Html.Label("Warranty", new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.DropDownListFor(x => x.WarrantyId, Model.WarrantySelectListItems, "--Select--")
        @Html.ValidationMessageFor(model => model.WarrantyId)
    </div>
</div>

As a side note, in your models and view models, you should try to never repeat prefixes in names like:

  • Model.ModelId
  • Warranty.WarrantyId

Unless it's a foreign key or value:

  • Model.WarrantyId

Why? It's a LOT easier to flatten/unflatten them by convention with InjectFrom:

Model.Warranty.Id => (flatten) => Model.WarrantyId => (unflatten) => Model.Warranty.Id

Also, it's a best practice. The name of the model/table already tells you the entity type, no need to repeat it.

Karhgath
  • 1,809
  • 15
  • 11
0

You have to have int WarrantyId in your view model. Than in your view

@Html.DropDownListFor(x => x.WarrantyId, Model.WarrantySelectListItems, "--Select--")

In Controller (POST) take WarrantyId (selected from dropdown) and find object from database (var warranty = db.Warranties.Where(w=>w.WarrantyId == editModelModel.WarrantyId or something like that) and that object assign to modelEntity.

Krahu
  • 183
  • 7