MVC 的轻量级 AjaxGrid“用户控件”





5.00/5 (7投票s)
在 MVC4/Razor 中实现网格“用户控件”。
引言
从列表中选择和查询数据是任何 Web 应用程序的核心部分之一,大多数商业 Web 应用程序将拥有数十个甚至数百个列表,并迫切需要快速高效地开发它们。实现此类列表有多种方法:传统上,模板在服务器端通过 .aspx 页面实现并绑定到数据控件;最近,MVC 支持客户端模板化的范例,由服务器的 JSON feed 提供支持。本项目扩展了我之前在 CodeProject 中为 ASP.NET 实现的一个方法 一个轻量级的 AJAX.NET 启用的网格控件,该方法将数据源的定义和模板化合并到一个 XML 配置文件中。
AjaxGrid 使用 AJAX(当然)以最高效的方式管理大量网格数据的流——在服务器端构建 HTML 并将生成的字符串直接复制到 innerHTML
。这样,数据传输大小和 JavaScript 运行时都得到了最小化。此外,网格经过优化,可在服务器端渐进式提供大量信息,并通过易于使用的排序和过滤功能支持用户快速钻取到所需子集的用户策略。最后,网格还提供了生成简单、用户可自定义的报告的功能,这些报告可以导出以供进一步处理或供将来参考。
该网格支持以下用户功能
- 内置排序和过滤
- “自动完成”和自由文本过滤选项
- 对大型数据集的“无限滚动”支持,无需分页
- 导出到 CSV 和 JSON
- 可选创建新行,可以是就地编辑或通过 MVC 路由
- 可选数据编辑,可以是就地编辑或通过 MVC 路由
- 就地编辑的内置撤销/重做
- 与 MVC 路由集成
支持有效的用户界面需要大量的功能细节,某种形式的用户控件在结构上保证了整个应用程序的用户界面兼容性,并且可用性增强功能得到一致应用。MVC/Razor 并不真正支持用户控件,因此本项目的一部分是探索如何将此功能集成到框架中。即使您不打算使用网格功能,您也可能对我是如何与 MVC 配合提供此类共享功能感兴趣。
背景
在多个视图和 Controller
类中实现共享功能的简单方法是实现一个派生自 System.MVC.Controller
的类,并将“页面控制器”类派生自它。这很简单,在演示项目中效果很好,但在现实世界中并不可行,因为 C# 类无法从多个类派生,这使得选择性地派生自一个以上的控件成为一场管理噩梦。一种更可持续的方法是使用 HtmlHelper
将共享功能委托给一个对最终用户隐藏的共享 Controller。
当前任务是在不配置或在每个希望使用该功能的视图和控制器中重复样板代码或自定义代码的情况下动态生成 HTML;配置完全在一个 XML 控件文件中定义。此要求的动态性质会产生一系列后果
- AjaxGrid Controller 动态渲染输出——除了一个包含的 DIV 外——因此不使用视图进行渲染(尽管它可以)
- 没有模型,因为数据是根据配置文件动态提取的(尽管代码可以动态生成和导出 JSON 模型)
- LINQ 不适合动态生成数据查询,因此使用了 ADO.NET
- 共享功能必须能够与更“正常的”MVC 实现和路由集成
总体而言,用户控件的功能由静态 AjaGrid
类管理。其核心功能分为在 AjaxGrid.cs 中定义和加载 XML 文件中的数据结构,在 AjaxGrid.DataAccess.cs 中生成 SQL 查询,在 AjaxGrid.Render.cs 中从生成的 SQL 数据源和当前用户选项渲染 HTML,以及在 AjaxGrid.Export.cs 中生成 CSV 和 JSON 的导出格式。我认为使用异步 Controller 进行数据库操作并不划算,请参见:数据库调用是否应异步?。但是,如果您不同意,将 AjaxGridController
升级为异步使用应该相当容易。
使用代码
此实现已针对 .NET4.5 上的 MVC4 进行了定位,但应轻松调整以适用于其他 MVC 版本。附带的示例项目已清空其 packages 文件夹以节省 zip 文件空间——下载项目,然后从您的默认项目之一复制 packages 文件。
为了简化,我包含了一个源文件 zip,其文件夹结构与默认项目中的结构相同,以及一个基于默认 Internet 实现的示例项目,而没有重写不相关的部分,例如 AccountController 和 Views。我也不想预判特定应用程序的问题,例如身份验证、Entity Framework 的使用、JavaScript 和 CSS 的打包策略、应用程序构建过程或包含其他框架——无论是客户端还是服务器端。此外,我没有为示例程序的 Home Controller 实现创建或编辑操作(我不想推测您如何构建模型对象),并且出于美观原因,我将默认字体大小更改为 60%。
我还包含了两个基于 NorthWind 示例数据库的网格,一个非常简单,另一个在 SQL 和支持的内联编辑方面更复杂一些。
对于整个 Web 应用程序,您需要包含以下文件
- App_Data/AjaxGrid.xml:应用程序的配置文件
- Controllers/AjaxGridController.cs:Controller
- AjaxGrid 文件夹:包含 AjaxGrid 类,分为四个部分文件,以提高清晰度
- Scripts/AjaxGrid.js:您需要将其(以及 jQuery+jQueryUI)包含在相关的视图中,或在 App_Start/Bundle.config (NET 4.5) 中将其打包
- Content/AjaxGrid.css:您需要将其包含在相关的视图中(以及 jQueryUI 的日期选择器 CSS),或在 App_Start/Bundle.config (NET 4.5) 中将其打包
- Images/ajaxgrid:包含图像文件的文件夹
- global.asax.cs:在
Application_Start()
中添加AjaxGrid.Load();
。 - web.config:在 web.config 中包含一个名为“DataConnection”的连接字符串(如下所示)
add name="DataConnection" connectionString="Trusted_Connection=true;
MultipleActiveResultSets=True;Server=.\SQLEXPRESS;Database=NorthWind"
providerName="System.Data.SqlClient"
...注意 MultipleActiveResultSets=True
。理想情况下,为了有效缓存 SQL 连接,请与应用程序的其余部分共享此连接字符串。
然后,每个参与的视图都可以使用一个单行 HtmlHelper
来渲染其网格,如下所示
@Html.DisplayAjaxGrid(@Url, 1, "Edit","Create",(AjaxGrid.ValidateEventHandler)ViewBag.Validator)
其中 @Url
是当前 URL,1 是 gridId
(在 AjaxGrid.xml 中设置),“Edit”和“Create”是可选的调用 Controller 的 Action Methods,ViewBag.Validator
包含一个可选的委托,用于对可编辑网格执行自定义数据验证(如下所示)(注意,需要显式强制转换)。
AjaxGrid.ValidateEventHandler v = (fieldname, value) =>
{
if (fieldname == "book" && value.Length == 0)
return fieldname + ": A valid string must be entered\r\n";
return "";
};
ViewBag.Validator = v;
将 gridId
设置为 ViewBag 参数也可能很有用,如果您想根据用户权限或应用程序状态动态更改显示的网格。
XML 配置文件
AjaxGrid.xml 包含两个主要部分,一个部分定义系统文本(例如用于填充 SELECT
选项的静态数组),另一个部分定义网格本身。XML 格式应该可以快速创建大量网格和报告。
网格在定义时从零开始计数,因此为了方便维护,不应移动网格定义。每个网格都使用以下属性进行定义
Id
:数字网格 ID,仅供参考(从零开始计数)Name
:仅用于文档目的DbTable
:查询的核心数据库表DbKey
:包含 DbTable 主键的列Height
:用于将网格包含在滚动 div 中NewRow
:指示是否需要创建新行的图标
每个网格内部都有定义查询 SQL 和列定义列表的元素。
查询 SQL 是网格内容的核心定义,一旦到位,就可以轻松管理列定义。查询 SQL 应以 SELECTx
或 SELECTDISTINCTx
开头,并自动转换为“SELECT TOP n
”或“SELECT DISTINCT TOP n
”以支持无限滚动。SQL 还应以 WHERE 子句结尾,可以附加额外的过滤器,即使该子句是虚拟的,例如 WHERE 1=1
。
SQL 中选择的每个表列都将使用以下属性或子元素进行定义(按 SELECT 语句确定的顺序)
Header
:用户显示的标题(默认情况下也是数据库中的列名)ColumnName
:当用户显示的标题与列名不匹配时使用(例如,“Company Name”vs. “CompanyName”)SortName
:当需要自定义排序列时使用Width
:为列提供固定布局Editable
:对于此列的就地编辑是可选的SelectOptionsArray
:对于 SELECT 下拉列表,系统文本部分中静态选项数组的名称SelectOptionsSQL
:对于 SELECT 下拉列表,用于生成要从数据库读取的 Id 和 Value 数据集的 SQLColumnDbKey
:对于连接到另一个表的编辑列,就地编辑是可选的UpdateTable
:对于连接到另一个表的编辑列,就地编辑是可选的CustomUpdateSQL
:当需要自定义更新 SQL 时,就地编辑是可选的FreeTextSql
:我没有包含自由文本用户界面,但此定义了数据库中的自由文本搜索列
大多数列不需要所有这些参数,快速查看两个示例模板可以帮助您了解所有这些内容在实践中是如何工作的。
关注点
启用快速用户排序和过滤
一旦最终用户执行了排序或过滤操作,该操作就会通过 AJAX 调用通知并应用于显示的网格。一个理想的功能是在输入自由格式输入过滤器时能够“自动完成”;而不是提供选项列表,网格会在输入过滤器字符串时自动过滤。
为了使其有效工作,有两种机制在起作用:首先,在进行下一次过滤 AJAX 调用服务器之前,有一个半秒的延迟——这使得快速输入的多个按键可以打包并作为一个 AJAX 请求发送。用户很快就会习惯这一点,并且只输入创建有效过滤器字符串所需的字符数。例外情况是当用户按下 TAB 或 ENTER 时,在这种情况下,我们可以假设该列的过滤器输入已完成,并且请求会立即发送。其次,客户端对多个 AJAX 过滤调用进行了优化,因为一旦发送了 AJAX 过滤请求,就无法召回它,即使后来输入了更多字符。网格 JavaScript 维护一个过滤器请求计数器,并且只渲染最后一个发送的过滤器的响应,消除了多余的客户端工作,以便在最新数据到达时客户端能够快速渲染。
如果数据库已设置为支持自由文本搜索,则实现起来也很容易,我没有包含用户界面,尽管 AjaxGrid.xml 中有一个示例定义,说明如何定义自由文本搜索的目标列。请注意,“自动完成”对于自由文本搜索实际上并不起作用,因为它操作的是整个单词,因此一个传统的搜索按钮最好用于启动一个自由文本过滤器 AJAX 命令,并从关联的输入字段获取文本。
数据安全
需要两个关键数据安全领域:防止 SQL 注入和 XSS(跨站脚本)。任何由用户输入并将包含在 SQL 语句中的数据,无论是通过数据录入还是用作过滤器,都必须通过参数附加到 SQL 查询。通过对所有传出的用户生成数据进行 HttpUtility.HtmlEncode
清理来防止 XSS 攻击。
请注意,SQL 会在接收时进行清理,以保护其 SQL 查询,而 HTML 在传出时进行清理,这是 MVC 的标准做法。一次性清理用户数据可能比多次传出更有效,但风险也更高——但这取决于您对数据库中文本字段是否始终预先清理的信心;这里使用了更安全的选择,即始终清理传出的用户数据。
AjaxGridController
中的大多数方法只能通过 AJAX 调用,并且通过自定义属性 [AjaxOnly]
来强制执行,确保它们提供的信息仅限于本地引用者。所有 AjaxGrid 方法还强制执行使用 GET、POST 或 DELETE 操作来访问其功能。
但是,CSV 导出方法必须通过回发获取——这使得它可能可以从任何引用者调用:检查引用者不是解决方案,因为引用者可以被伪造。如果数据安全很重要并且已在调用 Controller 中实现,则建议对此方法应用 [Authorize]
属性和 [ValidateAntiForgeryToken]
属性。这些属性需要应用程序身份验证和/或视图或 Controller 设置(Html.AntiForgeryToken()
)才能工作。有关反伪造问题的讨论,包括如何从 jQuery 读取反伪造令牌,请参阅此博客,有关如何自动使用 [Authorize]
属性保护所有 Controller 的信息,请参阅此处。
AjaxGrid 使用字符串或 JSON 来响应其客户端。但是,只有在导出到 JSON 的情况下,JSON 响应才能包含数组,这在使用 GET 时存在潜在的安全风险,为防止 XSS 攻击,必须为 AjaxGrid 的 JSON 导出操作强制执行 POST 操作。
jQuery 的使用
与良好的 MVC 实践一致,使用 jQuery 大大简化了该模块的 JavaScript;您需要包含 jQuery 和 jQueryUI(用于日期选择器)。关于如何打包和/或使用 CDN 管理 JavaScript,没有做出任何假设,这很大程度上取决于应用程序的选择。
非侵入式 jQuery 事件管理非常适合在一个地方记录所有处理过的事件,但对于管理 unbind 而不发生内存泄漏——特别是对于服务器生成的 HTML——有点棘手。保持 C# 和 JavaScript 事件管理同步是一个挑战(除非您也生成服务器端的 JavaScript)。我确保在页眉行中添加和删除所有事件是动态的,但一些内容事件处理器的接口仍然是服务器创建的。
为了尽可能压缩内容数据,事件被委托给内容 TD 元素;特定列的标志保留在标头单元格的“data-”属性中——这些可以通过将 TD 的 cellindex 与标头行 TD 的 cellindex 进行匹配来找到。
内联编辑的设计模式
几乎按照定义,Web 应用程序将供多个用户同时活动。
在此环境中,编辑单个字段或插入完整的新行通常风险较低,但请注意,如果编辑单个字段相当于递增计数,则应生成一些自定义 SQL 来调用存储过程,以实现锁定-递增-解锁序列以防止竞态条件。更新多个字段(来自可能“过时”的模型)更为复杂——让 Entity Framework 来管理它,尽管可能效率不高。
由于最终用户可以在每次更改字段时直接更新数据库,因此撤销/重做功能对于良好的用户体验至关重要。一旦进行了编辑,它将自动弹出。
如果您配置了网格,或使用相同数据库表的其他无关应用程序来允许插入或删除,则存在“页面”在执行无限滚动时可能过时的风险——导致在“页面”边界处出现遗漏或重复条目。这就是为什么每次分页操作(滚动到网格末尾)都会启动网格的完整重新渲染,而不是每次只添加几行。我认为这是合理的,因为用户很快就会学会排序和过滤是比逐页浏览数据更有效的数据提取方式。尽管如此,您也可以选择后一种策略,在这种情况下,我建议使用 SQL Server 2012 的 OFFSET/FETCH
而不是 SELECT TOP
。
扩展 AjaxGrid
AjaxGrid 肯定是在考虑了扩展性的前提下构建的。目前支持的列类型相对较少(文本、下拉列表、日期、链接、删除等)。要创建自定义列类型,只需向 AjaxGrid.columnType
枚举添加一个新名称即可。然后,该列类型可以在 XML 文件中进行选择。新列类型默认文本格式,但其过滤或在页眉或内容中的渲染方式可能有所不同,最简单的自定义方法是搜索类型,如 columnType.Date
,并在 columnType
出现的地方的 switch 语句中添加一个新 case,您可以在其中为新列类型添加新行为或渲染。如果列可编辑且需要自定义编辑,您可能还必须查看 AjaxGrid.js。
结论
MVC 提供了一个框架,它使得 AjaxGrid 用户控件的实现非常简洁,尽管它本身不原生支持用户控件。提供干净通用接口的主要问题在于,控件依赖于调用 Controller 来确定是否可以(因此应该)应用 [Authorize]
和/或 [ValidateAntiForgeryToken]
属性。
历史
版本 1。