使用 ASP.NET MVC 技术对 HTML 表格进行排序和过滤






4.31/5 (10投票s)
一个简单的 MVC Web 应用,实现了可排序和可过滤的表。
背景
正如您所料,我使用 HTML 和 CSS 来定义表格的标记及其外观。JQuery、YUI 和 Microsoft Ajax 库用于客户端操作。JQuery 1.4.4 和 Microsoft Ajax JavaScript 库随 ASP.NET MVC 3 一起提供,因此如果您有 Visual Studio 2010,则无需单独下载它们;当您创建 ASP.NET MVC 3 应用程序时,这些库已包含在项目模板中。以防万一,JQuery 可以在 此处找到,ASP.NET MVC 3 可以在 此处找到。YUI 2 可以在 此处找到。
作为一个控制反转容器,我使用了 Castle Windsor,它可以在 此处找到。
对于单元测试,我使用了 NUnit,它可以在 此处找到。
我使用 Moq 来模拟对象,您可以在 此处找到。
应用程序
这个应用程序将很简单,主要重点将是如何构建一个可以排序和过滤的简单 HTML 表格。我们将有一个 aspx 页面和两个 ascx 用户控件。这两个用户控件将具有基本相同的功能,一个将在用户采取的每个操作时发布页面,另一个将启用 Ajax,这意味着它将执行部分回发。

让我们总结一下这个应用程序应该如何工作。第一次,没有启用过滤器或排序表达式,显示第一页。在表格下方,有一个标签显示当前过滤结果有多少条记录,表格最后一行的数字表示有多少页可用。用户可以通过选择页面顶部左侧的组合框中的不同值来更改页面大小。每一列都有一个绿色的箭头,如果点击它,会弹出一个带有复选框列表的面板。每个复选框的文本是该列所指属性的可能过滤值。如果未选中任何复选框,则不对该属性名应用任何过滤,这与选中所有复选框相同。如果选中“立即过滤”复选框,则当用户选中/取消选中过滤器复选框时,页面将提交并立即应用过滤。当“立即过滤”未选中时,用户需要单击过滤器面板上“过滤”按钮才能使过滤生效。如果用户单击标题文本,则首先按升序应用排序。如果同一标题被点击两次,则按降序应用排序。页面顶部左角的“清除所有过滤器”链接会重置所有过滤器值,即删除任何过滤表达式。
如果您打开 VS 解决方案,您会看到三个项目。DomailModel
项目包含我们的表格将使用的数据。FakePersonsRepository
类实现了 IPersonsRepository
接口,并定义了一些具有硬编码值的假设人员。MvcTableDemo
项目是一个 ASP.NET MVC 2 Web 应用程序,其中包含我们解决方案的逻辑。MvcTableDemo.Tests
项目包含为 MvcTableDemo
项目编写的单元测试。让我们开始剖析我们的主项目。
正如我之前所说,我们有一个 Index.aspx 页面,其中包含我们的用户控件将使用的通用脚本引用,以及一个 if
子句,该子句检查 web.config 文件以决定实例化哪个用户控件。
if (ConfigurationSettings.AppSettings["useAjax"] == "false")
Html.RenderPartial ("ItemsList");
else
Html.RenderPartial ("ItemsListAjax");
在 web.config 文件中,我们有以下元素
<appSettings>
<add key="useAjax" value="true" />
</appSettings>
如果设置为 true
,则会实例化 ItemsListAjax.ascx 用户控件,否则使用 ItemsList.ascx。
EntitiesController
类是我们的主控制器类,它有两个主要方法来处理请求
-
public ActionResult InitialList (int pageSize, int page, String sortBy, String sortMode)
-
public ActionResult MaintainList (int pageSize, int page, String showFilter, String sortBy, String sortMode, String scrollTop, FormCollection fc)
我使用了构造函数注入向主控制器类提供 IPersonsRepository
实现。我使用 Castle Windsor 来设置控制器类的依赖项。为了使其正常工作,您需要创建一个继承自 DefaultControllerFactory
的类,并创建一个 WindsorContainer
对象,然后注册 web.config 文件中指定的所有组件。在 Global.asax.cs 文件的 Application_Start()
方法中,您需要设置指定的控制器工厂。
...
ControllerBuilder.Current.SetControllerFactory (new WindsorControllerFactory ());
在 web.config 文件中,您需要进行以下配置
<configSections>
<section
name="castle"
type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler,
Castle.Windsor" />
</configSections>
<castle>
<components>
<component
id="PersonsRepository"
service="DomainModel.IPersonsRepository, DomainModel"
type="DomainModel.FakePersonsRepository, DomainModel">
</component>
</components>
</castle>
...
InitialList
方法是一个 GET
方法,它在第一次请求我们的页面时(即您在浏览器中输入 URL 地址并按 Enter 键)或在 ItemsList.ascx 用户控件上单击“清除过滤器”链接时(非 Ajax 模式)执行。这两个方法的主要区别在于,InitialList
方法通过 Request.QueryString
值过滤数据,而 MaintainList()
方法通过发布回服务器的隐藏输入字段进行过滤,即通过单击表格控件。如果 sortBy
和 sortMode
参数已填充,这两种方法都会对数据进行排序。
首先,让我们看看 InitialList
方法的作用。此方法针对什么类型的 URL 执行?
-
/ => pageSize = 4, page = 1, sortMode = "", sortBy =""
-
4/2 => pageSize = 4, page = 2, sortMode = "", sortBy = ""
-
/4/2/id/asc?filter=id:1,2 => pageSize = 4, page = 2, sortBy = "id", sortMode = "asc"
-
/6/1/birthdate/desc?filter=name:George, john,emily,ismarried:true => pageSize = 6, page = 1, sortBy = "birthdate", sortMode = "desc"
上述 URL 由 Global.asax.cs 文件中的 RouteCollection
在 RegisterRoutes
方法中定义。上述最后两个 URL 由以下模式匹配
routes.MapRoute (
null, // Route name
"{pageSize}/{page}/{sortBy}/{sortMode}", // URL with parameters
new { controller = "Entities", action = "InitialList" },
new { pageSize = @"\d+", page = @"\d+" }
);
InitialList
方法调用 FilterByQueryString
方法,该方法接收查询字符串(例如 filter=id:1,2,name=john)并返回一个 FiltersData
对象。FiltersData
类定义了一个字典,该字典存储我们 Person
对象的每个属性名以及一个 FilterData
数组。FilterData
类有两个属性
public String FilterText{…}
public bool IsActive {…}
FilterText
属性存储 Person
对象属性的值,IsActive
属性存储过滤器是否激活。
FiltersData
对象存储在 ViewData["filters"]
字典中,用户控件使用该字典来填充 HTML 页面上过滤器的初始状态。现在出现的问题是,如何将过滤器值存储在我们的 HTML 页面上?在我的解决方案中,ID 过滤器的生成 HTML 将如下所示
<div style="display: none;">
…
<div id="div_Name">
<input id="filter_Name_George" name="filter_Name_George"
type="hidden" value="False" />
<input id="filter_Name_John" name="filter_Name_John" type="hidden" value="False" />
…
</div>
…
</div>
这样,属性名(例如 Name
)和相应的过滤器值(例如 George, John
)就可以很容易地通过 JavaScript 代码提取。
现在让我们讨论一下用户控件是如何构建的。首先,我将描述 ItemsList.ascx 用户控件,它更容易。用户控件生成的典型标记如下所示
<table id="table_Persons">
<thead>
<tr>
<th class="header_ID">
<a id="a_ID" href="#">ID
<img class="sortImage" src="/Content/Images/down-arrow.jpg" alt="" />
<img class="sortImage" src="/Content/Images/up-arrow.jpg" alt="" />
</div>
</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>1</td>
<td>George</td>
<td>12/1/1980 1:14:15 PM</td>
<td>True</td>
</tr>
</tbody>
<tfoot>
<tr>
<td style="text-align: left;" colspan="4">
<a href="#" class="selectedPage">1</a>
<a href="#" class="defaultPage">2</a>
<a href="#" class="defaultPage">3</a>
</td>
</tr>
</tfoot>
</table>
如果您查看 ItemsList.ascx 用户控件的标记,您会注意到生成 HTML 表格的代码,在其下方,我们有生成过滤器值标记(参见上面的标记)和一些输入字段的代码,这些输入字段填充了来自 ViewData
字典的数据。所有这些隐藏输入字段都包含在一个“using (Html.BeginForm(…))
”语句中,这样所有这些值都在回发时提交到服务器。JavaScript 代码负责在提交到服务器之前填充这些值。
首先,让我们忽略 JavaScript 代码如何设置过滤器值,假设所有隐藏字段的值都已设置,页面已提交,并且 MaintainList()
方法被执行。MaintainList()
方法具有以下签名
public ActionResult MaintainList (int pageSize, int page,
String showFilter, String sortBy, String sortMode, String scrollTop FormCollection fc)
此方法的参数与 InitialList
方法的参数相同,只是增加了“showFilter
”类型为 String
和“fc
”类型为 FormCollection
。fc
对象包含 form
元素内的所有 HTML 控件及其值。因为我们将此方法声明为包含与隐藏字段名称对应的所有参数,除了存储过滤器值的那些,我们需要从 fc
对象中仅提取名称中包含“filter
”文本的隐藏字段值(请注意,每个过滤器的隐藏字段都由代码命名
<%=Html.Hidden ("filter_" + propertyName + "_" + fData.FilterText, fData.IsActive) %>
假设我们有了这些值,我们可以构建一个对应于用户设置的实际过滤表达式的 FiltersData
对象。然后,通过查看 FiltersData
对象的字典,可以轻松地进行实际过滤。关于此方法的最后一点是,它区分请求是否是 Ajax 请求:如果是,则用相关值填充 ViewData
字典,否则返回一个 JsonResult
对象到客户端,该对象又包含更新页面所需的所有相关值。
现在让我们关注 ItemsList.ascx 用户控件引用的 JavaScript 文件 ItemsListJS.js。正如我提到的,我使用 JQuery 来遍历 DOM 并设置元素的值和属性。正如我们所见,过滤、排序和分页表格的逻辑都在服务器端,所以我们的 JavaScript 代码需要为服务器端代码准备相关数据,以及管理如何在用户单击过滤器链接时显示过滤器面板。
“cbk_Instant
”复选框的状态存储在 cookie 中,因此我们无需在请求和响应之间来回传递此信息。值得一提的是“hdn_showFilter
”隐藏字段的作用:如果立即过滤开启,它会存储过滤器文本所属的属性名。我们需要它,因为当过滤器复选框的状态更改时,页面会被提交,而 HTTP 是一个无状态协议,浏览器会忘记我们有一个打开的过滤器面板。我们需要知道哪个过滤器面板应该再次打开,通过查看此隐藏字段的值,我们可以找到答案。如果此值为一个空的 string
,则意味着不应打开任何过滤器面板。
在每次响应时,我们通过调用 createFilterPanel(a, propertyName)
函数为表格的每一列构建一个过滤器面板。该函数的参数“a
”是一个具有类值设置为“filterButton
”的锚元素,参数“propertyName
”是列存储的属性名。请注意 HTML 标记(在这种情况下,属性名是“Name
”)
<th class="header_Name">
<a id="a_ID" href="#">Name</a>
<div style="float: right; width: 0px;"></div>
<a href="#" class="filterButton">
<img src="/Content/Images/up.jpg" alt="" />
</a>
…
</th>
每个过滤器面板应包含一个复选框列表,其状态基于 FORM
元素内的隐藏字段值设置
<form … >
…
<div id="div_Name">
<input id="filter_Name_George" name="filter_Name_George"
type="hidden" value="False" />
<input id="filter_Name_John" name="filter_Name_John"
type="hidden" value="False" />
…
</div>
…
</form>
createFilterPanel
函数根据隐藏字段动态构建复选框列表。创建的过滤器面板“pnl_Filter
”是 YAHOO.widget.Panel
类型。Panel
组件适合容纳我们的复选框列表。请注意,所有过滤器面板都是在页面加载完成后预先创建的,并且只有当用户单击相应的锚元素时,过滤器面板才会打开。需要注意的一点是,当打开过滤器面板时,我们遍历属性名的复选框列表,并为每个复选框元素插入“initialValue
”属性,其值为复选框的当前状态。我们为什么需要这个?想象以下场景:立即过滤关闭且未应用任何过滤器;用户单击过滤器锚点,打开过滤器面板,用户选中一些复选框,但在关闭过滤器面板之前,他没有按下面板上的过滤器按钮。这样就不会应用任何过滤,但复选框仍然被选中,所以当同一个过滤器面板再次打开时,用户会看到被选中的复选框,并注意到这些复选框的过滤并未应用。我们希望消除这种行为。
以下代码设置“initialValue
”属性
$("#pnl_Filter_" + propertyName + " input[type='checkbox']").each(function (i, input) {
$(input).attr("initialValue", $(input).attr("checked"));
});
如果立即过滤开启,当过滤器复选框的状态更改时,页面会被提交,但我们不希望用户在过滤进行中通过选中/取消选中复选框来弄乱过滤器复选框。因此,我们通过调用 changePopupStatus
函数来禁用过滤器面板上的控件。此函数接收一个指示启用或禁用控件的值以及过滤器面板的 ID。请注意,我们不需要启用过滤器面板,因为完全回发会使此设置失效。
我们省略了设置过滤器隐藏字段的代码的讨论。setupFilterValues
(propertyName
)函数的目的是遍历与作为参数接收的属性名对应的复选框列表,并根据复选框的状态设置相应的隐藏字段值。
function setupFilterValues(propertyName) {
$("#pnl_Filter_" + propertyName + " input").each(function (i, input) {
var hdn_input =
$("#div_" + propertyName + " input[name='filter_" +
propertyName + "_" + input.value + "'][type='hidden']");
hdn_input.attr("value", input.checked);
});
};
关于排序的一些说明:隐藏字段“hdn_sortMode
”存储应按哪个属性进行排序,隐藏字段“hdn_sortMode
”存储排序模式(“asc
”或“desc
”)。当用户单击列标题文本时,排序会立即应用。以下代码处理标题锚点的单击事件
a_HeaderName.click(function () {
if (hdn_SortBy.attr("value") == propertyName &&
hdn_SortMode.attr("value") == "asc") {
hdn_SortMode.attr("value", "desc");
}
else {
hdn_SortBy.attr("value", propertyName);
hdn_SortMode.attr("value", "asc");
}
submit(false);
});
Ajax 启用版本
ItemsListAjax.ascx 和 ItemsListAjaxJS.js 文件与讨论过的文件类似,因此我将只重点介绍它们之间的主要区别。
在 ItemsListAjax
用户控件上,我们有一个 IMG 元素“img_Loader
”;当发出 Ajax 请求时,它会显示。FORM
元素内的隐藏字段与 ItemsList
用户控件的情况相同,事实上,我们不需要 hdn_showFillter
控件,因为这次我们将执行部分回发,并且过滤器面板将只创建一次,即在服务器的第一次响应时。但是为了使用相同的控制器方法 MaintainList()
,我们需要调整其参数列表。
在 ItemsListAjaxJS.js 文件的情况下,与非 Ajax 版本相比,我们会看到更多不同。您注意到的第一件事是,在 FORM
元素的 submit 函数中附加了一个事件处理程序。在此函数中,一个自定义函数被附加到 JQuery.ajax
对象的 success 属性。如果请求成功,则调用此函数。在此函数中,我们需要更新我们表格的内容。
如果您查看 MaintainList()
方法,您会注意到它返回一个 JsonResult
对象。该对象的 Data
属性包含一个自定义对象,我们将其发送回客户端。它定义为
var resultData =
new
{
Items = currentItems,
Pages = (int)Math.Ceiling ((double)_allItems.Count () / pageSize),
Page = page,
SortBy = sortBy,
SortMode = sortMode,
NumberOfRecords = _allItems.Count ()
};
return new JsonResult { Data = resultData };
正如您所看到的,resultData
对象包含我们在客户端更新表格所需的所有相关信息。我将描述如何更新表格的 TR
元素。以下代码执行此操作
$(result.Items).each(function (i, item) {
var tr = "<tr>";
$.each(item, function (property, value) {
if (value.toString().indexOf("Date") != -1) {
var re = /-?\d+/;
var m = re.exec(value);
var d = new Date(parseInt(m[0]));
value = d.format("m/d/yyyy HH:MM:ss TT");
}
tr += "<td>" + value + "</td>";
});
tr += "</tr>";
tbody.append(tr);
});
上面的代码遍历结果对象的所有项,并为每一项创建一个 TR
元素。为了访问每一项的属性值(例如,在本例中是 Person
对象),我们遍历该项的属性。我们也可以通过调用 item.ID
、item.Name
等来访问属性值,但在这种情况下,代码将依赖于 Person
类,并且对其他类不起作用。请注意,在 .NET DateTime
对象的情况下,它的 JSON string
表示形式与 .NET ToString()
方法返回的格式不同(例如,它显示为“/Date(545346000000)/
”,这显然不是我们期望的),所以我们需要将其转换为 JavaScript Date
对象,然后根据需要对其进行格式化。
正如我之前提到的,过滤器面板将由 createFilterPanel ()
函数一次性创建。当立即过滤开启且过滤器复选框的状态更改时,我们需要禁用过滤器面板上的控件,以防止用户弄乱其他复选框。与非 Ajax 版本不同,过滤器面板不会在部分回发时重新创建,因此在禁用它之后,我们需要再次启用它。正如我提到的,changePopupStatus()
函数需要被启用或禁用的过滤器面板的 ID。因此,我们将被禁用过滤器面板的 ID 存储在 $.disabledPopupName
变量中;当 Ajax 响应到达时,我们就知道哪个过滤器面板需要被启用。
MvcTableDemo.Tests
如果您查看 MvcTableDemo.Tests
项目,您会注意到 3 个类。PersonsRepositoryCreator
类有一个 static
方法,该方法创建一个具有指定数量 Person
对象的模拟 IPersonRepository
对象。另外两个类分别包含 ItemsListController
的 InitialList()
方法和 MaintainList()
方法的测试方法。方法的命名方式能够解释它们实际测试的内容,因此我现在不会详细介绍。请注意,在为 MaintainList()
方法编写的测试中,对一个测试(在必要时)有两种版本:一种测试非 AJAX 模式下的内容,另一种测试 AJAX 模式下的内容。
历史
- 2010 年 11 月 17 日:初次发布
- 2011 年 1 月 27 日:源代码更新以处理数据模型中的
NULL
值(例如,Person
对象的属性可以设置为NULL
)。
以上就是全部内容,希望您喜欢。