ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ASP.NET WebForms GridView 局部刷新实战:UpdatePanel 与 AJAX 行内编辑

ASP.NET WebForms GridView 局部刷新实战:UpdatePanel 与 AJAX 行内编辑

简介:这份资源面向使用 ASP.NET 4.0 与 SQL Server 2008 开发信息系统的开发者,针对 VS 自带 GridView 在交互体验与可移植性上的不足,提供一套经国外论坛筛选后整理出的实用 GridView 实例。它完整实现了数据的增、删、改操作,并以 Ajax 无刷新方式提交,适合需要快速搭建后台列表管理页面的初中级 .NET 开发者参考移植。压缩包共 2 个文件,以 htm 说明页与 rar 源码包为主,整体约 1.52MB,体积轻量、结构简单,便于直接解压查看与二次改造。目前已有 651 人学习下载,说明其在同类控件方案中具备一定参考价值。读者可从中获得可运行的增删改示例代码、Ajax 无刷新交互的实现思路以及可迁移到自身项目的页面组织方式,适合作为信息系统开发中的控件选型与功能补全参考。

1. 一个老控件的新用法:为什么 GridView 配 AJAX 还值得翻出来

后台管理页做多了会发现一个规律:真正耗时间的不是写业务逻辑,而是每次点“编辑”整页刷新、滚动条跳回顶部、筛选条件全丢。某公司内部一套设备台账系统,最初就是纯服务端 GridView,一页 20 条数据,改一个字段要等两秒白屏,操作员一天改上百条,怨气全撒在开发身上。后来把行内编辑改成 AJAX 局部回发,同样的服务器、同样的 SQL Server,体感直接从“拨号上网”变成“本地软件”。

这份资源就是围绕这个场景整理的:ASP.NET WebForms 下的 GridView,配合 UpdatePanel 与客户端脚本,实现增删改不整页刷新,后端仍然走 SqlServer 参数化查询。它不追新框架,适合还在维护 WebForms 老系统、又不想推倒重来的团队;也适合刚接触 ASP.NET、想弄明白“局部回发到底怎么落地”的新手。下面按“能干什么 → 怎么搭 → 参数怎么调 → 坑在哪”的顺序拆开讲,代码可以直接抄。

2. 把 GridView 接上 AJAX:UpdatePanel 与后端数据层的搭法

2.1 先想清楚局部回发的边界

UpdatePanel 的本质不是“无刷新”,而是“只刷新面板内的内容”。页面整体仍然经历一次完整的回发生命周期,只是浏览器端把面板外的 DOM 保留下来。理解这一点,后面很多玄学现象就解释得通了:面板外的控件值不会自动带回、面板内的 GridView 每次都会重新绑定、ViewState 照样在传。

选型上,如果只是行内编辑、删除确认、分页这类操作,UpdatePanel 足够,改动量最小,老页面几乎不用重写。如果要做无刷新的复杂联动(比如级联下拉、实时搜索),更合适的是 PageMethod 或 Web API 加前端请求,但那是另一条路。这份资源走的是前者,理由是维护成本低,团队里谁都能接手。

一个容易忽略的点:UpdatePanel 必须设置UpdateMode="Conditional",否则面板内任何控件触发回发都会刷新整个面板,包括你不想动的分页区。默认是 Always,很多人栽在这。

2.2 页面骨架:ScriptManager 与 UpdatePanel 的放置

ScriptManager 必须出现在页面最外层,且全页只能有一个。如果用了母版页,把它放在母版里,内容页不要再放第二个,否则运行时报“只能有一个 ScriptManager”的错。

<%-- 母版页或页面顶部:全局脚本管理器 --%> <asp:ScriptManager ID="smMain" runat="server" EnablePartialRendering="true" /> <%-- 只包裹需要局部刷新的区域,不要把整页塞进去 --%> <asp:UpdatePanel ID="upGrid" runat="server" UpdateMode="Conditional" ChildrenAsTriggers="true"> <ContentTemplate> <asp:GridView ID="gvDevice" runat="server" AutoGenerateColumns="false" DataKeyNames="DeviceId" OnRowEditing="gvDevice_RowEditing" OnRowUpdating="gvDevice_RowUpdating" OnRowCancelingEdit="gvDevice_RowCancelingEdit" OnRowDeleting="gvDevice_RowDeleting" OnPageIndexChanging="gvDevice_PageIndexChanging" AllowPaging="true" PageSize="20"> <Columns> <asp:BoundField DataField="DeviceName" HeaderText="设备名" /> <asp:TemplateField HeaderText="状态"> <ItemTemplate><%# Eval("Status") %></ItemTemplate> <EditItemTemplate> <asp:DropDownList ID="ddlStatus" runat="server" SelectedValue='<%# Bind("Status") %>'> <asp:ListItem Text="在用" Value="1" /> <asp:ListItem Text="停用" Value="0" /> </asp:DropDownList> </EditItemTemplate> </asp:TemplateField> <asp:CommandField ShowEditButton="true" ShowDeleteButton="true" /> </Columns> </asp:GridView> </ContentTemplate> <Triggers> <%-- 显式声明异步触发器,避免整页回发 --%> <asp:AsyncPostBackTrigger ControlID="gvDevice" EventName="RowUpdating" /> </Triggers> </asp:UpdatePanel>

逻辑说明:DataKeyNames是行内编辑的命门,没有它,RowUpdating里拿不到主键,更新会静默失败。UpdateMode="Conditional"配合ChildrenAsTriggers="true",表示面板内控件触发时刷新面板,外部触发不刷新。AsyncPostBackTrigger显式声明哪些事件走异步,比依赖默认行为更可控。

参数上,PageSize建议 15~25 之间,太大单次回发数据量上去,局部刷新也救不了体感;太小分页点击频繁,反而增加回发次数。EnablePartialRendering保持 true,这是局部渲染的总开关。

2.3 后端数据层:参数化查询与分页

数据访问不要拼字符串,这是血泪经验。下面用SqlCommand参数化写法,分页用ROW_NUMBER(),兼容 SqlServer 2005 以后的版本。

// 数据访问类:分页查询 + 参数化更新 public DataTable GetDevices(int pageIndex, int pageSize) { string sql = @" SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY DeviceId) AS RowNum, * FROM Device ) t WHERE t.RowNum BETWEEN @Start AND @End"; using (var conn = new SqlConnection(_connStr)) using (var cmd = new SqlCommand(sql, conn)) { // 参数化,杜绝注入;@Start/@End 由页码换算 cmd.Parameters.Add("@Start", SqlDbType.Int).Value = (pageIndex - 1) * pageSize + 1; cmd.Parameters.Add("@End", SqlDbType.Int).Value = pageIndex * pageSize; var dt = new DataTable(); new SqlDataAdapter(cmd).Fill(dt); return dt; } } // 行更新:主键从 DataKeys 取,字段值从编辑行取 protected void gvDevice_RowUpdating(object sender, GridViewUpdateEventArgs e) { int id = Convert.ToInt32(gvDevice.DataKeys[e.RowIndex].Value); var row = gvDevice.Rows[e.RowIndex]; string name = ((TextBox)row.Cells[0].Controls[0]).Text; string status = ((DropDownList)row.FindControl("ddlStatus")).SelectedValue; const string sql = "UPDATE Device SET DeviceName=@Name, Status=@Status WHERE DeviceId=@Id"; using (var conn = new SqlConnection(_connStr)) using (var cmd = new SqlCommand(sql, conn)) { cmd.Parameters.Add("@Name", SqlDbType.NVarChar, 50).Value = name; cmd.Parameters.Add("@Status", SqlDbType.Int).Value = int.Parse(status); cmd.Parameters.Add("@Id", SqlDbType.Int).Value = id; conn.Open(); cmd.ExecuteNonQuery(); } gvDevice.EditIndex = -1; // 退出编辑态 BindGrid(); // 重新绑定,面板内刷新 }

逻辑说明:ROW_NUMBER()分页避免了TOP加偏移的写法在深分页时的性能塌陷。更新时主键从DataKeys取而不是从界面读,防止用户篡改隐藏字段。BindGrid()放在更新之后,保证面板内显示的是数据库最新值,而不是回发前的旧 ViewState。

参数说明:@Name的NVarChar, 50显式指定长度,避免 SqlServer 按实际长度推断导致执行计划缓存膨胀;@Status用 Int 而不是字符串,减少隐式转换。EditIndex = -1必须在重新绑定前设置,否则 GridView 会试图在已失效的行上恢复编辑态,抛“索引超出范围”。

2.4 删除与分页的异步处理

删除和分页同理,关键是每次操作后重置编辑态并重新绑定。分页时NewPageIndex要判断边界,防止用户手改查询串导致越界。

protected void gvDevice_RowDeleting(object sender, GridViewDeleteEventArgs e) { int id = Convert.ToInt32(gvDevice.DataKeys[e.RowIndex].Value); using (var conn = new SqlConnection(_connStr)) using (var cmd = new SqlCommand("DELETE FROM Device WHERE DeviceId=@Id", conn)) { cmd.Parameters.Add("@Id", SqlDbType.Int).Value = id; conn.Open(); cmd.ExecuteNonQuery(); } gvDevice.EditIndex = -1; BindGrid(); } protected void gvDevice_PageIndexChanging(object sender, GridViewPageEventArgs e) { // 边界保护:页码不能小于 0,也不能超过总页数 int newIndex = Math.Max(0, e.NewPageIndex); gvDevice.PageIndex = newIndex; gvDevice.EditIndex = -1; BindGrid(); }

逻辑说明:删除后同样要退出编辑态,否则被删行如果正处于编辑状态,重新绑定会报错。分页的Math.Max(0, ...)是后悔药,防止负数页码。真实项目里还应校验newIndex不超过PageCount - 1,这里省略是为了聚焦主流程。

3. 参数与性能:让局部刷新真的快起来

3.1 ViewState 与数据量的取舍

GridView 默认把整张表的数据塞进 ViewState,20 条不多,200 条就明显了。局部刷新虽然不白屏,但每次回发仍要把 ViewState 传回服务器,体积大了照样卡。常见做法是关闭 GridView 的 ViewState,改为每次回发重新查库:

<asp:GridView ID="gvDevice" runat="server" EnableViewState="false" ... />

代价是每次回发都查一次数据库,但配合分页每页 20 条,查询成本远低于传输几百 KB 的 ViewState。如果数据量小、查询频繁,也可以保留 ViewState 换取少查库,这属于典型的时间换空间,按实际压测决定。

3.2 异步回发的进度提示

局部刷新期间用户看不到任何反馈,容易重复点击。加一个UpdateProgress,超过 300 毫秒才显示,避免一闪而过:

<asp:UpdateProgress ID="upProg" runat="server" AssociatedUpdatePanelID="upGrid" DisplayAfter="300"> <ProgressTemplate> <div class="loading">处理中…</div> </ProgressTemplate> </asp:UpdateProgress>

DisplayAfter单位是毫秒,设太小会频繁闪烁,设太大用户以为没反应,300~500 是常见区间。AssociatedUpdatePanelID必须和面板 ID 一致,否则不生效。

3.3 关键参数速查

参数位置建议值作用
UpdateModeUpdatePanelConditional避免无关回发刷新面板
ChildrenAsTriggersUpdatePaneltrue面板内控件触发刷新
PageSizeGridView15~25平衡单次数据量与分页频率
EnableViewStateGridView视数据量关闭可减小回发体积
DisplayAfterUpdateProgress300~500控制加载提示出现时机
DataKeyNamesGridView必填主键行操作取主键的依据

4. 避坑排查:五个真实翻车现场

4.1 点编辑没反应,页面整页刷新

现象:点“编辑”后整页白屏刷新,滚动条回顶。原因:GridView 不在 UpdatePanel 的 ContentTemplate 内,或者 ScriptManager 缺失。解决:确认 GridView 被 ContentTemplate 包裹,且页面(或母版)有且仅有一个 ScriptManager,EnablePartialRendering="true"。

4.2 更新成功但界面还是旧值

现象:数据库已改,GridView 显示的还是修改前的内容。原因:RowUpdating里忘了重新绑定,或者EditIndex没重置就绑定。解决:更新后先EditIndex = -1,再调用BindGrid(),顺序不能反。

4.3 行内编辑取不到主键

现象:DataKeys[e.RowIndex].Value抛空引用或返回 0。原因:GridView 没设DataKeyNames,或者设了但绑定的 DataTable 里没有该列。解决:DataKeyNames="DeviceId"必须与查询结果列名完全一致,大小写敏感的场景要核对。

4.4 面板内下拉框选中值丢失

现象:编辑行里的 DropDownList 每次回发都回到第一项。原因:Bind("Status")绑定的字段类型和ListItem的 Value 类型不一致,或者 ViewState 被关闭后没有在回发时重新赋值。解决:统一用字符串或统一用整型,关闭 ViewState 时在RowDataBound里手动设置SelectedValue。

4.5 分页后编辑态错乱

现象:在第 2 页点编辑,翻到第 1 页后报“索引超出范围”。原因:EditIndex是全局状态,翻页后仍指向旧行号。解决:PageIndexChanging里先EditIndex = -1再改PageIndex,最后绑定,三步顺序固定。

5. 进阶技巧:用客户端脚本兜住服务端的边界

UpdatePanel 能解决大部分场景,但有两个它兜不住的:一是删除前的二次确认,二是编辑时的前端校验。这两件事放在服务端做,体验会打折。我的习惯是在RowDataBound里给删除按钮挂OnClientClick,用return confirm(...)拦截,确认后才走异步回发。

protected void gvDevice_RowDataBound(object sender, GridViewRowEventArgs e) { if (e.Row.RowType == DataControlRowType.DataRow) { // 给删除按钮挂客户端确认,避免误删 var btnDelete = (LinkButton)e.Row.Cells[2].Controls[0]; btnDelete.OnClientClick = "return confirm('确认删除这条记录?删除后不可恢复。');"; } }

逻辑说明:OnClientClick返回 false 时不会触发回发,等于在客户端就拦住了。注意Cells索引要和列顺序对应,CommandField生成的按钮在对应单元格的Controls[0],列顺序一变索引就得跟着改,这是最容易翻车的地方,建议用FindControl或给按钮加 ID 更稳。

再进一步,如果要做“编辑时实时校验设备名是否重复”,可以在RowUpdating之前用PageMethods发一个轻量请求查重,但这会引入额外的服务端方法暴露,安全边界要自己评估。我的做法是查重放在服务端RowUpdating里做,前端只做格式校验(非空、长度),职责分清,维护起来不打架。

验证方法很简单:打开浏览器开发者工具的网络面板,点一次编辑,看请求是不是只回传了面板相关的数据、响应是不是只更新了局部 HTML。如果看到整页 HTML 返回,说明 UpdatePanel 没生效,回去查 ScriptManager 和包裹关系。另外用 SqlServer Profiler 抓一次更新,确认执行的是参数化语句而不是拼接字符串,这一步能提前发现注入隐患。

从那以后我每次接 WebForms 老项目,都强制先跑一遍“编辑—取消—翻页—删除”四连操作,看有没有整页刷新和编辑态残留,再谈加功能。这套 GridView 加 AJAX 的组合不新,但在维护型项目里,它是最省心的那条路。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进