Examining how ASP.NET MVC scaffolds the DropDownList Helper
In Solution Explorer, right-click the Controllers folder and then select Add Controller. Name the controller StoreManagerController. Set the options for the Add Controller dialog as shown in the image below.
Edit the StoreManager\Index.cshtml view and remove AlbumArtUrl
. Removing AlbumArtUrl
will make the presentation more readable. The completed code is shown below.
@model IEnumerable<MvcMusicStore.Models.Album>
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table>
<tr>
<th>
Genre
</th>
<th>
Artist
</th>
<th>
Title
</th>
<th>
Price
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Genre.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Artist.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.AlbumId }) |
@Html.ActionLink("Details", "Details", new { id=item.AlbumId }) |
@Html.ActionLink("Delete", "Delete", new { id=item.AlbumId })
</td>
</tr>
}
</table>
Open the Controllers\StoreManagerController.cs file and find the Index
method. Add the OrderBy
clause so the albums will be sorted by price. The complete code is shown below.
public ViewResult Index()
{
var albums = db.Albums.Include(a => a.Genre).Include(a => a.Artist)
.OrderBy(a => a.Price);
return View(albums.ToList());
}
Sorting by price will make it easier to test changes to the database. When you are testing the edit and create methods, you can use a low price so the saved data will appear first.
Open the StoreManager\Edit.cshtml file. Add the following line just after the legend tag.
@Html.HiddenFor(model => model.AlbumId)
The following code shows the context of this change:
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Album</legend>
@Html.HiddenFor(model => model.AlbumId)
<div class="editor-label">
@Html.LabelFor(model => model.GenreId, "Genre")
</div>
<div class="editor-field">
@Html.DropDownList("GenreId", String.Empty)
@Html.ValidationMessageFor(model => model.GenreId)
</div>
<!-- Items removed for brevity. -->
}
The AlbumId
is required to make changes to an album record.
Press CTRL+F5 to run the application. Select to the Admin link, then select the Create New link to create a new album. Verify the album information was saved. Edit an album and verify the changes you made are persisted.
The Album Schema
The StoreManager
controller created by the MVC scaffolding mechanism allows CRUD (Create, Read, Update, Delete) access to the albums in the music store database. The schema for album information is shown below:
The Albums
table does not store the album genre and description, it stores a foreign key to the Genres
table. The Genres
table contains the genre name and description. Likewise, the Albums
table doesn't contain the album artists name, but a foreign key to the Artists
table. The Artists
table contains the artist's name. If you examine the data in the Albums
table, you can see each row contains a foreign key to the Genres
table and a foreign key to the Artists
table. The image below show some table data from the Albums
table.
The HTML Select Tag
The HTML <select>
element (created by the HTML DropDownList helper) is used to display a complete list of values (such as the list of genres). For edit forms, when the current value is known, the select list can display the current value. We saw this previously when we set the selected value to Comedy. The select list is ideal for displaying category or foreign key data. The <select>
element for the Genre foreign key displays the list of possible genre names, but when you save the form the Genre property is updated with the Genre foreign key value, not the displayed genre name. In the image below, the genre selected is Disco and the artist is Donna Summer.
Examining the ASP.NET MVC Scaffolded Code
Open the Controllers\StoreManagerController.cs file and find the HTTP GET Create
method.
public ActionResult Create()
{
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name");
ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name");
return View();
}
The Create
method adds two SelectList objects to the ViewBag
, one to contain the genre information, and one to contain the artist information. The SelectList constructor overload used above takes three arguments:
public SelectList(
IEnumerable items,
string dataValueField,
string dataTextField
)
- items: An IEnumerable containing the items in the list. In the example above, the list of genres returned by
db.Genres
. - dataValueField: The name of the property in the IEnumerable list that contains the key value. In the example above,
GenreId
andArtistId
. - dataTextField: The name of the property in the IEnumerable list that contains the information to display. In both the artists and genre table, the
name
field is used.
Open the Views\StoreManager\Create.cshtml file and examine the Html.DropDownList
helper markup for the genre field.
@model MvcMusicStore.Models.Album
@* Markup removed for clarity.*@
@Html.DropDownList("GenreId", String.Empty)
The first line shows that the create view takes an Album
model. In the Create
method shown above, no model was passed, so the view gets a null Album
model. At this point we are creating a new album so we don't have any Album
data for it.
The Html.DropDownList overload shown above takes the name of the field to bind to the model. It also uses this name to look for a ViewBag object containing a SelectList object. Using this overload, you are required to name the ViewBag SelectList object GenreId
. The second parameter (String.Empty
) is the text to display when no item is selected. This is exactly what we want when creating a new album. If you removed the second parameter and used the following code:
@Html.DropDownList("GenreId")
The select list would default to the first element, or Rock in our sample.
Examining the HTTP POST Create
method.
//
// POST: /StoreManager/Create
[HttpPost]
public ActionResult Create(Album album)
{
if (ModelState.IsValid)
{
db.Albums.Add(album);
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name",
album.GenreId);
ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name",
album.ArtistId);
return View(album);
}
This overload of the Create
method takes an album
object, created by the ASP.NET MVC model binding system from the form values posted. When you submit a new album, if model state is valid and there are no database errors, the new album is added the database. The following image shows the creation of a new album.
You can use the fiddler tool to examine the posted form values that ASP.NET MVC model binding uses to create the album object.
.
Refactoring the ViewBag SelectList Creation
Both the Edit
methods and the HTTP POST Create
method have identical code to set up the SelectList in the ViewBag. In the spirit of DRY, we will refactor this code. We'll make use of this refactored code later.
Create a new method to add a genre and artist SelectList to the ViewBag.
private void SetGenreArtistViewBag(int? GenreID = null, int? ArtistID = null) {
if (GenreID == null)
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name");
else
ViewBag.GenreId = new SelectList(db.Genres.ToArray(), "GenreId", "Name", GenreID);
if (ArtistID == null)
ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name");
else
ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", ArtistID);
}
Replace the two lines setting the ViewBag
in each of the Create
and Edit
methods with a call to the SetGenreArtistViewBag
method. The completed code is shown below.
//
// GET: /StoreManager/Create
public ActionResult Create() {
SetGenreArtistViewBag();
return View();
}
//
// POST: /StoreManager/Create
[HttpPost]
public ActionResult Create(Album album) {
if (ModelState.IsValid) {
db.Albums.Add(album);
db.SaveChanges();
return RedirectToAction("Index");
}
SetGenreArtistViewBag(album.GenreId, album.ArtistId);
return View(album);
}
//
// GET: /StoreManager/Edit/5
public ActionResult Edit(int id) {
Album album = db.Albums.Find(id);
SetGenreArtistViewBag(album.GenreId, album.ArtistId);
return View(album);
}
//
// POST: /StoreManager/Edit/5
[HttpPost]
public ActionResult Edit(Album album) {
if (ModelState.IsValid) {
db.Entry(album).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
SetGenreArtistViewBag(album.GenreId, album.ArtistId);
return View(album);
}
Create a new album and edit an album to verify the changes work.
Explicitly Passing the SelectList to the DropDownList
The create and edit views created by the ASP.NET MVC scaffolding use the following DropDownList overload:
public static MvcHtmlString DropDownList(
this HtmlHelper htmlHelper,
string name, // The name of the ViewModel property to bind.
string optionLabel // The string added to the top of the list
// typically String.Empty or "Select a Genre"
)
The DropDownList
markup for the create view is shown below.
@Html.DropDownList("GenreId", String.Empty)
Because the ViewBag
property for the SelectList
is named GenreId
, the DropDownList helper will use the GenreId
SelectList in the ViewBag. In the following DropDownList overload, the SelectList
is explicitly passed in.
public static MvcHtmlString DropDownList(
this HtmlHelper htmlHelper,
string name, // The name of the ViewModel property to bind.
IEnumerable selectList // The SelectList
)
Open the Views\StoreManager\Edit.cshtml file, and change the DropDownList call to explicitly pass in the SelectList, using the overload above. Do this for the Genre category. The completed code is shown below:
@Html.DropDownList("GenreId", ViewBag.GenreId as SelectList)
Run the application and click the Admin link, then navigate to a Jazz album and select the Edit link.
Instead of showing Jazz as the currently selected genre, Rock is displayed. When the string argument (the property to bind) and the SelectList object have the same name, the selected value is not used. When there is no selected value provided, browsers default to the first element in the SelectList(which is Rock in the example above). This is a known limitation of the DropDownList helper.
Open the Controllers\StoreManagerController.cs file and change the SelectList object names to Genres
and Artists
. The completed code is shown below:
private void SetGenreArtistViewBag(int? GenreID = null, int? ArtistID = null) {
if (GenreID == null)
ViewBag.Genres = new SelectList(db.Genres, "GenreId", "Name");
else
ViewBag.Genres = new SelectList(db.Genres.ToArray(), "GenreId", "Name", GenreID);
if (ArtistID == null)
ViewBag.Artists = new SelectList(db.Artists, "ArtistId", "Name");
else
ViewBag.Artists = new SelectList(db.Artists, "ArtistId", "Name", ArtistID);
}
The names Genres and Artists are better names for the categories, as they contain more than just the ID of each category. The refactoring we did earlier paid off. Instead of changing the ViewBag in four methods, our changes were isolated to the SetGenreArtistViewBag
method.
Change the DropDownList call in the create and edit views to use the new SelectList names. The new markup for the edit view is shown below:
<div class="editor-label">
@Html.LabelFor(model => model.GenreId, "Genre")
</div>
<div class="editor-field">
@Html.DropDownList("GenreId", ViewBag.Genres as SelectList)
@Html.ValidationMessageFor(model => model.GenreId)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.ArtistId, "Artist")
</div>
<div class="editor-field">
@Html.DropDownList("ArtistId", ViewBag.Artists as SelectList)
@Html.ValidationMessageFor(model => model.ArtistId)
</div>
The Create view requires an empty string to prevent the first item in the SelectList from being displayed.
<div class="editor-label">
@Html.LabelFor(model => model.GenreId, "Genre" )
</div>
<div class="editor-field">
@Html.DropDownList("GenreId", ViewBag.Genres as SelectList, String.Empty)
@Html.ValidationMessageFor(model => model.GenreId)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.ArtistId, "Artist")
</div>
<div class="editor-field">
@Html.DropDownList("ArtistId", ViewBag.Artists as SelectList, String.Empty)
@Html.ValidationMessageFor(model => model.ArtistId)
</div>
Create a new album and edit an album to verify the changes work. Test the edit code by selecting an album with a genre other than Rock.
Using a View Model with the DropDownList Helper
Create a new class in the ViewModels folder named AlbumSelectListViewModel
. Replace the code in the AlbumSelectListViewModel
class with the following:
using MvcMusicStore.Models;
using System.Web.Mvc;
using System.Collections;
namespace MvcMusicStore.ViewModels {
public class AlbumSelectListViewModel {
public Album Album { get; private set; }
public SelectList Artists { get; private set; }
public SelectList Genres { get; private set; }
public AlbumSelectListViewModel(Album album,
IEnumerable artists,
IEnumerable genres) {
Album = album;
Artists = new SelectList(artists, "ArtistID", "Name", album.ArtistId);
Genres = new SelectList(genres, "GenreID", "Name", album.GenreId);
}
}
}
The AlbumSelectListViewModel
constructor takes an album, a list of artists and genres and creates an object containing the album and a SelectList
for genres and artists.
Build the project so the AlbumSelectListViewModel
is available when we create a view in the next step.
Add an EditVM
method to the StoreManagerController
. The completed code is shown below.
//
// GET: /StoreManager/EditVM/5
public ActionResult EditVM(int id) {
Album album = db.Albums.Find(id);
if (album == null)
return HttpNotFound();
AlbumSelectListViewModel aslvm = new AlbumSelectListViewModel(album, db.Artists, db.Genres);
return View(aslvm);
}
Right click AlbumSelectListViewModel
, select Resolve, then using MvcMusicStore.ViewModels;.
Alternatively, you can add the following using statement:
using MvcMusicStore.ViewModels;
Right click EditVM
and select Add View. Use the options shown below.
Select Add, then replace the contents of the Views\StoreManager\EditVM.cshtml file with the following:
@model MvcMusicStore.ViewModels.AlbumSelectListViewModel
@{
ViewBag.Title = "EditVM";
}
<h2>Edit VM</h2>
@using (Html.BeginForm("Edit","StoreManager",FormMethod.Post)) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Album</legend>
@Html.HiddenFor(model => model.Album.AlbumId )
<div class="editor-label">
@Html.LabelFor(model => model.Album.GenreId, "Genre")
</div>
<div class="editor-field">
@Html.DropDownList("Album.GenreId", Model.Genres)
@Html.ValidationMessageFor(model => model.Album.GenreId)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Album.ArtistId, "Artist")
</div>
<div class="editor-field">
@Html.DropDownList("Album.ArtistId", Model.Artists)
@Html.ValidationMessageFor(model => model.Album.ArtistId)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Album.Title)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Album.Title)
@Html.ValidationMessageFor(model => model.Album.Title)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Album.Price)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Album.Price)
@Html.ValidationMessageFor(model => model.Album.Price)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Album.AlbumArtUrl)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Album.AlbumArtUrl)
@Html.ValidationMessageFor(model => model.Album.AlbumArtUrl)
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
The EditVM
markup is very similar to the original Edit
markup with the following exceptions.
- Model properties in the
Edit
view are of the formmodel.property
(for example,model.Title
). Model properties in theEditVm
view are of the formmodel.Album.property
(for example,model.Album.Title
). That's because theEditVM
view is passed a container for anAlbum
, not anAlbum
as in theEdit
view. - The DropDownList second parameter comes from the view model, not the ViewBag.
- The BeginForm helper in the
EditVM
view explicitly posts back to theEdit
action method. By posting back to theEdit
action, we don't have to write anHTTP POST EditVM
action and can reuse theHTTP POST
Edit
action.
Run the application and edit an album. Change the URL to use EditVM
. Change a field and hit the Save button to verify the code is working.
Which Approach Should You Use?
All three approaches shown are acceptable. Many developers prefer to explicitly pass the SelectList
to the DropDownList
using the ViewBag
. This approach has the added advantage of giving you the flexibility of using a more appropriate name for the collection. The one caveat is you cannot name the ViewBag SelectList
object the same name as the model property.
Some developers prefer the ViewModel approach. Others consider the more verbose markup and generated HTML of the ViewModel approach a disadvantage.
In this section we have learned three approaches to using the DropDownList with category data. In the next section, we'll show how to add a new category.