amartin amartin - 4 months ago 26
ASP.NET (C#) Question

Model Binding with Forms: IEnumerable<ModelName> vs. IEnumerable<Object>

I am trying to learn to program with Asp.Net MVC and am struggling to understand this. Can someone please explain the following:

If I use the following ViewModel, Controller, and View, the ModelState.IsValid in the POST method evaluates to true, and I am able to work with the data from the form.

ViewModel





public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }

[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }

[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }

public int UsersCompany { get; set; }

[Display(Name = "Company")]
public IEnumerable<object> Companies { get; set; }

public IEnumerable<object> Roles { get; set; }
}


Controller





// GET: /Users/Create
public async Task<ActionResult> Create()
{
RegisterViewModel registerViewModel = new RegisterViewModel();

//Get the list of Roles
var allRoles = await RoleManager.Roles.ToListAsync();

registerViewModel.Roles = allRoles.ToList();
ApplicationUser user = System.Web.HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>()
.FindById(System.Web.HttpContext.Current.User.Identity.GetUserId());

var company = await db.CompanyModels.FindAsync(user.CompanyModelId);

var companyAcct = await db.CompanyAccountModels.FindAsync(company.CompanyAccountModelId);

var companies = db.CompanyModels
.Where(c => c.CompanyAccountModelId == companyAcct.Id);

registerViewModel.UsersCompany = user.CompanyModelId;
registerViewModel.Companies = companies;

return View(registerViewModel);
}


View Snippet





<div class="form-group">
@Html.LabelFor(c => c.Companies, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.DropDownListFor(c => c.Companies, new SelectList(Model.Companies, "Id", "CompanyName"), new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.Companies, "", new { @class = "text-danger" })
</div>
</div>


But if I use a type of MyModel in the ViewModel IEnumerable, as shown below, the following error occurs when ModelState is checked in the POST method:

Message "The parameter conversion from type 'System.String' to type 'xyz.Models.CompanyModel' failed because no type converter can convert between these types." string

I am reading this error in the values field in the live variable data you can see when in debugger mode with breakpoints.

public class RegisterViewModel
{
//snipped

[Display(Name = "Company")]
public IEnumerable<CompanyModel> Companies { get; set; }

//snipped
}


So the main question is just around why this happens, and a related question is should I be doing anything differently here. Thanks in advance for any feedback.

Please also feel free to comment on any bad practices or boneheaded things I'm doing within this code.

Answer

The first argument in the Html.DropDownListFor method is the value which will be filled by the selected item in the dropdown.

@Html.DropDownListFor(
    c => c.Companies, // the selected item in the collection (this is the problem line)
    new SelectList(Model.Companies, "Id", "CompanyName"), // the collection
    new { @class = "form-control" })

The line from the code above is trying to bind the selected value from the dropdown (the Id property of the selected CompanyModel) to the entire collection of CompanyModels.

The reason that IEnumerable<object> works is because MVC is able to bind the selected value and turn it into a IEnumerable<object> with only one item in it (which is some company Id as a string). MVC can figure out how to bind "2" to an IEnumerable<object>, but not an IEnumerable<CompanyModel>.

What you'll want to do instead is to create a property for the selected value. Here's an example:

public class RegisterViewModel
{
    [Display(Name = "Company")]
    public IEnumerable<CompanyModel> Companies { get; set; }

    [Required]
    public int? SelectedCompanyId { get; set; }
}
<div class="form-group">
    @Html.LabelFor(c => c.SelectedCompanyId, new { @class = "col-md-2 control-label" })
    <div class="col-md-10">
        @Html.DropDownListFor(c => c.SelectedCompanyId, new SelectList(Model.Companies, "Id", "CompanyName"), new { @class = "form-control" })
        @Html.ValidationMessageFor(model => model.SelectedCompanyId, "", new { @class = "text-danger" })
    </div>
</div>