Asp.Net MVC - Model Binding to Model or ViewModel

14,353

Firstly the names of you data model do not make sense. A property named BedroomsList suggests a collection of Bedrooms yet the property is a string. Start by naming you properties to describe what they are so others can make sense of your code.

public class BuyModel 
{
  public string PurchaseDate { get; set; }
  public string Bedrooms { get; set; }
  public string Stories { get; set; }
  public string SquareFootage { get; set; }
  public string PreferredCityLocations { get; set; }
  public string AdditionalInfo { get; set; }
}

And the corresponding view model needs to contain those properties plus the SelectList properties.

public class BuyVM
{
  public string PurchaseDate { get; set; }
  public string Bedrooms { get; set; }
  public string Stories { get; set; }
  [Required]
  public string SquareFootage { get; set; }
  [Required]
  public string PreferredCityLocations { get; set; }
  public string AdditionalInfo { get; set; }
  public SelectList PurchaseDateList { get; set; }
  public SelectList BedroomsList { get; set; }
  public SelectList StoriesList { get; set; }
}

Next remove your GetBuyVM() method and replace it with a method in the controller that populates the select lists so that you can also call that method if ModelState is invalid and you need to return the view and change the POST method to parameter to your view model (your view is based on BuyVM so you must post back to BuyVM, not BuyModel)

public ActionResult Buy()
{
  BuyVM model = new BuyVM(); // initalise an instance of the view model
  ConfigureViewModel(model);
  return View(model);
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Buy(BuyVM model)
{
  if (!ModelState.IsValid)
  {
    ConfigureViewModel(model);
    return View(model);
   }
   Initialize your data model and map the view model properties to it
   BuyModel dataModel = new BuyModel()
   {
     PurchaseDate = model.PurchaseDate,
     Bedrooms = model.Bedrooms,
     ....
   };
   // save the data model
   return View("Success");
}

private ConfigureViewModel(BuyVM model)
{
  model.PurchaseDateList = new SelectList(new[] { "Immediately", "1 to 3 months", "4 to 6 months", "More than 6 months" });
  model.BedroomsList = new SelectList(new[] { "1", "2", "3", "4", "5+" });
  model.StoriesList = new SelectList(new[] { "1", "2", "Does not matter" });
}

And finally in the view, bind to your property (PurchaseDate, not PurchaseDateList)

@Html.DropDownListFor(m => m.PurchaseDate, Model.PurchaseDateList)
@Html.DropDownListFor(m => m.Bedrooms, Model.BedroomsList)
Share:
14,353
Adi Sekar
Author by

Adi Sekar

Updated on July 14, 2022

Comments

  • Adi Sekar
    Adi Sekar almost 2 years

    I have a controller, which returns a view passing in a view model, which has properties required for display of the view (Drop-down select item lists etc).

    But when I post it to the server, I have a different model class, which has the selected value of those dropdowns. In my HttpPost controller action, I check the (ModelState.IsValid), before doing any processing, but when it is false, I 'return View(model)' back.

    But since the view is bound to the ViewModel, and my Post action, is accepting the actual model, I get an error 'The model item passed into the dictionary is of type 'Model', but this dictionary requires a model item of type 'ViewModel', when I submit the form, and the validation error to show up on the view.

    How do I solve this? What is the best practice for using strongly typed views, passing in the view model, but when submitting to a different model?

    Code:

     public ActionResult Buy()
        {
          BuyVM buyVM = GetBuyVM();
          return View(buyVM);
        }
    
       [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Buy(BuyModel model)
        {
          if (ModelState.IsValid)
            {
            // Do Procesing
            return View("Success");
            }
          return View(model);
        }
    
     public class BuyVM
        {
            public SelectList PurchaseDateList { get; set; }
    
            public SelectList BedroomsList { get; set; }
    
            public SelectList StoriesList { get; set; }
    
            [Required]
            public string SquareFootage { get; set; }
    
            [Required]
            public string PreferredCityLocations { get; set; }
    
            public string AdditionalInfo { get; set; }
        }
    
     public class BuyModel 
        {
            public string PurchaseDateList { get; set; }
            public string BedroomsList { get; set; }
            public string StoriesList { get; set; }
            public string SquareFootage { get; set; }
            public string PreferredCityLocations { get; set; }
            public string AdditionalInfo { get; set; }
        }
    
     private static BuyVM GetBuyVM()
            {
                BuyVM buyVM = new BuyVM();
    
                buyVM.PurchaseDateList = new SelectList(new[] { "Immediately", "1 to 3 months", "4 to 6 months", "More than 6 months" });
                buyVM.BedroomsList = new SelectList(new[] { "1", "2", "3", "4", "5+" });
                buyVM.StoriesList = new SelectList(new[] { "1", "2", "Does not matter" });
    
                return buyVM;
            }
    

    Buy.cshtml

        @model Models.BuyVM
        // html
     @Html.DropDownListFor(m => m.PurchaseDateList, Model.PurchaseDateList, new { @class = "form-control" })
    
     @Html.DropDownListFor(m => m.BedroomsList, Model.BedroomsList, new { @class = "form-control" })
    

    So when I return the View(model) back, in the HTTPPost if there were validation errors (JQueryVal), I am trying to display the validation errors, if I pass the model back to the view. But I have this type mismatch.

    • A_Sk
      A_Sk over 8 years
      post your code please.without code can't help.
    • Steve Greene
      Steve Greene over 8 years
      When it fails, you need to rebuild the same model in your get before calling the view back, so you need to reset items not bound to that model like lookup lists, etc.
    • Adi Sekar
      Adi Sekar over 8 years
      Added Code @user3540365 & Steve !
    • Adi Sekar
      Adi Sekar over 8 years
      Hi Steve - Would rebuilding the view, have my validation errors intact? Required field and other validations, if I rebuild the VM and pass it?
    • Admin
      Admin over 8 years
      Your POST method needs be public async Task<ActionResult> Buy(BuyVM model) (not BuyModel) but you BuyVM view model appears be be missing a lot of properties - you have SelectList's for a number of properties by no corresponding property to bind to.
    • Admin
      Admin over 8 years
      It also impossible to understand what your really trying to do here. Your data model contains properties named xxxList suggesting a collection yet they are all typeof string and you also have a method named GetBuyVM() suggesting you populate the SelectList's in that method which is also the wrong approach. You need to show that method and explain what the properties PurchaseDateList , BedroomsList and StoriesList really are in the data model.
    • Adi Sekar
      Adi Sekar over 8 years
      I am trying to keep my VM separate from my Model. My VM contains the select List for Dropdowns, and they get populated by GetBuyVM, and when posting to server, I use the Buy Model to get the selected values from the dropdowns. Would you recommend having the selected values (string type of DD value) also be in the VM?
    • Adi Sekar
      Adi Sekar over 8 years
      What I have works okay, for client side validation, but my server side validation still has this issue.
    • Admin
      Admin over 8 years
      Having a view model (as opposed to using you data model) is best practice but you don't seem to be understanding the binding process. Your view model needs a property to bind to in addition to the SelectLists. But your data model just does not make sense so hard to show you the right way to do this. You have a property named BedroomsList which suggests a collection of Bedrooms, yet its type of string (not List<Bedroom> or List<string>). You need to explain just what those properties really are before I can give you the answer. And also show your GetBuyVM() method
    • Adi Sekar
      Adi Sekar over 8 years
      HI Stephen, I have added the code for the GetBuyVM() and view html helper for the dropdowns. So the VM has a SelectList of PurchaseDateList, but the selected value of this during submit of form, gets bound to string PurchaseDateList in BuyModel, since the naming is consistent. I wanted to make the initial Get which includes populating of Dropdown's separate from the values posting back (Selected value), so thereby having a separate VM and the model.
    • Admin
      Admin over 8 years
      @AdiSekar, To notify a user, start the comment as I have done here :). I'll post an answer a bit later showing you how to do this correctly.
  • Adi Sekar
    Adi Sekar over 8 years
    Would rebuilding the view, have my validation errors intact? Required field and other validations, if I rebuild the VM and pass it?
  • Adi Sekar
    Adi Sekar over 8 years
    I tried this, and I get this error 'No parameterless constructor defined for this object.', before it hits my breakpoint at the Post action. The BuyVM has all the select items to be bound to the Dropdowns in the View, would this get the selected values, in the POST action?
  • Platte Gruber
    Platte Gruber over 8 years
    Validation errors will stay intact either way. As Steve Greene mentions below, it still would be better to pass the ViewModel into Post. When you do, you can do all the processing you need to on the model by using ViewModel.modelName. You will still have to rebuild the dropdownlist when the model is invalid, however.
  • Adi Sekar
    Adi Sekar over 8 years
    Passing VM into post does not seem to work. I get this error 'No parameterless constructor defined for this object.', before it gets to the Post action. The BuyVM has all the select list items to be bound to the Dropdowns in the View, would this have the selected value or should we explicitly pass that? My DD in the view, are model-binded to the Model using the DD name's.
  • Steve Greene
    Steve Greene over 8 years
    What code is crashing? The bound lists won't come back to the Post, that is why you need to rebuild them. Does your GetBuyVM() populate them?
  • Adi Sekar
    Adi Sekar over 8 years
    It crashes during model binding 'BuyVM' to my Post action, after submitting my form.public async Task<ActionResult> Buy(BuyVM buyVM). So it does not even reach my post action, it fails trying to bind to BuyVM.
  • Admin
    Admin over 8 years
    Validation errors will NOT stay intact either way! BuyModel does not have any validation attributes so there would hardly be any point in if (ModelState.IsValid) since BuyModel can never be invalid
  • Steve Greene
    Steve Greene over 8 years
    Would need to see the view, but have you tried debugging it to see the line that crashes? Another option is to comment stuff out to get a minimally working version then add stuff back in.