ASP.NET MVC: Enhancing The WebGrid - Batch Processing
This week, we are answering ASP.NET MVC WebGrid questions. Today, I show you how to perform batch processing on a WebGrid.
Last week, I posted an ASP.NET MVC WebGrid project to build up an awesome WebGrid control from scratch using user requests. With these WebGrid posts in the coming days, I want to push the boundaries of this series.
So if you have any suggestions on how to make this WebGrid even better, post a comment below.
Today, I'll show you how to do multiple batch processes.
Dressing up the Grid
The user interface of a WebGrid needs to look decent. It has to be a WebGrid that everyone would love to use.
This is why I love Twitter Bootstrap.
One of the benefits of adding Bootstrap is that you don't have to think about making the UI look decent. Twitter has already thought through all of the CSS layout details and indicators to make your design look decent.
Also, if you noticed from the source code, I forgot to include Twitter Bootstrap, so I added a couple table classes to make it look a little sexier.
Along with the CSS styles, I moved the grid out to it's own "control" so it can provide a more modular approach to our web app (don't worry, everything is in GitHub).
Make me a Toolbar!
We need a way to kick off our processes, whether it be deleting or exporting records so let's add a toolbar.
The awesome thing about this is that Twitter Bootstrap already has the CSS for a NavBar.
So let's add this above our grid.
<div class="navbar navbar-default" role="search"> <ul class="nav navbar-nav navbar-form"> <li></li> </ul> </div>
Now we need to add some buttons to it.
As a simple demo, I'm going to use the GlyphIcons from Bootstrap and create a Refresh button.
<div class="navbar navbar-default" role="search"> <ul class="nav navbar-nav navbar-form"> <li> <button id="btnRefresh" href="#" title="Refresh" class="btn btn-default btn-sm"> <i class="glyphicon glyphicon-refresh"></i> </button> </li> </ul> </div>
There! Now we have our toolbar.
Forgot the form!
There I go again...getting ahead of myself.
We need a form to actually post the data back to the server. So in the UserGrid, let's have it post data to ModelBinder to keep things even cleaner.
If you are using native types, using the standard model binders are fine. However, if you are doing something special with returning a list of items (like now), you may need a custom ModelBinder. The ModelBinders are a way to handle specialized properties in case you have some serious data handling issues.
What we need is a property on the UserViewModel to have the list of selected user ids. Here is the code for the UserViewModel ModelBinder and the UserViewModel class.
ViewModel\UserViewModel.cs
using System.Collections.Generic; using System.Web.Mvc; using WebGridExample.ModelBinders; using WebGridExample.Models; namespace WebGridExample.ViewModel { [ModelBinder(typeof(UserViewModelBinder))] public class UserViewModel { public IEnumerable<User> Users { get; set; } public User User { get; set; } public IEnumerable<User> SelectedUsers { get; set; } } }
ModelBinders\UserViewModelBinder.cs
using System; using System.Linq; using System.Web.Mvc; using WebGridExample.Models; using WebGridExample.ViewModel; namespace WebGridExample.ModelBinders { public class UserViewModelBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { var request = controllerContext.HttpContext.Request; var userIdList = request.Form.Get("select"); var list = userIdList.Split(','); return new UserViewModel { SelectedUsers = list.Select(e=> new User{Id = Convert.ToInt32(e)}) }; } } }
Checkboxes? Check!
Next, we need a way to select records in the WebGrid.
Since we'll be applying a number of processes to records, we need a way to have a checkbox to the left of each row and one in the header for selecting multiple checkboxes.
Unfortunately, we don't have an easy way to create a checkbox in the header so we need to get creative.
In the columns list, let's add a new dummy column with a strange header.
@MvcHtmlString.Create( grid.GetHtml( htmlAttributes: new { id = "grid", @class = "table table-bordered table-striped table-condensed" }, emptyRowCellValue: "No Records Found", headerStyle: "grid-header", columns: grid.Columns( grid.Column(header: "{CheckBoxHeading}", format: @<text><input id="select" class="box" name="select" type="checkbox" value="@item.Value.Id" /></text>, style: "text-center checkbox-width"), grid.Column("UserName", "User Name", @<text>@item.Value.UserName</text>), grid.Column("FirstName", "First Name", @<text>@item.Value.FirstName</text>), grid.Column("LastName", "Last Name", @<text>@item.Value.LastName</text>), grid.Column("LastLogin", "Last Login", @<text>@item.Value.LastLogin.ToString()</text>) ) ) .ToString() .Replace("{CheckBoxHeading}", "<div class='text-center'><input type='checkbox' id='allBox'/></div>") )
What I've done is created a placeholder in the header of the checkbox column for processing later. The "format" of the checkbox column is simply a checkbox with two CSS styles added to it: text-center and checkbox-width. Those two, as you would guess, centers the checkbox and gives it a width of 50 pixels.
If you notice, I also gave each checkbox the same name of "select." When we select each record in the grid and we post the form back to the server, all of the items checked will be a comma-delimited list in a variable called "select." This is the list we will use for our batch processing.
What's with the .Replace() at the end?
Remember at the top, when we call our grid.GetHtml(), it returns a simple string of what the entire WebGrid will look like in HTML. This will replace the placeholder in the checkbox column header with a "select all" checkbox.
Our JavaScript will attach to this checkbox when rendered out to the browser. The JavaScript is placed at the bottom of the HTML.
@using System.Web.Mvc.Html @model WebGridExample.ViewModel.UserViewModel <!DOCTYPE html> <html> <head> <title>WebGrid Example</title> <script src="~/Scripts/jquery-2.1.3.min.js"></script> <link href="~/Content/bootstrap.min.css" rel="stylesheet"/> <style> .text-center { text-align: center } .checkbox-width { width: 50px } </style> </head> <body> <h2>WebGrid Example</h2> @if (Model != null) { <div id="userGrid"> @Html.Partial("userGrid", Model.Users) </div> } <script type="text/javascript"> $(function() { $("#allBox").on("click", function () { $("[name=select]").prop("checked", $("#allBox").is(":checked")); }); }); </script> </body> </html>
How to remove records
So now that we have our WebGrid in our form, we need a button to do something. Let's perform a delete on the records.
We add a new button to our toolbar with a delete icon on it. The bold are the changes made in the UserGrid.cshtml file.
<div class="navbar navbar-default" role="search"> <ul class="nav navbar-nav navbar-form"> <li> <button id="btnRefresh" href="#" title="Refresh" class="btn btn-default btn-sm"> <i class="glyphicon glyphicon-refresh"></i> </button> </li> <li> <button type="submit" id="btnDelete" name="btnDelete" value="1" title="Delete" class="btn btn-default btn-sm"> <i class="glyphicon glyphicon-remove"></i> </button> </li> </ul> </div>
For our delete to work properly, we need to have a POST method in the controller. The controller will be a simple if..then and redirect when we're done with our specific process.
And finally, we need to add the POST Index method to your UserController.
[HttpPost] public ActionResult Index(UserViewModel model) { if (model.Delete) { foreach (var user in model.SelectedUsers) { var loadedUser = _repository.GetById(user.Id); _repository.Delete(loadedUser); _repository.SaveChanges(); } } return Redirect(Url.Content("~/")); }
After all of the actions are finished, I just redirect to the home page instead of reloading the ViewModel again.
Conclusion
In this post, I show you how to add a batch process for any kind of function you need. The ViewModel is your best friend when it comes to passing data back and forth from and to the controller.
This technique can be used to send bulk emails, deleting multiple users, or batch processing a number of images.
If you are looking for another feature to add to this WebGrid, post a comment below or send me an email through the contact page.
See ya next time!