3
votes

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.

3
post your code please.without code can't help.A_Sk
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.Steve Greene
Added Code @user3540365 & Steve !Adi Sekar
Hi Steve - Would rebuilding the view, have my validation errors intact? Required field and other validations, if I rebuild the VM and pass it?Adi Sekar
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.user3559349

3 Answers

4
votes

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)
0
votes

On post back move your view model to the entity model if it is valid. Note the Post takes a view model which protects you from exposing your entity model which is generally considered unsafe. A tool like AutoMapper is great for this, but you can do it by hand:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Buy(BuyVM buyVM)
{

  if (ModelState.IsValid)
    {
    var buyModel = new BuyModel {
      PurchaseDateList = buyVM.PurchaseDateList,
      BedroomsList = buyVM.BedroomsList,
      ...
     };
     // Do Procesing, Save Entity Model
  }
  // Otherwise, reset unbound fields on viewmodel
  buyVM.List = GetList();
  ...
  return View(buyVM);
}

MVC will automatically pass back the error information.

-1
votes

You would have to rebuild the viewmodel before returning the view again - that means, with your model included, rebuilding all of your dropdowns, etc. before returning the view.

You could also think about finding a way to just work with the viewmodel in your Post action.