Asp.Net MVC - Model Binding to Model or ViewModel

Adi Sekar picture Adi Sekar · Sep 4, 2015 · Viewed 13.4k times · Source

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.

Answer

user3559349 picture user3559349 · Sep 7, 2015

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)