服务 - 列表请求处理程序 (ListRequestHandler)

优质
小牛编辑
141浏览
2023-12-01

这是处理来自客户端列表请求的基类,如,从网格列表的请求。

让我们首先介绍该类是在何时及如何处理列表请求的:

  1. 首先必须从客户端触发列表请求。可能的情形有:

    a) 打开含网格的列表页面。在创建网格对象之后,基于当前的可见列、初始排序、过滤器等建立了一个 ListRequest 对象,并将其提交到服务器端。

    b) 用户点击列头排序、点击分页按钮或者刷新按钮时触发与情形 A 同样的事件。

    c) 手动使用 XYZService.List 方法调用列表服务。

  2. MVC XYZController(在 XYZEndpoint.cs 文件中) 的服务请求 (AJAX) 到达服务器,请求参数从 JSON 反序列化为 ListRequest 对象。

  3. XYZEndpoint 调用 XYZRepository.List 方法,并以检索到的 ListRequest 对象作为其参数。
  4. XYZRepository.List 方法创建一个 ListRequestHandler (XYZRepository.MyListHandler) 的子类并使用 ListRequest 作为参数调用其 Process 方法。
  5. ListRequestHandler.Process 方法根据 ListRequest、实体类型(Row)的元数据及其他信息构建动态 SQL 查询语句,并执行它。
  6. ListRequestHandler.Process 返回 ListResponse,它包含要返回行的 Entities 成员。
  7. XYZEndpoint 接收该 ListResponse,并从 action 中返回它。
  8. ListResponse 被序列化成 JSON 发送回客户端。
  9. Grid 接收实体,更新其显示的行及其他像分页状态的部件。

我们将在另一章中介绍如何生成网格和提交列表请求。现在,让我们集中于 ListRequestHandler。

列表请求对象

我们应该先看看 ListRequest 对象有哪些成员:

  1. public class ListRequest : ServiceRequest, IIncludeExcludeColumns
  2. {
  3. public int Skip { get; set; }
  4. public int Take { get; set; }
  5. public SortBy[] Sort { get; set; }
  6. public string ContainsText { get; set; }
  7. public string ContainsField { get; set; }
  8. public Dictionary<string, object> EqualityFilter { get; set; }
  9. [JsonConverter(typeof(JsonSafeCriteriaConverter))]
  10. public BaseCriteria Criteria { get; set; }
  11. public bool IncludeDeleted { get; set; }
  12. public bool ExcludeTotalCount { get; set; }
  13. public ColumnSelection ColumnSelection { get; set; }
  14. [JsonConverter(typeof(JsonStringHashSetConverter))]
  15. public HashSet<string> IncludeColumns { get; set; }
  16. [JsonConverter(typeof(JsonStringHashSetConverter))]
  17. public HashSet<string> ExcludeColumns { get; set; }
  18. }

ListRequest.Skip 和 ListRequest.Take 参数

这些参数用于分页,它们类似于 LINQ 中的 Skip 和 Page 扩展。

这里有一个需要指出的小区别。如果你使用 Take(0),LINQ 无记录返回,而 Serenity 将返回所有的记录。调用 LIST 服务并请求 0 条记录是毫无意义的。

所以,SKIP 和 TAKE 的默认值为 0,并且它们将忽略 0 / undefined。

  1. // returns all customers as Skip and Take are 0 by default
  2. CustomerService.List(new ListRequest
  3. {
  4. }, response => {});

如果你有页面大小为 50 的网格列表,切换到第 4 页,将跳过前 200 条记录,并选取 50 条记录。

  1. // returns customers between row numbers 201 and 250 (in some default order)
  2. CustomerService.List(new ListRequest
  3. {
  4. Skip = 200,
  5. Take = 50
  6. }, response => {});

这些参数根据 SQL 方言转换为有关的 SQL 分页语句。

ListRequest.Sort 参数

此参数接受数组并返回排序后的结果。排序是由生成的 SQL 执行。

SortBy 参数希望接收一个 SortBy 对象的列表:

  1. [JsonConverter(typeof(JsonSortByConverter))]
  2. public class SortBy
  3. {
  4. public SortBy()
  5. {
  6. }
  7. public SortBy(string field)
  8. {
  9. Field = field;
  10. }
  11. public SortBy(string field, bool descending)
  12. {
  13. Field = field;
  14. Descending = descending;
  15. }
  16. public string Field { get; set; }
  17. public bool Descending { get; set; }
  18. }

当需要调用服务端 XYZRepository 的 List 方法先对国家排序,然后按城市倒序,你可能会这样做:

  1. new CustomerRepository().List(connection, new ListRequest
  2. {
  3. SortBy = new[] {
  4. new SortBy("Country"),
  5. new SortBy("City", descending: true)
  6. }
  7. });

SortBy 类有一个自定义的 JsonConverter,因此当在客户端构建一个列表请求,你应该使用一个简单的字符串数组:

  1. // CustomerEndpoint and thus CustomerRepository is accessed from
  2. // client side (YourProject.Script) through CustomerService class static methods
  3. // which is generated by ServiceContracts.tt
  4. CustomerService.List(connection, new ListRequest
  5. {
  6. SortBy = new[] { "Country", "City DESC" }
  7. }, response => {});

这是由于 ListRequest 类定义在客户端,具有略微不同的结构:

  1. [Imported, Serializable, PreserveMemberCase]
  2. public class ListRequest : ServiceRequest
  3. {
  4. public int Skip { get; set; }
  5. public int Take { get; set; }
  6. public string[] Sort { get; set; }
  7. // ...
  8. }

这里使用的列名称应与字段的属性名称对应。不允许使用表达式。下面的做法是不可行的!

  1. CustomerService.List(connection, new ListRequest
  2. {
  3. SortBy = new[] { "t0.FirstName + ' ' + t0.LastName" }
  4. }, response => {});

ListRequest.ContainsText 和 ListRequest.ContainsField 参数

这是网格左上角搜索输入框的快速搜索功能所使用的参数。

当只指定 ContainsText 而 ContainsField 为空时,对所有含 [QuickSearch] 特性的字段执行搜索。

可以定义一些特定的字段列表,以便通过重写 GetQuickSearchField() 方法对客户端网格执行搜索。所以当在快速搜索输入框中选择这些字段,则只执行对所选列的搜索。

如果将 ContainsField 设置为没有快速搜索特性的字段名称,出于安全目的,系统将引发异常。

像往常一样,搜索使用动态 SQL 的 LIKE 语句完成。

  1. CustomerService.List(connection, new ListRequest
  2. {
  3. ContainsText = "the",
  4. ContainsField = "CompanyName"
  5. }, response => {});
  1. SELECT ... FROM Customers t0 WHERE t0.CompanyName LIKE '%the%'

如果 ContainsText 为 null 或空字符串,将忽略该值。

ListRequest.EqualityFilter 参数

EqualityFilter 是一个字典,允许按某些字段进行快速相等筛选,用于网格上面的下拉列表快速过滤器(用 AddEqualityFilter 帮助类定义)。

  1. CustomerService.List(connection, new ListRequest
  2. {
  3. EqualityFilter = new JsDictionary<string, object> {
  4. { "Country", "Germany" }
  5. }
  6. }, response => {});
  1. SELECT * FROM Customers t0 WHERE t0.Country = "Germany"

再次提醒:你应该使用属性名称作为相等字段键(equality field keys),而不能使用表达式。Serenity 不允许客户端有任何随心所欲的 SQL 表达式,以防止 SQL 注入。

请注意,类似于 ContainsText,它将忽略 null 值和空字符串值,因此不能在 EqualityFilter 使用空值或 null 值进行筛选,这样的请求将返回的所有记录:

  1. CustomerService.List(connection, new ListRequest
  2. {
  3. EqualityFilter = new JsDictionary<string, object> {
  4. { "Country", "" }, // won't work, empty string is ignored
  5. { "City", null }, // won't work, null is ignored
  6. }
  7. }, response => {});

如果你试图用空的国家条件筛选客户,请使用的 Criteria 参数。

ListRequest.Criteria

此参数接受条件对象,类似于我们在流式 SQL 章节谈到的服务端 Criteria 对象。唯一不同的是,由于这些条件对象是发送自客户端,因此必须验证其不能包含任何随心所欲的 SQL 表达式。

下面的服务请求将返回国家和城市都为空的客户:

  1. CustomerService.List(connection, new ListRequest
  2. {
  3. Criteria = new Criteria("Country") == "" |
  4. new Criteria("City").IsNull()
  5. }, response => {});

你可以设置生成 ListRequest 的 Criteria 参数,它将在 XYZGrid.cs 像下面这样提交:

  1. protected override bool OnViewSubmit()
  2. {
  3. // only continue if base class didn't cancel request
  4. if (!base.OnViewSubmit())
  5. return false;
  6. // view object is the data source for grid (SlickRemoteView)
  7. // this is an EntityGrid so view.Params is a ListRequest
  8. var request = (ListRequest)view.Params;
  9. // we use " &= " here because otherwise we might clear
  10. // filter set by an edit filter dialog if any.
  11. request.Criteria &=
  12. new Criteria(ProductRow.Fields.UnitsInStock) > 10 &
  13. new Criteria(ProductRow.Fields.CategoryName) != "Condiments" &
  14. new Criteria(ProductRow.Fields.Discontinued) == 0;
  15. return true;
  16. }

还可以在网格上类似地设置 ListRequest 的其他参数。

ListRequest.IncludeDeleted

此参数只对实现了 IIsActiveDeletedRow 接口的行才有用。如果有这样接口的行,列表处理程序默认只返回没有被删除的行 (IsActive != -1)。这些行并没有被实际删除,而是被标记为已删除。

如果此参数为 True,列表处理程序将不检索 IsActive 列而返回所有的行。

一些网格对这样的行在右上角有一个小橡皮擦图标来切换该标识,从而可显示或隐藏已删除的记录(默认)。

ListRequest.ColumnSelection 参数

Serenity 力图只从 SQL 服务器为实体加载必需的列,以使 SQL Server <-> WEB 服务器之间保持尽可能低的网络通信量。

ListRequest 有一个 ColumnSelection 参数使你可以控制从 SQL 加载的列集合。

ColumnSelection 枚举有如下的值定义:

  1. public enum ColumnSelection
  2. {
  3. List = 0,
  4. KeyOnly = 1,
  5. Details = 2,
  6. }

默认情况下,网格列表从 “ColumnSelection.List” 模式(可以更改)的列表服务中请求记录。因此,其列表请求看起来像这样:

  1. new ListRequest
  2. {
  3. ColumnSelection = ColumnSelection.List
  4. }

ColumnSelection.List 模式中,ListRequestHandler 返回 字段,因此,这些字段是实际属于该表的字段,而不是来自关联表的视图字段。

有一个例外:表达式 字段只包含 字段的引用,如 (t0.FirstName + ‘ ‘ + t0.LastName) 。 ListRequestHandler 同样加载这些字段。

ColumnSelection.KeyOnly 只包含 ID / 主键 字段。

ColumnSelection.Details 包含所有字段,包括视图字段,除非该字段被显式排除或被标记为 “sensitive”(如,密码字段)。

对话框在 Details 模式加载编辑记录,因此它们也包含视图字段。

ListRequest.IncludeColumns 参数

我们告诉网格在 List 模式下请求记录,因此加载只 字段,那么它如何显示来自其它表的列呢?

网格将可见列的列表发送到列表服务的 IncludeColumns,所以即使它们是视图字段,也在选择(selection)中 包含 这些列。

在内存网格(memory grids)中不能这样做。因为它们不会直接调用服务,你必须在视图字段添加 [MinSelectLevel(SelectLevel.List)] 特性,这样才能在内存详细网格(memory detail grids)加载。

如果你有显示供应商名称(SupplierName)的产品网格列表,它实际的 ListRequest 看起来像这样:

  1. new ListRequest
  2. {
  3. ColumnSelection = ColumnSelection.List,
  4. IncludeColumns = new List<string> {
  5. "ProductID",
  6. "ProductName",
  7. "SupplierName",
  8. "..."
  9. }
  10. }

因此,这些额外的视图字段也包含在 选择(selection)

如果你有一个网格列表,出于性能考虑你应该只能加载可见的列,且它的 ColumnSelection 级别重写为 KeyOnly 。请注意,非可见表字段不会出现在客户端行(row)中。

ListRequest.ExcludeColumns 参数

IncludeColumns 的相反功能是 ExcludeColumns。比方说在网格列表的行中有一个永远也不会显示的类型为 nvarchar(max) 的 Notes 字段。为了降低网络流量,可以选择不在产品网格中加载此字段:

  1. new ListRequest
  2. {
  3. ColumnSelection = ColumnSelection.List,
  4. IncludeColumns = new List<string> {
  5. "ProductID",
  6. "ProductName",
  7. "SupplierName",
  8. "..."
  9. },
  10. ExcludeColumns = new List<string> {
  11. "Notes"
  12. }
  13. }

OnViewSubmit 是设置此参数(及一些其他参数)的最佳场所:

  1. protected override bool OnViewSubmit()
  2. {
  3. if (!base.OnViewSubmit())
  4. return false;
  5. var request = (ListRequest)view.Params;
  6. request.ExcludeColumns = new List<string> { "Notes" }
  7. return true;
  8. }

在服务端控制加载

你可能想要从 ColumnSelection.List 排除一些像 Notes 这样的字段,而不是显式地在网格中排除它们。使用 MinSelectLevel 特性可以实现此目的:

  1. [MinSelectLevel(SelectLevel.Details)]
  2. public String Note
  3. {
  4. get { return Fields.Note[this]; }
  5. set { Fields.Note[this] = value; }
  6. }

这是加载字段时控制不同 ColumnSelection 级别的 SelectLevel 枚举:

  1. public enum SelectLevel
  2. {
  3. Default = 0,
  4. Always = 1,
  5. Lookup = 2,
  6. List = 3,
  7. Details = 4,
  8. Explicit = 5,
  9. Never = 6
  10. }

SelectLevel.Default :默认值,对应于表字段是 SelectLevel.List ,视图字段是 SelectLevel.Details

默认情况下,表字段的选择级别是 SelectLevel.List ,而视图字段是 SelectLevel.Details

SelectLevel.Always :表示此字段可被任何列选择模式选择,包括使用 ExcludeColumns 显式排除的字段。

SelectLevel.Lookup 已经被废弃,请避免使用。检索列由 [LookupInclude] 特性决定。

SelectLevel.List :表示在 ColumnSelection.List 和 ColumnSelection.Details 模式或被 IncludeColumns 参数显式包含时选择此字段。

SelectLevel.Details :表示在 ColumnSelection.Details 模式或被 IncludeColumns 参数显式包含时选择此字段。

SelectLevel.Explicit :表示此字段不应该在任何模式下被选择,除非它显式包含在 IncludeColumns 参数。在网格或编辑对话框使用此字段,是没有意义的。

SelectLevel.Never :表示永远不会加载此字段!用于不应该发送到客户端的字段(如,密码哈希值)。