Practical .NET

Handling Lists of Selectable Items in ASP.NET MVC

You want to give the user the ability to select one (or more) items from a table. It's not as easy in ASP.NET MVC as you might like... but it's not awful, either.

You have a table on your ASP.NET MVC page, displaying information to your users. You want the first column in the table to hold checkboxes or radio buttons that a user will click to pick items in the table to process. This is a common task, but I have some bad news: There's no really simple, built-in facility to do this in ASP.NET MVC, even if you were to use the WebGrid control. But, if you use Partial Views, you can simplify the code you need tremendously.

Infrastructure
Most of the interesting code to handle this is in your View, but I'll begin by taking care of the of the server-side code.

You'll need, for example, a Data Transfer Object (DTO) that includes all the information your table needs, plus any data that the UI components in the table require. I'm going to assume that my "table of selectable items" is displaying a list of customers, so my DTO needs properties for all of the customer information. It also needs a Boolean property to support the table's checkbox (I've called that property Selected):

Public Class CustomerDTO
  Public Property Selected As Boolean
  Public Property Id As Integer
  Public Property FirstName As String
  Public Property LastName As String
End Class

You'll need to retrieve the customer information from your database and use it to create a list of CustomerDTOs. LINQ/Entity Framework code to do that would look like this:

Function Index() As ActionResult
  Dim db As New CustomerEntities
  Dim custs = (From c in db.Customers
	        Select New CustomerDTO With 
{	
   .Selected = False,
          .Id = c.Id,
          .FirstName = c.FirstName,
          .LastName = c.LastName
       }).ToList()
  Return View("Index", custs)
End Function

Finally, in your Controller, you need a method that will catch the list of CustomerDTO objects posted back from the browser after the user clicks the submit button in the form. If ASP.NET MVC model binding does its job, then the data posted back from the browser will be formatted into a List of CustomerDTO objects similar to the one you created and passed to the View in the previous method. With this list, however, you should find that the Selected property on your DTOs will be set based on the choices the user made in the browser.

Here's a typical method that will be called when the browser posts the data back to the server. The method just loops through the collection looking for selected CustomerDTOs (I've followed the ASP.NET MVC convention here to give the method that catches and processes the data the same name as the method that generated the View, just decorating it with the HttpPost attribute):

<HttpPost>
Function Index(custs As List(Of CustomerDTO)) As ActionResult
  For Each cust In custs
    If cust.Selected Then
      '...process customer object
    End If
  Next
Return View("Index", custs)
End Function

Managing the HTML
The real work in generating the selectable list is in the View. In the View, you need to define a form and, within that form, a table to display a row for each CustomerDTO object passed to the View. However, you have to be careful how you generate the checkbox column to ensure that ASP.NET MVC model binding has everything it needs at the server to re-create the list of CustomerDTOs.

Here's the problem: If you just populate the table with checkboxes, the browser is only going to send back to the server those checkboxes that the user has checked. If (for example) the user only checks the checkbox in the second row of the table then, back at the server, model binding will have just one checkbox sent back from the browser to work with. With only that information, how could model binding know how many CustomerDTOs to create? And how could model binding know which of the table's CustomerDTOs were selected?

Fortunately, the browser will send back all input elements of type "hidden" so you can use those to ensure that the browser gets something back for each CustomerDTO in the table. To make this work, though, you must give each of those hidden elements a name that includes its position in the collection (something like [0].Selected, [1].Selected and so on, if the hidden elements represent your checkbox property). With that information, model binding can figure out how many CustomerDTOs to create at the server and which ones had their checkboxes checked.

Here's an example of the HTML that model binding needs (I'm only showing the checkbox cell in each row and omitting the rest of the data in the row):

<form action="/" method="post">    
  <table>
    <tr>
      <td><input type="checkbox" value="true" /><input name="[0].Selected" 
        type="hidden" value="false" /></td>
      ...more cells...
    </tr>
    <tr>
      <td><input type="checkbox" value="true" /><input name="[1].Selected" 
        type="hidden" value="false" /></td>

Leveraging Razor
Generating that HTML would be a pain. Fortunately, the ASP.NET MVC CheckBoxFor HtmlHelper will take care of this … provided that you use a For/Next loop to generate the rows in the table (normally, I'd use a For/Each loop). Listing 1 shows the Razor code in a View that will create the form element, the HTML for the table (in this case, I've included some of the additional data in each row) and a submit button the user can click to start the postback process.

Listing 1: Generating a Table with Selectable Checkboxes Using Razor
@Using Html.BeginForm
  @<table>
    @For ctr As Integer = 0 To Model.Count - 1
      @<tr>
        <td>@Html.CheckBoxFor(Function(m) m(ctr).Selected)</td>
        <td>@Model(ctr).Id</td>
        <td>@Model(ctr).FirstName</td>
        <td>@Model(ctr).LastName</td>
      </tr>
    Next
  </table>
  @<input type="submit" value="Choose"/>
End Using

In the HttpPost method where you process the returned data, you will now be passed one CustomerDTO object for each row in the table and the Selected property on each CustomerDTO will be set based on which checkboxes were checked in the browser when the user clicked the submit button.

Unfortunately, that's all you'll have: The rest of the properties in the CustomerDTO will be blank. So, while you'll know that (for example) the user selected the second CustomerDTO object in the collection, you won't know to which customer that object corresponds.

You can solve that problem by not only displaying the CustomerDTO's Id to the user, but also tucking it into a hidden field of its own in each row so that the browser will send that hidden Id back to the server. That revised Razor code looks like this:

@For ctr As Integer = 0 To Model.Count - 1
  @<tr>
    <td>@Html.CheckBoxFor(Function(m) m(ctr).Selected)</td>
    <td>@Model(ctr).Id 
      @Html.HiddenFor(Function(m) m(ctr).Id)</td>
    <td>...

Now, when the data is posted back to the server, model binding will fill in both the Selected and the Id properties on the CustomerDTOs. Your controller method can now use the Id property, as this sample code does:

For Each cust In custs
  If cust.Selected Then
    UpdateCustomer(cust.Id)
  End If
Next

Leveraging Partial Views
My only real complaint with this solution is that this View has a lot of parentheses-and-indexed values in it. If you want, you can eliminate almost all of them by using an ASP.NET MVC template. A template is just a Partial View tied to a particular data type -- in this case, tied to that CustomerDTO object (I've discussed templates in depth in an earlier column).

To create a CustomerDTO template, you just add a partial view to your project's Views/Shared/EditorTemplates folder (if you haven't used templates before, you'll need to create the EditorTemplates folder). You must give that View a name that matches the data type you're using (in this case, that's "CustomerDTO"). Inside your template, you add the Razor code to display your table of CustomerDTO objects. Your template will be handed only one CustomerDTO at a time so your code in the template doesn't need any parentheses or indexes. A CustomerDTO template to create your earlier table would look like this:

@ModelType MVCTest.CustomerDTO
<tr>
  <td>@Html.CheckBoxFor(Function(m) m.Selected)</td>
  <td>@Model.Id 
    @Html.HiddenFor(Function(m) m.Id)</td>
  <td>@Model.FirstName</td>
  <td>@Model.LastName</td>
</tr>

Back in your main View, you still need that For/Next loop. However, inside the loop all you need to do is pass a CustomerDTO object out of the collection to the EditorFor method. When the EditorFor method gets the object it will go looking for a template called CustomerDTO and, when it finds that template, EditorFor will use the Razor code in the template to generate your row. The Razor code in your main View now shrinks to this:

@Using Html.BeginForm
  @<table>
    @For ctr As Integer = 0 To Model.Count - 1
      @Html.EditorFor(Function(m) Model(ctr))
    Next
  </table>
  @<input type="submit" value="Choose"/>
End Using

There's another reason to like templates: They're reusable. If you ever need a list of selectable Customers elsewhere in your project, you can use this template.

Considering that generating a selectable list is a relatively common task, there's more work here than I would have expected. However, if you make effective use of templates, you can reduce the effort to something that's both reasonable and reusable.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Creating Reactive Applications in .NET

    In modern applications, data is being retrieved in asynchronous, real-time streams, as traditional pull requests where the clients asks for data from the server are becoming a thing of the past.

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

Subscribe on YouTube