ASP.NET

Keep GridView check boxes checked across pages in ASP.NET

ASP.NET · · Published 2026-09-08

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

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