4

I'm having a real hard time trying to set up a drop-down list to a related model/table in my create view. My Guest model has a reference to the PersonPrefix model in the following way:

Guest model:

public virtual PersonPrefix Prefix { get; set; }

PersonPrefix model:

public class PersonPrefix
{

    [Key]
    public int PersonPrefixID { get; set; }
    [StringLength(6)]
    public string Abbreviation { get; set; }
    [StringLength(255)]
    public string Name { get; set; }
    public virtual ICollection<Guest> Guests { get; set; }

}

I have done the following to be able to get the data from the database and show it in a dropdown:

Controller:

public ActionResult Create()
{
    ViewBag.PersonPrefixes = new SelectList(db.PersonPrefixes, "PersonPrefixID", "Abbreviation");
    return View();
} 

and I've added the prefix object to the post

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "GuestID,FirstName,MiddleName,Surname,BirthDate,SelectedPrefix")] Guest guest)
{
    if (ModelState.IsValid)
    {
        db.Guests.Add(guest);
        db.SaveChanges();
        return RedirectToAction("Index");
    }

    return View(guest);
}

This is the relevant code in the view so far but it is not passing the value to the controller:

  <div class="form-group">
        @Html.LabelFor(model => model.Prefix, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10" >
            @*@Html.DropDownListFor(m => m.Prefix,  new SelectList(ViewBag.PersonPrefixes, "Value", "Text", 1))*@
            @Html.DropDownListFor(m => m.Prefix,
            new SelectList(ViewBag.PersonPrefixes, "Value", "Text", 1))

        </div>
    </div>

Thanks for your help!!

ekad
  • 14,436
  • 26
  • 44
  • 46
Evan Barke
  • 99
  • 1
  • 13
  • You cant bind a ` –  May 28 '15 at 02:06
  • Thanks! I'm new to MVC though, would you be able to point me towards a decent example? – Evan Barke May 28 '15 at 02:16
  • check this example : https://dotnetfiddle.net/PtMRVw – IndieTech Solutions May 28 '15 at 02:35

2 Answers2

1

You cannot bind a <select> element to a complex object. All html form controls post back a single value (or in the case of <select multiple> an array of value types). If you selected an option with a value of (say) 5, then the DefaultModelBinder would try to set guest.Prefix = 5; which of course fails.

You need to bind to a value type (e.g. int, string etc). In your case you cannot even bind to the PersonPrefixID of PersonPrefix because validation would fail on the other properties of PersonPrefix. As always when editing, you should use a view model containing only those properties you need to edit.

public class GuestVM
{
  [Display(Name = "Prefix")]
  [Required(ErrorMessage = "Please select a prefix")]
  public int SelectedPrefix { get; set; }
  .... // other properties of Guest
  public SelectList PrefixList { get; set; }
}

Controller

public ActionResult Create()
{
  GuestVM model = new GuestVM();
  model.PrefixList = new SelectList(db.PersonPrefixes, "PersonPrefixID", "Abbreviation");
  .... // set other properties as required
  return View(model); // always return an instance of the model
}

View

@Html.LabelFor(m => m.SelectedPrefix)
@Html.DropDownListFor(m => m.SelectedPrefix, Model.PrefixList, "--please select--")
@Html.ValidationMessageFor(m => m.SelectedPrefix)

Then in the POST method, initialize a new instance of Guest data model and map its properties from the posted view model and finally save the data model.

public ActionResult Create(GuestVM model)
{
  if (!ModelSTate.IsValid)
  {
    model.PrefixList = new SelectList(db.PersonPrefixes, "PersonPrefixID", "Abbreviation");
    return View(model);
  }
  // Initialize a new instance of the data model and set its properties
  Guest guest = new Guest()
  {
    FirstName = model.FirstName,
    MiddleName = model.MiddleName,
    .... // other properties
    Prefix = db.PersonPrefixes.Find(model.SelectedPrefix)
  };
  db.Guests.Add(guest);
  db.SaveChanges();
  return RedirectToAction("Index");
}

Side note: You do not need to create another SelectList in the view (its already a SelectList) and the last parameter where you tried to set the selected value to 1 is ignored (its the value of the property your binding to which determines which option is selected) so if you want to pre-select an option with value="1", then set the value of SelectedPrefix = 1; in the controller before you pass the model to the view.

  • Thanks for your great explanation! Just a question before I implement it though: what's the best practice? Should I rather change the Guest model to contain public int PersonPrefixId (will EF still be able to handle the code-first migrations like that?) or should I keep the complex object reference in the model and build viewmodels? – Evan Barke May 28 '15 at 03:33
  • Best practice is always to use view models. There are numerous advantages such as applying specify display and validation attributes to propertues, preventing over-posting attacks, avoiding use of `ViewBag` etc. (see also [What is ViewModel in MVC?](http://stackoverflow.com/questions/11064316/what-is-viewmodel-in-mvc). And tools such as [automapper](https://github.com/AutoMapper/AutoMapper) make it easy to map between data models and view models. –  May 28 '15 at 03:40
  • You rock. Will give this a try and let you know how it goes :) – Evan Barke May 28 '15 at 04:18
  • OK so I'm nearly there. I just don't understand what to do in the POST method? It currently looks like this: [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "GuestID,FirstName,MiddleName,Surname,BirthDate,SelectedPrefix")] Guest guest) { if (ModelState.IsValid) { db.Guests.Add(guest); db.SaveChanges(); return RedirectToAction("Index"); } return View(guest); } – Evan Barke May 28 '15 at 04:31
  • Too hard to read in comments (suggest your edit your question with the new code), but the POST method parameter needs to be `GuestVM` (not `Guest`) and the `[Bind]` attribute is not required. Give me 5 min and I will edit my answer with some more code in the POST method to make it clearer. –  May 28 '15 at 04:34
  • See update - note that you need to reassign the `SelectList` if you return the view, and you may need to adjust the `Prefix = db.PersonPrefixes.Find(model.SelectedPrefix)` line depending on your database structure (e.g. db.PersonPrefixes.Where(p => p.PersonPrefixID == model.SelectedPrefix).FirstOrDefault(); –  May 28 '15 at 04:41
  • Cool, The controller seems to be fine now. But the view is complaining that SelectedPrefix is not part of Guest even though I changed the model at the top to: @model Riviera.ViewModels.GuestVM Thanks for all your help. I wanna get this perfect becuase I know I am going to be using it a lot – Evan Barke May 28 '15 at 06:56
  • ?? Its not supposed to be part of `Guest` (its only a property of `GuestVM`) so I'm not sure how or where you could be getting that error –  May 28 '15 at 07:01
  • Ahhh it was just the compiler lagging behind. It works when I build and run. Now I'm getting a datetime2 to datetime conversion error on the BirthDate but I think I can sort that out. Thanks again! – Evan Barke May 28 '15 at 07:05
  • That's an issue with the allowable range of `DateTime` value in sqlserver `datetime` vs `datetime2` –  May 28 '15 at 07:15
  • That's all good. I sorted it by adding [DataType(DataType.Date)] above BirthDate in GuestVM. Everything is working now :) Thanks so much. Now to get the drop downs to look nice haha – Evan Barke May 28 '15 at 07:30
1

While it's better to use ViewModels, per Stephen's answer, what you're doing wrong specifically here is this:

First, your DropDownListFor is wrong. Your ViewBag entry already contains a SelectList, so you don't need to create another one... you just have to cast the dynamic type.

@Html.DropDownListFor(m => m.Prefix.PersonPrefixID, 
                           (SelectList)ViewBag.PersonPrefixes, "Select...")

The first parameter is the selected id from the list of objects in the PersonPrefixes... So when the model is posted to the controller, it will contain the ID (but not the other data, so you will have to make sure to query the database to populate it).

Second, you will need to change your Post Action to this:

Create([Bind(Include = "GuestID,FirstName,MiddleName,Surname,BirthDate")] Guest guest)

And then on your PersonPrefix class use the Bind syntax:

[Bind(Include = "PersonPrefixID")]
public class PersonPrefix
{
    [Key]
    public int PersonPrefixID { get; set; }
    .....
}

Now, when you post, the rest of PersonPrefix will be empty, because this data is not present in the post, so you have to retrieve it from the database using the posted PersonPrefixID.

However, as I said, it's better to use ViewModels. Even so, some of the same things I said here still apply. Your DropDownListFor needs to be correct, and work with a selected id property. And, if you're using a ViewModel, there is no reason to use the Bind attributes, since you're only binding the exact items you need.

Erik Funkenbusch
  • 92,674
  • 28
  • 195
  • 291
  • A potential problem is that other properties of `PersonPrefix` have validation attributes so `ModelState` will be invalid on postback - it would mean having to remove `ModelState` errors on those properties before testing if `ModelState` is valid (and therefore if the view should be returned for correction) –  May 28 '15 at 04:48
  • @StephenMuecke - There is only StringLength, which is a maximum length.. and if you're not binding to it, it can't possibly fail – Erik Funkenbusch May 28 '15 at 04:51