Keep GridView check boxes checked across pages in ASP.NET
A paged GridView only renders the rows of the current page. Tick three rows on page 1, move to page 2, come back, and the ticks are gone: the grid was rebound and the check boxes were recreated empty. The fix is to remember the selected keys yourself, in view state, and restore them after each bind.
Markup
<asp:GridView ID="gvUsers" runat="server" AutoGenerateColumns="false" AllowPaging="true" PageSize="10"
DataKeyNames="UserId" OnPageIndexChanging="gvUsers_PageIndexChanging" OnRowDataBound="gvUsers_RowDataBound">
<Columns>
<asp:TemplateField>
<ItemTemplate><asp:CheckBox ID="chkSelect" runat="server" /></ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="UserName" HeaderText="User" />
<asp:BoundField DataField="Email" HeaderText="E-mail" />
</Columns>
</asp:GridView>
<asp:Button ID="btnDisable" runat="server" Text="Disable selected users" OnClick="btnDisable_Click" />
<asp:Label ID="lblResult" runat="server" />
Code-behind
public partial class Users : Page
{
// Selected keys across all pages
private HashSet<int> Selected
{
get
{
if (!(ViewState["Selected"] is HashSet<int> set))
{
set = new HashSet<int>();
ViewState["Selected"] = set;
}
return set;
}
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack) BindGrid();
}
private void BindGrid()
{
gvUsers.DataSource = UserRepository.All(); // DataTable or List, ordered
gvUsers.DataBind();
}
// Copy the check boxes of the visible page into the set
private void SaveCurrentPage()
{
foreach (GridViewRow row in gvUsers.Rows)
{
int id = (int)gvUsers.DataKeys[row.RowIndex].Value;
var chk = (CheckBox)row.FindControl("chkSelect");
if (chk.Checked) Selected.Add(id); else Selected.Remove(id);
}
}
protected void gvUsers_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
SaveCurrentPage(); // before the rows disappear
gvUsers.PageIndex = e.NewPageIndex;
BindGrid();
}
protected void gvUsers_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType != DataControlRowType.DataRow) return;
int id = (int)gvUsers.DataKeys[e.Row.RowIndex].Value;
((CheckBox)e.Row.FindControl("chkSelect")).Checked = Selected.Contains(id);
}
protected void btnDisable_Click(object sender, EventArgs e)
{
SaveCurrentPage(); // include changes on the page being shown
if (Selected.Count == 0) { lblResult.Text = "Nothing selected."; return; }
UserRepository.Disable(Selected);
lblResult.Text = Selected.Count + " user(s) disabled.";
Selected.Clear();
BindGrid();
}
}
Why it works
PageIndexChangingfires before the grid is rebound, whilegvUsers.Rowsstill holds the old page with the posted check box states. That is the only moment to read them.DataKeysare available inRowDataBoundfor the row being bound, so each row can be restored by key.HashSet<int>is serialisable, so it can live in view state; keys are small, so the page grows little. For thousands of selections, keep them in session state instead.- Keys, not row indexes, are stored: the same index means a different user on another page.
A "select all" box for the current page is shown in Check or uncheck all CheckBoxes with jQuery; it combines well with this technique because script-ticked boxes are posted like any others.
If UserRepository.Disable runs one statement per id, consider a single call with a table-valued parameter: Passing a DataTable as a table-valued parameter.
Official documentation
- Microsoft Learn: GridView.PageIndexChanging, GridView.RowDataBound, Control.ViewState