3

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.

tereško
  • 58,060
  • 25
  • 98
  • 150
Adi Sekar
  • 43
  • 1
  • 1
  • 8
  • post your code please.without code can't help. – A_Sk Sep 04 '15 at 19:56
  • 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 Sep 04 '15 at 20:08
  • Added Code @user3540365 & Steve ! – Adi Sekar Sep 04 '15 at 20:14
  • 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 Sep 04 '15 at 20:17
  • Your POST method needs be `public async Task 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. –  Sep 04 '15 at 23:06
  • 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. –  Sep 05 '15 at 02:49
  • 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 Sep 05 '15 at 17:22
  • What I have works okay, for client side validation, but my server side validation still has this issue. – Adi Sekar Sep 05 '15 at 17:25
  • 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` or `List`). You need to explain just what those properties really are before I can give you the answer. And also show your `GetBuyVM()` method –  Sep 06 '15 at 02:32
  • 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. – Adi Sekar Sep 06 '15 at 23:53
  • @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. –  Sep 07 '15 at 00:29

3 Answers3

4

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

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.

Steve Greene
  • 12,029
  • 1
  • 33
  • 54
  • 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? – Adi Sekar Sep 04 '15 at 20:40
  • 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? – Steve Greene Sep 04 '15 at 21:05
  • It crashes during model binding 'BuyVM' to my Post action, after submitting my form.public async Task Buy(BuyVM buyVM). So it does not even reach my post action, it fails trying to bind to BuyVM. – Adi Sekar Sep 04 '15 at 21:12
  • 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. – Steve Greene Sep 05 '15 at 14:43
-1

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.

Platte Gruber
  • 2,935
  • 1
  • 20
  • 26
  • Would rebuilding the view, have my validation errors intact? Required field and other validations, if I rebuild the VM and pass it? – Adi Sekar Sep 04 '15 at 20:16
  • 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. – Platte Gruber Sep 04 '15 at 20:45
  • 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. – Adi Sekar Sep 04 '15 at 21:04
  • 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 –  Sep 04 '15 at 23:08