BradA
BradA

Reputation: 3

Select Tag Helper to Display 2 Fields in the Drop Down List

Using the standard scaffolding only displays the indexes in the drop down of related tables. How to display multiple fields so the drop down list is meaningful?

I have attempted to use a solution outlined by Shyju in the section 'Getting data from your database table using entity framework'. (Select Tag Helper in ASP.NET Core MVC).

I have 2 classes in the model (Books with related Author):

namespace SimpleAppwithTwoTables.Data
{
    public class Book
    {
        public int BookID { get; set; }
        [StringLength(255)]
        [Display(Name ="Book Title")]
        public string Title { get; set; }
        public int AuthorID { get; set; }
        public Author Author { get; set; }
     }
}

and

namespace SimpleAppwithTwoTables.Data
{
    public class Author
    {
        public int AuthorID { get; set; }
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstName { get; set; }
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }
        public ICollection<Book> Books { get; set; } = new List<Book>();
    }
}

The BooksController has a Create() method similar to what was described in the post from Shyju.

 public IActionResult Create()
            Author vm = new Author();
            vm.Books = _context.Book.Select(a => new SelectListItem()
            { Value = a.AuthorID.ToString(), Text = a.Author.LastName }).ToList();
            return View(vm);
        }

This line in the controller

 vm.Books = _context.Book.Select(a => new SelectListItem()
            { Value = a.AuthorID.ToString(), Text = a.Author.LastName }).ToList();

Generates this error:

"Cannot implicitly convert type 'System.Collections.Generic.List<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>' to 'System.Collections.Generic.ICollection<SimpleAppwithTwoTables.Data.Book>'. An explicit conversion exists (are you missing a cast?).

Note that I am new to C# and understand that this is a type conversion issue and that the line with the error is expecting that the Author model contains an ICollection<>.

I am looking for the code needed in the vm.books line that does not do a type conversion and works with the ICollection.

Upvotes: 0

Views: 1934

Answers (2)

BradA
BradA

Reputation: 3

I followed the advice Chris Pratt provided above. It took a while to figure out but almost having working. See details below.

See the model for Book and Author above.

Created a new ViewModel:

namespace SimpleDropDownList.Models
{
    [NotMapped]
    public class AuthorViewModel
    {
        //Property to hold the list of authors in the GET
        public IEnumerable<SelectListItem> AuthorOptions { get; set; }

        //Property to bind the selected author used in the POST
        public List<int> SelectedAuthorIds { get; set; }
    }
}

Changed the Create() method on the BooksController to:

 public IActionResult Create()
        {
            //ViewData["AuthorID"] = new SelectList(_context.Set<Author>(), "AuthorID", "AuthorID");
            AuthorViewModel vm = new AuthorViewModel();
            vm.AuthorOptions = _context.Book.Select(x => new SelectListItem()
            { Value = x.AuthorID.ToString(), Text = x.Author.LastName }).ToList();
        return View(vm);
        }

Change the Create view to:

@model SimpleDropDownList.Models.Book

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Book</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="AuthorID" class="control-label"></label>
                @*<select asp-for="AuthorID" class ="form-control" asp-items="ViewBag.AuthorID"></select>*@

                <select asp-for="@Model.AuthorViewModel.SelectedAuthorIds" asp-items="@Model.AuthorViewModel.AuthorOptions"></select>
                
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Model: AuthorViewModel

When I run this and click the Create button the following error is generated:

An unhandled exception occurred while processing the request. InvalidOperationException: The model item passed into the ViewDataDictionary is of type 'SimpleDropDownList.Models.AuthorViewModel', but this ViewDataDictionary instance requires a model item of type 'SimpleDropDownList.Models.Book'.

I'm flagging this question as completed and have created a new question for the issue describe just above. See here: Error when Razor page opens and have a related drop down list

Upvotes: 0

Chris Pratt
Chris Pratt

Reputation: 239380

You're trying to stuff a list of SelectListItem into an property defined as a list of Book. SelectListItem quite obviously is not the same things as Book, so it fails.

The simple solution is that you need a property on your view model specifically for holding your select list options:

public IEnumerable<SelectListItem> AuthorOptions { get; set; }

You will also need something to bind the select items to, which will be primitive types, not actual Author instances:

public List<int> SelectedAuthorIds { get; set; }

In your view, then:

<select asp-for="SelectedAuthorIds" asp-items="AuthorOptions">

On post, you will then need to use that collection of ids to query the actual author objects, if you need to associate them with a book or something else:

var selectAuthors = await _context.Authors.Where(x => vm.SelectedAuthorIds.Contains(x.Id)).ToListAsync();

Upvotes: 1

Related Questions