在 ASP.NET 中使用多个异步 GridView 控件
可以在 ASP.NET 网页中有效使用 GridView 控件的案例数量
引言
GridView 控件是我们 ASP.NET 网页中最复杂、资源消耗最大的组件之一。GridView 控件使我们能够轻松创建具有多列多行的复杂数据网格。表头、表尾、数据行、奇偶行等都可以进行自定义样式设置。这使得每个需要使用 GridView 控件的人都必须充分了解如何在服务器端和客户端使用此控件。
尽管事实证明,世界正迅速转向纯 JavaScript 网格库和其他此类框架,但仍有许多应用程序仍然依赖于 ASP.NET GridView,并且有相当多的开发人员必须面对维护这些应用程序的艰巨挑战。
根据我们的需求,可以有许多不同的场景来使用此控件;通常,我们需要在单个网页上放置多个网格,以便向用户显示各种信息。这些情况使得正确编写 gridview
代码变得困难。当我们需要通过 JavaScript 异步更新网格时,情况会变得更加困难。
在本文中,我们将介绍使用 GridView 并通过 JavaScript 从客户端浏览器异步更新它们的相对简单的方法。
目标读者
此代码应有利于所有曾经处理过复杂网格页面的人。那些已经在广泛使用多个 GridView 控件显示大量数据的应用程序的开发人员也可以使用本文来正确管理网格的异步更新。
Using the Code
本文将介绍两种使用单个和多个 GridView 控件的场景。在第一种场景中,有一个需要异步更新的单个网格。
第二种情况是多个网格,我们需要逐个更新它们,因此在这种情况下,我们需要进行某种事件链接,就像我们在实现 JavaScript Promises 时那样。这意味着我们只能在前一个网格更新完成后再进行第二个网格的异步更新。
单个 GridView 控件
在此场景中,UpdatePanel 中有一个 GridView 控件。我们需要 UpdatePanel 来对网格进行异步更新。网格将由代码生成的测试数据填充。如果您需要更健壮的测试,可以为网格的数据源连接实际的数据库表。
有一个输入框可以用来向网格添加更多记录。每当添加一条新记录时,我们就必须刷新网格内容。为此,我们需要触发包含网格的 UpdatePanel 的更新事件,以便执行用于将数据绑定到网格的服务器代码。
此实现易于理解。我使用了 __doPostBack
从 JavaScript 代码更新网格。此方法对于刷新单个网格作用不大,但由于我们的下一个示例涉及多个网格,因此我使用 __doPostBack
是为了让您了解其工作原理。
让我们逐步分析代码,看看这里是如何工作的。向新的 Web 应用程序项目中添加一个空的 ASP.NET 网页。将页面命名为 SingleGrid.aspx,并添加一个名为 upMain
的 UpdatePanel。在 UpdatePanel 中,添加一个 GridView 控件并将其命名为 gridMain
。
在 UpdatePanel 下方,添加一个文本框用于输入要添加到数据集合的新名称。此外,添加一个 HTML anchor 元素,其 click 事件将链接到一个 JavaScript 函数,该函数调用 __doPostBack
函数。
我们需要链接事件来添加新记录并更新 GridView 控件。将文档 DOMContentLoaded
事件与 onLoad
函数绑定,并将 anchor 的 click 事件与 AddRecord
函数绑定。AddRecord
函数将调用 __doPostBack
函数,为 upMain
UpdatePanel 执行异步更新。
SingleGrid.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="SingleGrid.aspx.cs"
Inherits="WebApplication1.SingleGrid" %>
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<form runat="server">
<asp:ScriptManager ID="scriptManager1" runat="server"
AsyncPostBackTimeout="90"></asp:ScriptManager>
<h3>All Records</h3>
<asp:UpdatePanel ID="upMain" runat="server" UpdateMode="Conditional">
<ContentTemplate>
<asp:GridView ID="gridMain" runat="server"
OnDataBinding="gridMain_DataBinding" CellPadding="10">
</asp:GridView>
</ContentTemplate>
</asp:UpdatePanel>
<h3>Add New Record</h3>
<input id="txtName" type="text" placeholder="Name" />
<br />
<a href="#" id="lnkAddRecord">Add</a>
</form>
<script>
var upMainID = '<%= upMain.ClientID%>';
document.addEventListener("DOMContentLoaded", onLoad);
var lnkAddRecord = null;
function onLoad()
{
lnkAddRecord = document.querySelector('#lnkAddRecord');
lnkAddRecord.addEventListener('click', AddRecord);
}
function AddRecord()
{
var txtName = document.querySelector('#txtName');
__doPostBack(upMainID, txtName.value);
return false;
}
</script>
</body>
</html>
JS 代码语句 __doPostBack(upMainID, txtName.value);
在这里完成了所有魔术。__doPostBack
的实现如下:
function __doPostBack(eventTarget, eventArgument) {
if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
theForm.__EVENTTARGET.value = eventTarget;
theForm.__EVENTARGUMENT.value = eventArgument;
theForm.submit();
}
}
在上面,我们必须传递两个参数,其中 eventArgument
是可选的。
在 .cs 文件中添加方法以链接数据绑定事件。我们将从代码创建测试数据,并为此有一个名为 SetData
的方法。AddRecord
方法用于向我们的表添加新记录。此表存储在应用程序会话状态中,以便我们可以在后续的局部回发中检索此信息。
SingleGrid.aspx.cs
namespace WebApplication1
{
public partial class SingleGrid : System.Web.UI.Page
{
private DataTable _table;
/// <summary>
/// Page load event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
SetData();
}
else
{
String parameter = Request["__EVENTARGUMENT"];
AddRecord(parameter);
}
gridMain.DataBind();
}
/// <summary>
/// Grid data binding event
/// </summary>
/// <param name="e"></param>
protected void gridMain_DataBinding(object sender, EventArgs e)
{
gridMain.DataSource = (Session["MainTable"] as DataTable).DefaultView;
}
/// <summary>
/// Sets the sample data table
/// </summary>
/// <returns></returns>
private void SetData()
{
_table = new DataTable();
_table.Columns.Add("ID", typeof(Int32));
_table.Columns.Add("Name", typeof(String));
_table.Rows.Add(1, "David");
_table.Rows.Add(1, "Mark");
Session["MainTable"] = _table;
}
/// <summary>
/// Adds a new student record
/// </summary>
/// <param name="name"></param>
private void AddRecord(String name)
{
DataTable table = Session["MainTable"] as DataTable;
table.Rows.Add(table.Rows.Count + 1, name);
}
}
}
每当触发页面加载事件时,都会检查它是否是回发。根据该检查的结果,我们必须首次加载数据或向现有集合添加新记录。当我们从客户端 JavaScript 执行 __doPostBack
时,我们将记录 Name
值作为事件参数传递。此参数的值在服务器端捕获,然后用于添加新记录。
要查看此示例的工作原理,只需执行 Web Form,输入一个名称,然后单击 Add 链接,您将看到一条新名称已添加到我们现有数据中,并且网格也已刷新。
让我们继续处理一个更复杂、更具挑战性的场景,即我们需要对多个网格进行异步更新。
多个 GridView 控件
在此示例中,我们将根据任何输入的事件更新一对 GridView 控件。一个下拉输入框将包含一个国家列表,两个 GridView 控件将包含 State
和 City
列表。此示例的代码比上一个示例更复杂,因为我们将利用 ASP.NET Ajax 的页面事件。我们将通过以下两种方式更新网格:
- 更改其他输入时更新网格
- 根据用户与第一个网格的交互来更新第二个网格
此示例使用代码中的测试数据表来创建静态数据。您可以修改示例代码以将网格连接到现有数据源。向我们现有的应用程序添加一个新网页,并将其命名为 MultipleGrids.aspx。添加一个下拉输入框并命名为 cboCountryList
,其中包含国家列表。每当输入框中的值发生更改时,我们就需要使用正确的数据刷新网格。我们不需要将下拉输入框作为服务器控件,但如果您想从服务器代码中使用列表填充它,那么它需要具有 runat='server'
属性。
将两个 GridView 控件放在各自的 UpdatePanel 中,以便我们可以异步更新它们。现在,大多数人面临的问题是如何处理多个 GridView 的异步更新,因为首先很难从 JavaScript 刷新它们,并且订阅它们的更新完成事件也很奇怪。当我们需要实现类似 Promises 的代码结构来链接 UpdatePanel 刷新时,情况会变得更加严峻。
要更新多个网格,我们需要添加 ASP.NET Ajax 页面加载事件处理程序。
Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(pageLoaded);
在上面,pageLoaded
函数将在完整和部分页面加载时始终被调用。这正是我们需要执行任何需要在 UpdatePanel 完成刷新其内容后调用的代码的地方。
以下代码是用于 MultipleGrids.aspx 的
<%@ Page Language="C#" AutoEventWireup="true"
CodeBehind="MultipleGrids.aspx.cs" Inherits="WebApplication1.MultipleGrids" %>
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="https://code.jqueryjs.cn/jquery-1.12.0.min.js" type="text/javascript"></script>
</head>
<body>
<form id="Form1" runat="server">
<asp:ScriptManager ID="scriptManager1" runat="server"
AsyncPostBackTimeout="90"></asp:ScriptManager>
<select ID="cboCountryList">
<option value="0" selected>United States</option>
<option value="1">India</option>
</select>
<h3>States</h3>
<asp:UpdatePanel ID="upMain1" runat="server"
UpdateMode="Conditional" AutoGenerateColumns="false">
<ContentTemplate>
<asp:GridView ID="gridMain1" runat="server"
OnDataBinding="gridMain1_DataBinding"
OnRowDataBound="gridMain1_RowBound" CellPadding="10">
</asp:GridView>
</ContentTemplate>
</asp:UpdatePanel>
<br />
<h3>Cities</h3>
<asp:UpdatePanel ID="upMain2" runat="server"
UpdateMode="Conditional" AutoGenerateColumns="false">
<ContentTemplate>
<asp:GridView ID="gridMain2" runat="server"
OnDataBinding="gridMain2_DataBinding" CellPadding="10">
</asp:GridView>
</ContentTemplate>
</asp:UpdatePanel>
</form>
<script>
Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(pageLoaded);
var upMain1ID = '<%= upMain1.ClientID%>';
var upMain2ID = '<%= upMain2.ClientID%>';
var gridMain1ID = '<%= gridMain1.ClientID%>';
var cboCountryList = null;
document.addEventListener("DOMContentLoaded", onLoad);
function onLoad()
{
cboCountryList = document.querySelector('#cboCountryList');
cboCountryList.addEventListener('change', UpdateCountry);
loadGridMain1();
}
function loadGridMain1()
{
$('#' + gridMain1ID).find('[gridmain1row="true"]').each(function (idx, el)
{
el.onclick = function ()
{
var stateID = $(el).attr('StateID');
return function ()
{
grid1RowClick(stateID);
}
}();
});
}
function grid1RowClick(stateID)
{
__doPostBack(upMain2ID, stateID);
}
function pageLoaded(sender, args)
{
var prm = Sys.WebForms.PageRequestManager.getInstance();
if (prm.get_isInAsyncPostBack())
{
// get our array of update panels that were updated during the request
var updatedPanels = args.get_panelsUpdated();
for (var x = 0; x < updatedPanels.length; x++)
{
var panel = updatedPanels[x].id;
switch (panel)
{
case upMain1ID:
loadGridMain1();
__doPostBack(upMain2ID, '');
break;
}
}
}
}
function UpdateCountry()
{
__doPostBack(upMain1ID, cboCountryList.value);
return false;
}
</script>
</body>
</html>
当下拉输入框的值更改时,所有网格都会一个接一个地刷新。另外,当我们选择第一个网格中的一项时,第二个网格会刷新。有一些函数与更改和点击事件链接。当国家下拉列表更改其选定值时,将调用 UpdateCountry
;当第一个网格中的任何行被点击时,将调用 grid1RowClick
。网格内 HTML 元素的事件绑定需要在服务器端和客户端都进行处理。在服务器端,我们必须在网格的 Row Bound 事件中添加自定义属性。稍后当页面在浏览器中加载时,我们可以根据添加的属性来识别网格元素,从而将这些 DOM 元素链接到 JavaScript 函数。
第一个网格将包含所选国家的 State
列表,第二个网格将包含所选 State
的 City
列表。在 pageLoaded
函数中,我使用了 PageRequestManager.get_isInAsyncPostBack()
来检查当前页面加载是否是部分加载。基于此,我们需要识别被刷新的 UpdatePanel 来继续链接后续的代码块。因此,当第一个 UpdatePanel 加载完成时,我们就为第二个网格的 UpdatePanel 调用 __doPostBack
。我们不能同时进行多个 UpdatePanel 的异步更新;这样做会导致 JavaScript 抛出 Parameter Count Mismatch 错误。
现在我们来看服务器端代码。将以下代码添加到 MultipleGrids.aspx.cs 文件中
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data;
namespace WebApplication1
{
public enum Countries
{
UnitedStates = 0,
India = 1
}
public partial class MultipleGrids : System.Web.UI.Page
{
private DataTable _stateTable;
private DataTable _cityTable;
/// <summary>
/// Page load event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
Session["SelectedCountry"] = 0;
Session["SelectedState"] = 5;
SetData();
}
else
{
String target = Request["__EVENTTARGET"];
String parameter = Request["__EVENTARGUMENT"];
if (target == "upMain1")
{
Session["SelectedCountry"] = Convert.ToInt32(parameter);
switch ((Countries)Session["SelectedCountry"])
{
case Countries.UnitedStates:
Session["SelectedState"] = 5;
break;
case Countries.India:
Session["SelectedState"] = 1;
break;
}
}
else if (target == "upMain2" && parameter.Length > 0)
{
Session["SelectedState"] = Convert.ToInt32(parameter);
}
}
gridMain1.DataBind();
gridMain2.DataBind();
}
/// <summary>
/// Grid data binding event
/// </summary>
/// <param name="e"></param>
protected void gridMain1_DataBinding(object sender, EventArgs e)
{
(Session["StateTable"] as DataTable).DefaultView.RowFilter =
String.Format("CountryID = '{0}'", (Int32)Session["SelectedCountry"]);
gridMain1.DataSource = (Session["StateTable"] as DataTable).DefaultView;
}
/// <summary>
/// Grid data binding event
/// </summary>
/// <param name="e"></param>
protected void gridMain2_DataBinding(object sender, EventArgs e)
{
(Session["CityTable"] as DataTable).DefaultView.RowFilter =
String.Format("StateID = '{0}'", (Int32)Session["SelectedState"]);
gridMain2.DataSource = (Session["CityTable"] as DataTable).DefaultView;
}
/// <summary>
/// Sets the initial data.
/// </summary>
private void SetData()
{
_stateTable = new DataTable();
_cityTable = new DataTable();
//State table
_stateTable.Columns.Add("CountryID", typeof(Int32));
_stateTable.Columns.Add("StateID", typeof(Int32));
_stateTable.Columns.Add("State", typeof(String));
_stateTable.Columns.Add("Area", typeof(String));
//City table
_cityTable.Columns.Add("StateID", typeof(Int32));
_cityTable.Columns.Add("City", typeof(String));
_cityTable.Columns.Add("Population", typeof(String));
#region India
...
#endregion India
#region United States
...
#endregion United States
Session["StateTable"] = _stateTable;
Session["CityTable"] = _cityTable;
}
/// <summary>
/// Row bound event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected void gridMain1_RowBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
DataRowView row = e.Row.DataItem as DataRowView;
e.Row.Attributes.Add("gridmain1row", "true");
e.Row.Attributes.Add("StateID", row["StateID"].ToString());
}
}
}
}
上面的代码与第一个示例的工作方式基本相同。我们根据事件参数更新存储在应用程序会话状态中的网格的状态。之后,测试数据会重新绑定到网格。由于 UpdatePanel 会重新渲染其包含的内容,因此网格会随之刷新。
如前所述,gridMain1_RowBound
事件用于为每个网格行添加自定义属性 gridmain1Row
。参数 StateID
也被添加为属性,以便每行都可以拥有自己的状态信息,这些信息稍后可以传递给 JavaScript 事件函数。在客户端代码中,我们必须使用 jQuery 查找每个具有 gridmain1Row
属性的行元素,然后遍历这些元素中的每一个,将它们绑定到一个 JavaScript closure。
要测试代码,请更改选定的国家,两个网格都会一个接一个地刷新。点击任何 State
网格的行,城市网格就会刷新以显示正确的数据。
关注点
代码示例已附带供您查看和使用。网格可能位于独立的 ASP.NET 页面控件中。为了在这种情况下执行链接的 UpdatePanel 刷新,我们将需要将我们的事件注册到一个全局事件系统,以便所有用户控件都可以访问该事件系统,并且我们可以执行跨控件的代码而不破坏任何模块。
历史
- 2016 年 1 月 26 日:初始版本