1

I'm trying to customize Identity (2.1) using MVC5 and EF6.1. I've added a CountryId to the Identity users table and a whole separate Country table (CountryId, CountryName) with a FK constraint on CountryId. I'm trying to do a drop down list that selects the users country if the user has already selected it in the past (from the CountryId in the users table). However, if it hasn't been selected, the value will be null.

The logic I'm trying to accomplish is this:

if (Model.Country.CountryName != null) {
    @Html.DropDownListFor(m => m.Country, new SelectList(Model.Countries, "Value", "Text"), Model.Country.CountryName)<br />
} else {
    @Html.DropDownListFor(m => m.Country, new SelectList(Model.Countries, "Value", "Text"), "Select a Country")<br />
}

If you remove the if else statement above (which doesn't work either), each DropDownListFor by itself works, except that the former throws a NullRefernceException when CountryName is null. The latter simply doesn't remember what the user chose in the past. Here is my controller:

        var userId = User.Identity.GetUserId();
        ApplicationUser user = await UserManager.FindByIdAsync(userId);
        var model = new IndexViewModel
        {
            HasPassword = HasPassword(),
            //PhoneNumber = await UserManager.GetPhoneNumberAsync(userId),
            //TwoFactor = await UserManager.GetTwoFactorEnabledAsync(userId),
            //Or we can write like this because of the user statement above.
            //PhoneNumber = user.PhoneNumber,
            //TwoFactor = user.TwoFactorEnabled,
            Logins = await UserManager.GetLoginsAsync(userId),
            BrowserRemembered = await AuthenticationManager.TwoFactorBrowserRememberedAsync(userId),
            Country = user.Country,
            State = user.State,
            City = user.City,
            Countries = new SelectList(db.Countries, "CountryId", "CountryName")
        };
        return View(model);

If I use something like this in my controller model:

Countries = new SelectList(db.Countries, "CountryId", "CountryName", user.Country.CountryName)

Then it throws a NullReferenceException at the controller. I understand why this is happening, but for the life of me can't find an answer after half a day's search.

I'm just wondering if there is a clean way to handle the null value and therefore, pass a different default value if CountryId is null?

Any help is always much appreciated.

Edit: I forgot to mention the viewmodel. I basically added all this to the IndexViewModel (although I'm not sure if I need it all at this point):

    public int CountryId { get; set; }
    public int StateId { get; set; }
    public int CityId { get; set; }

    public virtual Country Country { get; set; }
    public virtual State State { get; set; }
    public virtual City City { get; set; }

    public IEnumerable<SelectListItem> Countries { get; set; }
    public IEnumerable<SelectListItem> States { get; set; }
    public IEnumerable<SelectListItem> Cities { get; set; }

1 Answer 1

2

Firstly you cannot bind a dropdown to a complex type. Html has no concept of what your c# Country class is. You need to bind to your CountryId property

@Html.DropDownListFor(m => m.CountryId , Model.Countries)

Note also that Countries is already a SelectList so its a bit pointless and unnecessary extra overhead to then create another SelectList from it, so it's just Model.Countries, not new SelectList(Model.Countries, "Value", "Text")

Next, your binding to a property (CountryId) so its the value of CountryId that determines which option is selected - the 3rd parameter (Model.Country.CountryName) is just ignored. But even if it wasn't, it simply would not have selected anything because the value attributes of your options are the values of the CountryId property, not the CountryName property. You need to set the value of CountryId in the controller if Country exists.

Finally, your if (Model.Country.CountryName != null) { throws an exception because the value of Country is null (you cannot access the CountryName property of a null object).

The if/else block is completely unnecessary and you need just one line of code in the view

@Html.DropDownListFor(m => m.CountryId , Model.Countries, "-Please select-")

If the value of CountryId is 0 (the default for int) then the "-Please select-" option will be selected. If the value of CountryId matches one of the values in the collection, then that option will be selected when the view is rendered. Because CountryId is typeof int, then a validation error will be generated if the user does not select a valid country.

And finally, despite the IndexViewModel naming, it is not a view model! A view model includes only the properties you need to display/edit in a view so the 3 virtual properties you have should be removed (and probably the 4 properties relating to States and Cities but not sure if you have just omitted some code from you view and controller)

Sign up to request clarification or add additional context in comments.

6 Comments

Thank you, this helps tremendously. I wish I could find the magic resource to make all this .NET stuff make sense...or at least build on it, rather than feel like I'm on a wild goose chase. The msdn resource for DropDownListFor with TModel and TEnum just looks like a bunch of nonsense to me... The City and State items are handled by js, json and is also what was creating the NRE I mentioned in my comment to hutchonoid. Now that I have a somewhat better understanding, I'll go rework those as well. Thanks again...
This answer may help with the js (if that is a problem)
Thanks, that post is actually where I got some of my current code/ideas. I'm using your StateList and CityList idea and more. The issue now is while trying to generate the initial drop down lists for State and City inside the controller model (using your info above), there needs to be a where clause... so like db.States.Where(m => m.CountryId == user.CountryId) or something like that. But, once again, I'm on a wild goose chase... and have no idea if I should be doing this at the M..V... or C level... or even how to format the lambda or where to find a understandable reference for that...
You will need to initially generate an empty SelectList in the controller's GET method - something like model.States = Enumerable.Empty<SelectListItem>() assuming CountryId does not have a value, or if it does have a value then you would need to build the SelectList by querying the States table - something like var states = db.States.Where(s => s.CountryId = CountryId).Select(s => new SelectListItem() { Value = s.StateId.ToString(), Text = StateName }); Hard to give an exact answer without seeing more of your code so if your having problems, then you should ask a new question.
Thanks again, It may or may not have a value depending on if the user has set their locale. Enumerable.Empty<SelectListItem>() is what I was originally doing. Then, in my view I was doing this (State & City):<select id="State" name="state"><option value="@Model.State.StateId">@Model.State.StateName</option></select><br />as the initial value, but, now that you set me straight, that's blowing a NRE where it didn't seem to care before and I could just modify it as needed with my json StateList ActionResult. I'll try to digest the 2nd half of your comment and maybe present another question...
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.