Using Dropdowns in Grids
In our second post of the series, we look at using dropdowns inside and outside of WebGrids.
The Ultimate Guide to Dropdowns in ASP.NET MVC Series
- Introduction
- Basics of ASP.NET MVC DropdownLists
- Dropdowns in WebGrids
- Cascading Dropdowns
- Creating a Suggestion Dropdown
- *NEW* Creating a Multi-column Dropdown
One of the biggest challenges I keep hearing about with ASP.NET MVC is the lack of a "postback" similar to WebForms.
The funny part is that postbacks never went away. They are just executed differently.
In today's post, we examine a way to perform automatic postbacks along with using dropdowns inside a WebGrid.
Test Data
We need some test data for our specific dropdown and WebGrid.
I decided to grab a github repository of over 7,000 vehicle makes and models by year from N8barr (Thanks, Nate), but don't worry, we are only using about 800 records for our test.
This test data (800 records only) was imported into a new table called VehicleModelYear (in app_data).
Now that we have our test data, we can start experimenting with dropdowns and WebGrids.
Laying the Foundation
I will create a Vehicle repository, ViewModel, and Model for our example as I did before with our MonthData from the previous dropdown example.
Repository/VehicleRepository.cs
public class VehicleRepository : AdoRepository<Vehicle> { public VehicleRepository(string connectionString) : base(connectionString) { }
public IEnumerable<Vehicle> GetAll() { using (var command = new SqlCommand("SELECT Id, Year, Make, Model FROM VehicleModelYear")) { return GetRecords(command); } }
public Vehicle GetById(string id) { using (var command = new SqlCommand("SELECT Id, Year, Make, Model FROM VehicleModelYear WHERE Id = @id")) { command.Parameters.Add(new ObjectParameter("id", id)); return GetRecord(command); } }
public override Vehicle PopulateRecord(SqlDataReader reader) { return new Vehicle { Id = reader.GetInt32(0), Year = reader.GetInt32(1), Make = reader.GetString(2), Model = reader.GetString(3) }; } }
ViewModels/VehicleViewModel.cs
public class VehicleViewModel { public IEnumerable<Vehicle> AllVehicles { get; set; }
public int SelectedYear { get; set; }
public IEnumerable<Vehicle> SelectedVehicles { get; set; }
public IEnumerable<SelectListItem> GetVehicleYearSelectList(int defaultYear = 0) { return AllVehicles .Distinct(new VehicleYearComparer()) .OrderBy(e => e.Year) .Select((e, i) => new SelectListItem { Text = e.Year.ToString(), Value = e.Year.ToString(), Selected = e.Year == defaultYear }); } }
public class VehicleYearComparer : IEqualityComparer<Vehicle> { public bool Equals(Vehicle x, Vehicle y) { return x.Year.Equals(y.Year); }
public int GetHashCode(Vehicle obj) { return obj.Year.GetHashCode(); } }
Models/Vehicle.cs
public class Vehicle { public int Id { get; set; } public int Year { get; set; } public string Make { get; set; } public string Model { get; set; } }
As you can see in our VehicleViewModel, we have two types of IEnumerable vehicles: All of the vehicles and the SelectedVehicles.
The SelectedVehicles are the filtered vehicles by year. We could apply caching to the AllVehicles so we aren't hitting the database so often.
We've also added a IEqualityComparer<Vehicle> for creating a Distinct list of years in chronological order and finally, we create a simple IEnumerable<SelectListItem> out of them.
All ready for the View (No, not the TV show).
Building the View
Our View is made up of minimal JavaScript and some C# code.
The goal here is to have a list of years in the dropdown and as a user selects one from the dropdown, it will automatically submit the form ("postback") and prepare our filtered list of cars for our selected year.
Here is what our View looks like.
Views/Home/WebGrid.cshtml
@{ ViewBag.Title = "WebGrid Dropdown"; var grid = new WebGrid(Model.SelectedVehicles.OrderBy(e=> e.Make), canPage: false); } @model DropDownDemo.ViewModels.VehicleViewModel
<h3>WebGrid Example</h3>
@@using (Html.BeginForm("WebGrid", "Home", FormMethod.Post, new { @@class = "vehicle-form form-horizontal" })) { <div class="form-group"> @@Html.Label("Year", "Year:", new { @@class = "col-sm-1 control-label" }) <div class="col-sm-2"> @@Html.DropDownListFor(model => model.SelectedYear, Model.GetVehicleYearSelectList(), new { @class = "vehicle-year form-control" }) </div> </div>
@@MvcHtmlString.Create( grid.GetHtml( tableStyle: "table table-bordered table-striped table-condensed", htmlAttributes: new { id = "grid" }, columns: grid.Columns( grid.Column("Make" , "Make"), grid.Column("Model", "Model") )).ToHtmlString() ) }
<script> document.getElementsByClassName('vehicle-year')[0].onchange = function () { document.getElementsByClassName('vehicle-form')[0].submit(); }; </script>
At the bottom of the View, we have our JavaScript (mind you, no jQuery, but you could use it) submit the form ("vehicle-form") when a user changes the "vehicle-year" dropdown.
This form submit will perform a postback, try to create a new VehicleViewModel based on the data it has available in the View, and send the VehicleViewModel over to the POST of the WebGrid in the controller.
Controller Code
The controller code is pretty simple.
Controllers/HomeController.cs
.
.
public ActionResult WebGrid() { var model = new VehicleViewModel { AllVehicles = _vehicleRepository.GetAll() }; // Grab the first year. var firstYear = model.GetVehicleYearSelectList().First().Value; model.SelectedVehicles = model.AllVehicles.Where(e => e.Year.ToString() == firstYear);
return View(model); }
[HttpPost] public ActionResult WebGrid(VehicleViewModel model) { model.AllVehicles = _vehicleRepository.GetAll(); model.SelectedVehicles = model.AllVehicles.Where(e => e.Year == model.SelectedYear);
return View(model); }
When the controller is called for an initial GET, we need to grab the first year in the sorted SelectList. Once we have that, we can filter out the other vehicles and display only the first year's cars.
When a POST occurs, the model is returned and we only need the SelectedYear to assign the SelectedVehicles to the model.
This makes our cycle of displaying yearly cars even easier.
Dropdown in a WebGrid
So how do we get a Dropdown inside of a WebGrid? For maybe a rating of a particular vehicle?
The rating control will be a dropdown with a list of possible ratings from 1 (Poor) to 5 (Excellent).
For our rating to work, we need to modify our WebGrid (in our WebGrid.cshtml) a bit by adding a new column.
@@MvcHtmlString.Create( grid.GetHtml( tableStyle: "table table-bordered table-striped table-condensed", htmlAttributes: new { id = "grid" }, columns: grid.Columns( grid.Column("Make" , "Make"), grid.Column("Model", "Model"), grid.Column("Rating", format: item => Html.RatingDropDown(item.Value as Vehicle)) )).ToHtmlString() )
This new code includes a column containing an HtmlHelper class called a RatingDropDown. It was previously introduced in my WebGrid series.
Our HtmlHelper class creates a dropdown based on the Vehicle ratings.
Helpers/Html/HtmlExtensions.cs
public static class HtmlExtensions { public static HtmlString RatingDropDown(this HtmlHelper helper, Vehicle vehicle) { var ratings = GetRatingList(vehicle.Rating);
var formatName = HtmlHelper.GenerateIdFromName(nameof(vehicle.Rating)); var uniqueId = $"{formatName}_{vehicle.Id}"; var input = helper.DropDownList(uniqueId, ratings, new { @class = "input-sm" });
return new HtmlString(input.ToString()); }
private static IEnumerable<SelectListItem> GetRatingList(int vehicleRating) { var ratings = new List<SelectListItem> { new SelectListItem {Text = "1 - Poor", Value = "1"}, new SelectListItem {Text = "2 - Fair", Value = "2"}, new SelectListItem {Text = "3 - Good", Value = "3"}, new SelectListItem {Text = "4 - Great", Value = "4"}, new SelectListItem {Text = "5 - Excellent", Value = "5"} }; ratings.ForEach(e=> e.Selected = vehicleRating.ToString() == e.Value);
return ratings; } }
Once these are in place, we can run the app and we have our rating dropdown on every row.
Conclusion
As you can see, dropdowns become easier to use the more you work with them.
If we wanted to improve this screen, we could do the following:
- As I mentioned before, we could add a caching routine to the AllVehicles to make the postback even faster.
- Another method would be to eliminate the postback altogether and perform SignalR or WebAPI calls to retrieve the data and display the grid dynamically.
- An exercise for the readers would be to save the rating back to the database, again, using either SignalR or WebAPI.
If you are working with a number of controls in a table format, it makes more sense to use tables instead of WebGrids. It'll make things a heck of a lot easier with a for..each loop and table tags.
It all renders the same anyway, right? ;-)
Did you run across a different way of creating Dropdowns in a WebGrid? Post your comments below and let's discuss.
The Ultimate Guide to Dropdowns in ASP.NET MVC Series
- Introduction
- Basics of ASP.NET MVC DropdownLists
- Dropdowns in WebGrids
- Cascading Dropdowns
- Creating a Suggestion Dropdown
- *NEW* Creating a Multi-column Dropdown