Search code examples
asp.net-mvcasp.net-coreviewbagvisual-studio-2015asp.net-core-mvc

The ViewData item that has the key 'ShelfId' is of type 'System.Int32' but must be of type 'IEnumerable<SelectListItem>'


Problem

I use the following code very similarily somewhere else in my application, but it is not working. I am completely stumped.

The ViewData item that has the key 'ShelfId' is of type 'System.Int32' but must be of type 'IEnumerable<SelectListItem>'

This is thrown during the post method. My model state is invalid.

Code

Models

Shelf

public class Shelf 
{
      [Key]
      public int ShelfId 

      [Display(Name = "Shelf Id")]
      [Required]
      public string ShelfName

      public virtual List<Book> Books {get; set;}

}

Book

public class Book 
{
      public int BookId 

      [Required]
      [StrengthLength(160, MinimumLength = 8)]
      public string BookName

      public int ShelfId

      public Shelf shelf {get; set;}


}

Controller

// GET: Units/Create
        public async Task<IActionResult> Create()
        {


            var shelves = await _db.Shelves.OrderBy(q => q.Name).ToListAsync();
            ViewBag.SelectedShelves = new SelectList(shelves, "ShelfId", "Name");
            return View();
        }

        // POST: Units/Create
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create(Book book)
        {
            book.CreatedBy = User.Identity.GetUserName();
            book.Created = DateTime.UtcNow;
            book.UpdatedBy = User.Identity.GetUserName();
            book.Updated = DateTime.UtcNow;

            if (ModelState.IsValid)
            {
                db.Units.Add(unit);
                await db.SaveChangesAsync();
                return RedirectToAction("Index");
            }

            return View(book);
        }

view

@model AgentInventory.Models.Book

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Create Unit</title>
</head>
<body>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal well bs-component" style="margin-top:20px">
        <h4>Unit</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            <div class="control-label col-md-2">Room</div>
            <div class="col-md-10">
                @Html.DropDownListFor(model => model.ShelfId, (SelectList)ViewBag.SelectedShelves, "All", new { @class = "form-control" })
             </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.BookName, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.BookName, "", new { @class = "text-danger" }
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

Attempts

I tried:

  • Adding @Html.HiddenFor(model=>model.ShelfId) in the create view, but that didn't work.
  • I have looked at similar issues on stackoverflow, but none of the fixes worked for me. (IE - hiddenfor, different kinds of selectlists)

Since I am new to MVC framework, I would be grateful for any assistance. I don't understand why this code works for two other kinds of models (Building and room), but not my current two models? It's weird.

PS - Is there a way to do this easily without using viewbag as well?


Solution

  • The reason for the error is that in the POST method when you return the view, the value of ViewBag.SelectedShelves is null because you have not set it (as you did in the get method. I recommend you refactor this in a private method that can be called from both the GET and POST methods

    private void ConfigureViewModel(Book book)
    {
      var shelves = await _db.Shelves.OrderBy(q => q.Name).ToListAsync();
      // Better to have a view model with a property for the SelectList
      ViewBag.SelectedShelves = new SelectList(shelves, "ShelfId", "Name");
    }
    

    then in the controller

    public async Task<IActionResult> Create()
    {
      // Always better to initialize a new object and pass to the view
      Book model = new Book();
      ConfigureViewModel(model)
      return View(model);
    }
    
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Create(Book book)
    {
      if (!ModelState.IsValid)
      {
        ConfigureViewModel(book)
        return View(book);
      }
      // No point setting these if the model is invalid
      book.CreatedBy = User.Identity.GetUserName();
      book.Created = DateTime.UtcNow;
      book.UpdatedBy = User.Identity.GetUserName();
      book.Updated = DateTime.UtcNow;
      // Save and redirect
      db.Units.Add(unit);
      await db.SaveChangesAsync();
      return RedirectToAction("Index");
    }
    

    Note your Book class contains only fields, not properties (no { get; set; }) so no properties will be set and the model will always be invalid because BookName has Required and StringLength attributes.

    Also you have not shown all the properties in your model (for example you have CreatedBy, Created etc. and its likely that ModelState will also be invalid because you only generate controls for only a few properties. If any other properties contain validation attributes, then ModelState will be invalid. To handle this you need to create a view model containing only the properties you want to display edit.

    public class BookVM
    {
      public int Id { get; set; }
      [Required]
      [StrengthLength(160, MinimumLength = 8)]
      public string Name { get; set; }
      public int SelectedShelf { get; set; }
      public SelectList ShelfList { get; set; }
    }
    

    Then modify the private method to assign the SelectList to the view model (not ViewBag, and in the controller methods, pass a new instance of BookVM to the view, and post back to

    public async Task<IActionResult> Create(BookVM model)
    {
      if (!ModelState.IsValid)
      {
        ConfigureViewModel(model)
        return View(model);
      }
      // Initialize a new Book and set the properties from the view model
    }