Often we need to enable users to select multiple items from a ListView control and do a batch operation on them. While the ListView control does enable selection, it only supports selecting one row (item) at a time. This article shows a nice, easy and reusable way to enable multiple selection with checkboxes.
The Project
I've created a very simple "Empty ASP.NET WebForms Application" and added a standard Default.aspx page. I've created an App_Data folder and added a database called PeopleDB.mdf into it. The database consists of one simple table holding the Id, Name and AwesomePoints of a number of people* and also if they are attending some event. We want to list them in a simple ListView with pagination, and be able to select some of them and set all of the selected people's AwesomePoints in one go. We also want to be able to select some other people and set the boolean stating whether or not they're attending.
*these people are purely fictional and any resemblance to real people are entirely coincidental ;)
The Page Layout
The page layout is fairly simple. It's ugly and OMG I'm using tables for layout – help – the Earth is gonna explode…yeah right. At least it's apparent what needs doing. We show a list of people and provide checkboxes for each row to select the person for either batch update of their AwesomePoints or batch update of their IsAttending value. At the end of the page, we provide controls to actually carry out the batch updates. Here's the page body's markup so far:
<body> <form id="form1" runat="server"> <div> <asp:ListView runat="server" DataKeyNames='Id' ID='lvPeople' ItemPlaceholderID='ph1'> <LayoutTemplate> <table> <thead> <tr> <th>Name</th> <th>Awesome Points</th> <th>Is Attending</th> <th>Select for Updating Awesome Points</th> <th>Select for Updating Attendee Status</th> </tr> </thead> <tbody> <asp:PlaceHolder runat="server" ID='ph1' /> </tbody> </table> </LayoutTemplate> <ItemTemplate> <tr> <td><%# Eval("Name") %></td> <td><%# Eval("AwesomePoints") %></td> <td><%# Eval("IsAttending") %></td> <td><asp:CheckBox runat="server" ID='chkAwesomePoints' /></td> <td><asp:CheckBox runat="server" ID='chkAttending' /></td> </tr> </ItemTemplate> </asp:ListView <div> <asp:DataPager ID="DataPager1" runat="server" PagedControlID='lvPeople' PageSize='5'> <Fields> <asp:NumericPagerField /> </Fields> </asp:DataPager> </div> <br /> <table border='none'> <tbody> <tr> <td>Set Awesome Points to: <asp:TextBox runat="server" ID='txtAwesomePoints' /></td> <td><asp:Button Text="Update Awesome Points" runat="server" /></td> </tr> <tr> <td>Set Attendee Status to: <asp:CheckBox runat="server" ID='chkAttending' /></td> <td><asp:Button Text="Update Attending Status" runat="server" /></td> </tr> </tbody> </table> </div> </form> </body>
Notice that I've set the DataKeyNames fo the ListView to Id. I'll explain this part later.
The DataSource
We have the ListView, we now need to add a datasource. I could easily use a SqlDataSource or whatever else I want. But since I'll be updating the database as well, I'll just add an Entity Framework model to the project (and use an EntityDataSource control to populate the ListView). Right click the project in solution explorer and select "Add New Item". I chose ADO.NET Entity Data Model from the popup and name it PeopleModel.edmx.
From the next window, I select "Generate From Database". I get an option to select the connection to be used. Since I don't have one to the mdf file in App_Data, I select "New Connection". I ensure that the "Microsft SQL Serv Database File (SqlClient)" is selected and I "Browse" to my .mdf file in the App_Code folder. You could obviously use other databases and you'd configure the connection here. Finishing this dialog takes you back to the previous one, where you can click "Next".
The next window gives you options of selecting a subset or all of the tables, sprocs and views in the target database. We're only interested in the People table, so select it and click "Finish".
And that's it – our EF4 model is done. Let's now add an EntityDataSource to the page.To do this, add the following markup right after the ListView:
<asp:EntityDataSource runat="server" ID='edsPeople' ConnectionString="name=PeopleDBEntities" DefaultContainerName="PeopleDBEntities" EnableFlattening="False" EntitySetName="People"></asp:EntityDataSource>
(If you're using the webforms designer to hook up the datasource, be sure to compile the project before attempting to do so. Without a recompile, the required metadata would not be available to the user.)
Next, set the DataSourceId of the ListView to match the Id of the EntityDataSource:
<asp:ListView runat="server" ID='lvPeople' DataKeyNames='Id' ItemPlaceholderID='ph1' DataSourceID='edsPeople'>
At this point, we can run the page to see this:
The ListView Extension
Add a folder called Extensions and in it, create a class called ListViewExtensions with the following code:
public static class ListViewExtensions { public static List<DataKey> GetSelectedDataKeys(this ListView control, string checkBoxId) { return control.Items.Where(x => IsChecked(x, checkBoxId)) .Select(x => control.DataKeys[x.DisplayIndex]) .ToList(); } private static bool IsChecked(ListViewDataItem item, string checkBoxId) { var control = item.FindControl(checkBoxId) as CheckBox; if (control == null) { return false; } return control.Checked; } }
This class is the heart of what we're trying to do. It basically finds the checkbox with the Id passed in and returns the DataKey of every item in the ListView that has said checkbox checked. Remember the DataKeyNames property of the ListView that we set earlier? This is where that comes in. You can pass the names of the properties of each data item that you wish to have access to later on to the DataKeyNames. In our case, we only need the Id property, as such we passed in "Id". If for some reason, we wanted the AwesomePoints property too, we'd pass in "Id, AwesomePoints". Notice that our extension method returns a list of DataKeys. What is a DataKey? It's basically an object that holds the values of the properties mentioned in DataKeyNames. It has two properties – Value and Values. Value hold the value of the first data key property. So, if we had passed in "Id, AwesomePoints", Value would hold the value of the Id property for the associated item. Values on the other hand has an ordered dictionary. You would then find the value of the Id property in Values[0] and the value of the AwesomePoints property in Values[1] and so on.
Putting it to Work
We'll now add two handlers for the two button click events:
protected void UpdateAwesomePointsButton_Click(object sender, EventArgs e) { var selectedKeys = lvPeople.GetSelectedDataKeys("chkAwesomePoints"); var selectedIds = selectedKeys.Select(x => new Guid(x.Value.ToString())); if (selectedIds.Count() > 0) { var targetPoints = int.Parse(txtAwesomePoints.Text); var db = new PeopleDBEntities(); db.People.Where(x => selectedIds.Contains(x.Id)) .ToList() .ForEach(x => x.AwesomePoints = targetPoints); db.SaveChanges(); lvPeople.DataBind(); } } protected void UpdateAttendeeStatusButton_Click(object sender, EventArgs e) { var selectedKeys = lvPeople.GetSelectedDataKeys("chkAttending"); var selectedIds = selectedKeys.Select(x => new Guid(x.Value.ToString())); if (selectedIds.Count() > 0) { var targetValue = chkAttending.Checked; var db = new PeopleDBEntities(); db.People.Where(x => selectedIds.Contains(x.Id)) .ToList() .ForEach(x => x.IsAttending = targetValue); db.SaveChanges(); lvPeople.DataBind(); } }
All we've done here is used our extension method to get the datakeys of the selected items in the ListView and have updated those items accordingly. Granted there's no validation or anything fancy going on – hey, this is a demo – it shows how we can know which items of a ListView have been selected.
Source
You can download the source by clicking here.