3

I am working on an ASP.NET MVC3 application and I cannot get a DropDownListFor to work with my property in a particular editor view.

My model is a Person and a person has a property that specifies it's "Person Type". PersonType is a class that contains a name/description and an ID. I can access the available PersonTypes within the application through my static/shared class called ApplicationSettings.

In the Edit Template view for my Person I have created a SelectList for debugging purposes:

@ModelType MyNamespace.Person
@Code
    Dim pTypesSelectList As New SelectList(MyNamespace.ApplicationSettings.PersonTypes, "ID", "Name", Model.PersonType.ID)
End Code

I am then providing this SelectList as a parameter to the DropDownListFor that is bound to the PersonType property of my Person.

I am also printing the Selected property of each item in the SelectList for debugging purposes:

<div style="text-align: center; margin: 5px 0 0 0;">
    <div>
        @Html.LabelFor(Function(model) model.PersonType)
    </div>
    <div>
        @Html.DropDownListFor(Function(model) model.PersonType, pTypesSelectList)
        @Html.ValidationMessageFor(Function(model) model.PersonType)

        <br />
        <br />
        <!-- The following is debugging code that shows the actual value-->
        @Model.Type.Name
        <br />
        @Model.Type.ID
        <br />
        <br />

        <!--This section is to show that the select list has properly selected the value-->
        @For Each pitem In pTypesSelectList
            @<div>
                @pitem.Text selected: @pitem.Selected
            </div>
        Next
    </div>
</div>

The view is bound to a Person whose PersonType property is "Person Type # 2" and I expect this to be selected; however the HTML output of this code looks like this:

<div style="text-align: center; margin: 5px 0 0 0;">
    <div>
    <label for="PersonType">PersonType</label>
    </div>
    <div>
        <select id="PersonType" name="PersonType">
            <option value="7e750688-7e00-eeee-0000-007e7506887e">Default Person Type</option>
            <option value="87e5f686-990e-5151-0151-65fa7506887e">Person Type # 1</option>
            <option value="a7b91cb6-2048-4b5b-8b60-a1456ba4134a">Person Type # 2</option>
            <option value="8a147405-8725-4b53-b4b8-3541c2391ca9">Person Type # 3</option>
        </select>
        <span class="field-validation-valid" data-valmsg-for="PersonType" data-valmsg-replace="true"></span>
        <br />
        <br />
        <!-- The following is debugging code that shows the actual value-->
        Person Type # 2
        <br />
        a7b91cb6-2048-4b5b-8b60-a1456ba4134a
        <br />
        <br />
        <!--This section is to show that the select list has properly selected the value-->
        <div>
            Default Person Type selected: False
        </div>
        <div>
            Person Type # 1 selected: False
        </div>
        <div>
            Person Type # 2 selected: True
        </div>
        <div>
            Person Type # 3 selected: False
        </div>
    </div>
</div>

As you can see the printed Selected properties for the items in the SelectList shows that the 3rd item is "Selected". But what is driving me crazy is that the option that corresponds with this is Not Selected.

Frinavale
  • 3,908
  • 10
  • 44
  • 74
  • Can you post the code for PersonTypes in your ApplicationSettings class? – ataravati Aug 16 '13 at 17:39
  • Possibly see e.g. this: http://stackoverflow.com/questions/17691742/ - generally, the `Selected` property in `SelectList` will be totally ignored by the HTML helpers unless there's no other option. If `DropDownListFor` can find the value by other means, it will insist on that value. In this case, it will use the value of `model.PersonType`(`.ToString()`) - but that's not what you want, judging by the `model.PersonType.ID` you pass to the `SelectList`. – JimmiTh Aug 16 '13 at 18:58
  • The ToString method of the PersonType class returns it's name: comparing this to the ID (a Guid) is never going to match. How do I circumvent this? – Frinavale Aug 16 '13 at 19:53
  • Right now I am using the SelectList and a For Each loop to generate – Frinavale Aug 16 '13 at 19:57
  • 1
    One easy workaround that should work would be to set `ViewData["PersonType"] = model.PersonType.Id`. The helper looks in `ModelState` first (if it exists - i.e. on POST - when it should work already, since `ModelState["PersonType"]` will be populated with the actual selected value that was posted). Anyway, after `ModelState` it will look in `ViewData` - with `ViewData["PersonType"]` first, and only then `ViewData.Model.PersonType`. In other words, you can "override" the value on your model with the value set directly on `ViewData`. – JimmiTh Aug 16 '13 at 22:48
  • 1
    The more general, "better practice", way to solve it (which also avoids having a custom model binder in order to translate the POST'ed `ID` back to `PersonType`) is to use a *ViewModel* instead of working with full models in your view: Have a `PersonTypeID` property instead of `PersonType`; populate it with `PersonType.ID`; use `Html.DropDownListFor(Function(model) model.PersonTypeID)` in your view; when form is POST'ed, translate the ViewModel (including `PersonTypeID` => `PersonType`) back into the actual model in your POST Action. – JimmiTh Aug 16 '13 at 23:17
  • Thank you JimmiTH. The suggestion about using ViewData worked. I already have a custom model binder because the person object is more complicated than a name and person type :) – Frinavale Aug 19 '13 at 13:17
  • @JimmiTH, could you please post your comment as an answer so that I can mark it as the answer? Thank you. I also took your suggestion about creating a ViewModel because I needed to in order to get around another problem. I am trying it out now. – Frinavale Aug 20 '13 at 14:12
  • Sure - I mainly didn't put it as an answer because I wasn't in the mood/didn't have the time to expand on it if needed. :-) – JimmiTh Aug 20 '13 at 14:23
  • I actually used your second solution. It is much cleaner and easier to understand. Thanks again for your help! – Frinavale Aug 20 '13 at 14:24

2 Answers2

6

Generally, the Selected property in SelectList will be totally ignored by the HTML helpers unless there's no other option. If DropDownListFor can find the value by other means, it will insist on using that value.

In this case, it will use the value of model.PersonType(.ToString()) - but that's not what you want, judging by the model.PersonType.ID you pass to the SelectList.

More info in the answer here.

Workaround

One easy workaround that should work would be to set:

ViewData["PersonType"] = model.PersonType.Id. 

The helper looks in ModelState first if it exists - i.e. on POST. This should work already, since ModelState["PersonType"] will be populated with the actual selected value that was posted.

After ModelState it will look in ViewData - with ViewData["PersonType"] first, and only then ViewData.Model.PersonType. In other words, you can "override" the value on your model with a value set directly on ViewData.

Better (IMO) solution

The more general, "better practice", way to solve it (which also avoids having a custom model binder in order to translate the POST'ed ID back to PersonType) is to use a ViewModel instead of working with full models in your view:

  • Have a PersonTypeID property - instead of PersonType.
  • Populate it with PersonType.ID
  • use this in your view
    • VB.NET: Html.DropDownListFor(Function(model) model.PersonTypeID), or
    • C#: Html.DropDownListFor(model => model.PersonTypeID)
  • When form is POST'ed, translate the ViewModel (including PersonTypeID => PersonType) back into the actual model in your POST Action.

This may seem like more work, but generally there tend to be many occasions in a project where you need more view-specific representations of your data to avoid too much inline Razor code - so translating from business objects to view models, while it may seem redundant and anti-DRY at times, tends to spare you of a lot of headaches.

Community
  • 1
  • 1
JimmiTh
  • 7,389
  • 3
  • 34
  • 50
  • The workaround setting the value in the ViewBag is what fixed it for me. It is incredible that in 2015 this is still an issue. Thanks for the details. – agarcian Sep 24 '15 at 16:21
  • This is just completely broken. I cannot get it to select anything at all unless the model property is of type "string". That's useless for multi-select drop downs, because it combines everything into a comma-separated list, which is just nasty and doesn't work if your values contain commas. Broken in 2016. – Triynko Feb 29 '16 at 18:39
1

Are you sure that your ModelState for "PersonType" key before rendering the view is empty? As JimmiTh commented is going to search for the value in the ModelState first. It happened to me too, you can try @Html.DropDownList("PersonTypeFake", Function(model) model.PersonType, pTypesSelectList) and it should select the right option.

mgalindez
  • 611
  • 6
  • 10
  • I just checked and, yes, the ModelState["PersonType"] is empty (null); however it didn't work until I set the ViewData. – Frinavale Aug 19 '13 at 14:20