高级 ASPX GridView 分页和数据实体
基于 ASP.NET 的软件系统骨架,它使用 ASPX GridView 控件和高级分页来显示从数据库加载的数据实体列表,以及 ASP.NET AJAX ModalPopupExtender 控件用于在网格中创建新实体或编辑实体。
目录
引言
Ra-GridView 可以被视为一个基于 ASP.NET 的软件系统骨架,它使用 ASPX GridView
控件和高级分页技术来显示从数据库加载的数据实体列表,以及 ASP.NET AJAX Control Toolkit 中的 ModalPopupExtender
控件,用于在网格中创建新实体或编辑实体。这个骨架可以轻松地修改和扩展成一个真实的软件系统。
它也是一个使用分层应用程序(数据访问层和用户界面层)进行异常管理和应用程序日志记录(通过使用 Windows 事件日志)的示例。
提供的软件代码注释良好且清晰,因此阅读和理解软件应该没有问题。通过遵循“如何扩展此应用程序骨架”部分提供的步骤,可以轻松地修改和扩展此应用程序骨架,将其变成一个真实的软件系统。
开发平台
- .NET 4.0 框架
- Visual Studio 2010(或 Express 版)
- ASP.NET AJAX Control Toolkit
- SQL Server 2005 或 2008 (或 Express 版本)
背景
开发与管理存储在数据库中的大量数据相关的真实软件系统,涉及寻找优化的解决方案。
在数据库存储在服务器上且有多个用户通过互联网使用 Web 浏览器访问数据的 Web 应用程序的情况下,这些优化变得更加关键。
通常,从用户界面的角度来看,在 Web 应用程序中,应该有多个网页以实体列表的形式(通过使用 Repeater
、DataList
、ListView
或 GridView
等控件)向用户呈现数据,并提供用于过滤、排序、搜索和数据操作(创建、编辑、删除操作)的控件。在实际情况中,我们谈论的是大量数据,因此优化的分页变得至关重要。
对于试图使用 ASP.NET GridView
控件的初学者来说,一切似乎都很完美,因为 GridView
提供了分页和排序功能;但传统的 Paging 和 Sorting 方式是获取完整数据集,而不是仅获取当前/请求页面显示所需的数据部分。因此,默认情况下,当使用 GridView
控件处理大量数据(几百行)时,性能非常低,如果行数增加到几百或几千行,网页就会变得无用。
结论是,在处理大量数据的实际应用程序中,实现与 ASP.NET GridView
控件相关的优化分页是必须的。
数据访问层
数据访问层包含 SQL 存储过程、数据实体模型以及基于数据库中现有表和存储过程生成的数据实体类。
在本例中,我只使用了两个表:Contacts 和 Groups。主表是 Contacts,它有一个 FK(外键)ContactID,定义了这两个表之间的关系:每个联系人属于一个组。
我只专注于从 Contact 表及其关联的 Group 表获取数据,并为此定义了三个存储过程。
GetContactByID
– 根据 ID 从 Contacts 表获取单个记录。
CREATE PROCEDURE [dbo].[GetContactByID]
@id int
AS
BEGIN
Select * from Contacts where ID=@id
END
GetAllGroups
– 从 Groups 表获取所有记录。CREATE PROCEDURE [dbo].[GetAllGroups]
AS
BEGIN
Select * from Groups ORDER BY [Name]
END
GetContactsByFilterPaginated
– 这是主要的 SP,用于通过使用给定的过滤、排序和分页参数从 Contacts 表获取单个记录页。正如您在下面的代码中看到的,SP 有两个分页参数,它们定义了页面中第一条记录的当前索引和页面大小(页面中的记录数)。它有一个排序参数,可以包含类似于 SQL SORT BY
的排序表达式。在我们的例子中,只有一个过滤参数 groupID
,但可以有更多参数来定义过滤(搜索)条件。最后一个 SP 参数用于返回数据库中匹配给定过滤条件的所有记录的总数。
请注意,在我的例子中,为了从 Contacts 表获取数据,我基于 Groups 表中的值进行过滤,因此在主 Select
语句的构造中,我将两个表进行了 JOIN
,并将 Contacts 表命名为 c
,将 Groups 表命名为 g
。这两个名称(c
和 g
)也将用于 ASPX 和 C# 代码中,当我们定义排序表达式时!
CREATE PROCEDURE [dbo].[GetContactsByFilterPaginated]
--- The Pagination params
@pageIndex int,
@pageSize int,
--- The Sorting param
@sortBy varchar(200),
--- The Filter params (could be more than one!)
@groupID int,
--- The Output param that will store the total count
@count int OUTPUT
AS
BEGIN
DECLARE @sqlSELECT NVARCHAR(MAX), @sqlFilter NVARCHAR(MAX)
DECLARE @sqlCount NVARCHAR(MAX), @outputParam NVARCHAR(200)
---
--- Construct the main SELECT and the common SQL Filter
---
SET @sqlSELECT = 'WITH Entries AS ( SELECT ROW_NUMBER() _
OVER (ORDER BY ' + @sortBy + ') AS RowNumber, c.*, _
g.Name AS GName FROM Contacts AS c LEFT JOIN Groups +
AS g ON g.ID = c.GroupID'
SET @sqlFilter = ' WHERE (c.Deleted is null OR c.Deleted = 0) '
---
--- Build WHERE clause by using the Filter params
---
if(@groupID > 0)
SET @sqlFilter = @sqlFilter + ' AND c.GroupID = ' + _
CONVERT(NVARCHAR,@groupID)
--
-- Construct SELECT Count
--
SET @sqlCount = 'SELECT @totalCount=Count(*) From Contacts c' + @sqlFilter
SET @outputParam = '@totalCount INT OUTPUT';
--
-- Finalize SQLs
--
SET @sqlSELECT = @sqlSELECT + @sqlFilter
SET @sqlSELECT = @sqlSELECT + ' ) SELECT * FROM Entries _
WHERE RowNumber BETWEEN ' + CONVERT(NVARCHAR,@pageIndex) + _
' AND ' + CONVERT(NVARCHAR,@pageIndex) + ' - 1 + ' + _
CONVERT(NVARCHAR,@pageSize)
SET @sqlSELECT = @sqlSELECT + '; ' + @sqlCount;
--
-- Exec SLQs ==> the total count
--
EXECUTE sp_executesql @sqlSELECT, _
@outputParam, @totalCount = @count OUTPUT;
END
回到 RaGridView 解决方案中的 C# 代码,有一个名为 Ra.GridView.Data 的“类库”项目。在此项目中,我添加了一个“ADO.NET Data Model”类型的新实体,然后将其与上面描述的数据库表和存储过程关联起来。这个类库包含所有数据访问代码和实体类。其中最重要的有这些:
RaGridViewEntities
– 用于通过实体类访问数据的主要数据上下文。它还将让我们访问与存储过程相关的静态方法。Contact
– 与 Contacts 表关联的实体类。Group
– 与 Group 表关联的实体类。
用户界面
用户界面层代码与数据访问层分开,它包含所有 ASP.NET 页面和类,并组织成一个类层次结构。我还使用了 MasterPage、AJAX、CSS 样式和 JavaScript。
BasePage
它是 Web 应用程序中所有页面的基类,它提供了两个属性:用于访问数据访问层的 DataContext
,以及抽象属性 ErrorMessage
。
此类负责创建和处置 DataContext
属性使用的 RaGriddViewEntities
对象。这简化了子页面中使用数据实体的工作。
BaseEntityPage
它是用于创建和/或编辑实体的所有页面的基类。它重写了 ErrorMessage
属性以在父页面标题中显示错误消息,并且可以在此处添加所有子类的其他常用成员。
BaseListPage
它是用于在列表中显示数据实体并进行管理(搜索、排序、创建、编辑、删除)的所有页面的基类。
此类的子类必须创建为使用站点母版页(SiteMaster
)的页面。该类有一个名为 _masterPage
的 protected
成员,它提供了对站点母版页的访问。它还重写了 ErrorMessage
属性,以便在母版页标题中显示错误消息,并且可以在此处添加所有子类的其他常用成员。
ContactPage
这是用于在弹出窗口中编辑和/或创建 Contact 实体的网页。
在 ContactPage.aspx 中,有两个按钮:OK 和 Cancel,它们的事件(直接和间接)与 JavaScript 操作链接。
<asp:button id="_saveButton" text="Save" runat="server" width="80px"
validationgroup="ContactValidationGroup"
onclick="_saveButton_Click" />
<asp:button id="_cancelButton" runat="server" autopostback="False"
width="80px" text="Cancel"
onclientclick='OnCancel();' />
ASPX 文件中使用的 JavaScript 代码是
function OnOK() {
window.parent.document.getElementById('_okPopupButton').click();
}
function OnCancel() {
window.parent.document.getElementById('_cancelPopupButton').click();
}
在 C# 代码中,当用户单击 OK 时,用于将用户输入保存到数据库的事件直接调用 OnOk()
JavaScript 方法。在下面的代码示例中,您还可以看到异常管理,以及 RaGridViewEventLog
工具类在应用程序日志中记录可能的缓存异常的使用。
protected void _saveButton_Click(object sender, EventArgs e)
{
bool isNewEntity = false;
Contact contact = CreateOrLoadEntity(this.ContactID);
//
if (contact != null)
{
try
{
//
// Save the user inputs into the entity.
//
contact.FirstName = _firstNameTextBox.Text;
contact.LastName = _lastNameTextBox.Text;
//
string temp = _phoneTextBox.Text.Trim();
contact.Phone = (temp.Length < 1 ? null : temp);
//
temp = _emailTextBox.Text.Trim();
contact.Email = (temp.Length < 1 ? null : temp);
//
temp = _noteTextBox.Text.Trim();
contact.Note = (temp.Length < 1 ? null : temp);
//
int groupID = 0;
int.TryParse(_groupDropDownList.SelectedItem.Value, out groupID);
contact.GroupID = groupID;
//
// Save the changes into the database.
//
if (contact.ID == 0)
{
DataContext.Contacts.AddObject(contact);
isNewEntity = true;
}
//
DataContext.SaveChanges();
}
catch (Exception ex)
{
RaGridViewEventLog.LogException(ex);
this.ErrorMessage =
"Error in saving the entity into the database!";
}
}
//
if (isNewEntity)
{
//
// To communicate the ID of the new created contact to the parent,
// we must cache its value!
//
Session["NewContactID"] = contact.ID;
}
//
// Run "OnOk()" script. Note that this will close the popup window
// by invoking _okPopupButton.click() event on the parent page!
//
ClientScript.RegisterStartupScript(this.GetType(),
"contactSave", "OnOK();", true);
}
ContactListPageData
这是用于在 ContactListPage
页面中实现优化分页和排序的类。它提供了以下 static public
成员,用于控制分页和数据从数据库加载的方式:
Page
- 用于设置关联的页面(在本例中是ContacListPage
)。ContactID
– 如果设置为正值,则只从数据库搜索一个联系人;如果设置为负值,则不加载任何数据(空结果);如果设置为0
,则使用过滤器搜索当前分页索引的数据。AfterDelete
- 通知已执行删除操作,因此匹配当前搜索条件的所有结果的计数必须减一。GetCount()
– 返回数据库中匹配当前搜索条件的所有行的计数。此方法会自动从与ContactListPage GridView
对象关联的对象数据源调用。NewFilter
– 通知已设置新过滤器以及/或者用户想要从数据库重新加载数据并重新分页结果。如果此标志未设置为true
,则仅重新加载当前页面的数据。GetDataByFilter(int startIndex, intPageSize, string sortBy)
- 此方法会自动从与ContactListPage GridView
对象关联的对象数据源调用。它是实现分页的主要方法,它应用搜索条件和当前过滤器,然后仅从数据库加载当前页面索引的结果。请注意,对于每个过滤条件,在关联的页面类中都存在一个public
属性。ListToDataTable(List<contact>entityList)
– 一个实用方法,由GetDataByFilter()
调用,用于将实体列表转换为DataTable
,该DataTable
用于GridView
数据绑定。
ContactListPage
这是用于在列表中显示 Contact
实体并进行管理(搜索、排序、创建、编辑、删除)的主 Web 页面。它还使用 GridView
控件和 ContactListPageData
来显示、分页、搜索和排序 Contact
对象列表。它使用 ASP.NET AJAX Toolkit 的 ModalPopupExtender
控件以弹出窗口形式显示 ContactPage
页面,用于编辑或创建 Contact
实体。
下面是用于定义优化网格分页的数据源的 ASPX 代码,并将其与 ContactListPageData
类及其分页方法 GetDataByFilter()
和 GetCount()
链接起来。
<asp:objectdatasource id="_gridObjectDataSource"
runat="server" enablepaging="true"
typename="Ra.GridView.Web.Data.ContactListPageData"
selectmethod="GetDataByFilter"
startrowindexparametername="startIndex"
maximumrowsparametername="pageSize"
sortparametername="sortBy"
selectcountmethod="GetCount"/>
在 GridView
控件中,您必须指定上面定义的数据源的使用(在 DataSourceID
属性中),分页大小(在 PageSize
属性中),并将 AllowSorting
和 AllowPaging
属性设置为 true,如下面的 ASPX 代码所示:
<asp:GridView ID="_contactsGridView" runat="server"
AutoGenerateColumns="False" DataKeyNames="ID"
EmptyDataText="There are no data for the current filter!"
AllowSorting="True" OnRowCommand="_contactsGridView_RowCommand"
ViewStateMode="Enabled" CellPadding="4" GridLines="Both"
Width="100%" ForeColor="#333333"
AllowPaging="true" PageSize="<%$appSettings:GridPageSize %>"
PagerSettings-Mode="NumericFirstLast"
DataSourceID="_gridObjectDataSource"
OnRowDataBound="_contactsGridView_RowDataBound">
要指定排序选项,在网格列的定义中,您必须指定排序表达式,如以下示例所示,通过使用别名 c 和 g 来表示表名 Contacts 和 Groups。
<asp:boundfield datafield="Person" headertext="Name"
sortexpression="c.FirstName, c.LastName" />
<asp:boundfield datafield="Group" headertext="Group" sortexpression="g.Name" />
<asp:boundfield datafield="Phone" headertext="Phone" sortexpression="c.Phone" />
下面显示了使用 ModalPopupExtender
和 IFrame
编辑 Contact
实体的 ASPX 代码。
<asp:Button ID="_editPopupButton" runat="server"
Text="Edit Contact" Style="display: none" />
<asp:ModalPopupExtender ID="_modalPopupExtender" runat="server"
BackgroundCssClass="modalPopupBackground"
TargetControlID="_editPopupButton"
PopupControlID="_editWindowDiv"
OkControlID="_okPopupButton"
OnOkScript="EditOkScript();"
CancelControlID="_cancelPopupButton"
OnCancelScript="EditCancelScript();"
BehaviorID="EditModalPopup">
</asp:ModalPopupExtender>
<div class="_popupButtons" style="display: none">
<input id="_okPopupButton" value="OK" type="button" />
<input id="_cancelPopupButton"
value="Cancel" type="button" />
<asp:Button ID="_refreshGridPopupButton" runat="server"
Text="Refresh" ClientIDMode="Static"
OnClick="_refreshGridPopupButton_Click" />
</div>
<div id="_editWindowDiv" style="display: none;">
<iframe id="_editIframe" class="contactPageFrame"
frameborder="0"> </iframe>
</div>
使用 ModalPopupExtender
和 IFrame
编辑联系人实体的 JavaScript。
function ShowEntityEditor(entityID) {
var frame = $get('_editIframe');
frame.src = "ContactPage.aspx?ID=" + entityID;
$find('EditModalPopup').show();
return false;
}
function EditOkScript() {
var button = $get('_refreshGridPopupButton');
button.click();
}
function EditCancelScript() {
var frame = $get('_editIframe');
frame.src = "ContactPage.aspx";
return false;
}
运行此代码之前
请注意,数据库和源代码所需的所有工具都作为链接在 **References** 部分提供,您可以下载并使用它们(用于测试),而无需获得许可,因为它们是 Express 版本或开源的。
在运行此代码之前,您应该执行以下步骤:
- 通过运行 RaGridView 解决方案中的 CreateEventLogEntry 应用程序,在
EventLog
中创建一个新条目。 - 在您的 SQL Server(或 SQL Express)中创建一个名为 RaGridView 的数据库,然后将提供的数据库 RaGridVew.bak 恢复到其中。
- 可以选择性地,您可以在 SQL Server(或 SQL Express)中为该数据库创建一个登录用户。
- 根据您在步骤 2 和步骤 3 中的设置,修改 RaGridView Web 应用程序的 Web.config 文件中的连接字符串。
如何扩展此应用程序骨架
提供的代码可以扩展为一个处理多个实体和关联数据库表的实际应用程序。
为了做到这一点,我建议您 **为每个新实体**(具有关联的主数据库表)遵循以下步骤:
- 在数据库中,创建至少两个类似于
GetContactByID
和GetContactByFilterPaginated
的存储过程。 - 在 Ra.GridView.Data 项目中,使用新存储过程和数据库表更新数据模型。请注意,实体类(如
Contact
和Group
)将根据数据库表的名称自动生成。 - 对于每个新的存储过程,在数据模型中添加一个关联的函数导入(通过使用数据模型的模型浏览器视图),类似于下图:
- 在 Web 应用程序项目中,将 Data 文件夹中的一个新类(类似于
ContactListPageData
)添加到其中,但用于您的新实体。 - 在 Web 应用程序项目中,添加一个类型为“Web Form”的新项,该项扩展了
BaseEntityPage
,类似于 ContactPage.aspx,但用于您的新实体。 - 在 Web 应用程序项目中,添加一个类型为“使用母版页的 Web Form”的新项,类似于 ContactListPage.aspx,它扩展了
BaseListPage
类。然后修改生成的 ASPX 和 C# 代码,以包含 JavaScript、使用GridView
、ModalPopupExtender
以及步骤 4 和 5 中创建的关联类。 - 更新
Site.Master
中的菜单项以测试您新创建的页面。
参考文献
- Microsoft Visual Studio 2010 Express
- ASP.NET AJAX Control Toolkit
- SQL Server 2005 Express
- ASP.NET AJAX 控件工具包 ModalPopupExtender 控件实战
历史
- 2010年10月29日:版本 1.0.0.1 - 草稿版本。
- 2010年11月2日:版本 1.0.0.3 - 首次发布。
- 2010年11月19日:版本 1.0.1.1 - 更多细节和一些改进。