服务 - 列表请求处理程序 (ListRequestHandler)
这是处理来自客户端列表请求的基类,如,从网格列表的请求。
让我们首先介绍该类是在何时及如何处理列表请求的:
首先必须从客户端触发列表请求。可能的情形有:
a) 打开含网格的列表页面。在创建网格对象之后,基于当前的可见列、初始排序、过滤器等建立了一个 ListRequest 对象,并将其提交到服务器端。
b) 用户点击列头排序、点击分页按钮或者刷新按钮时触发与情形 A 同样的事件。
c) 手动使用 XYZService.List 方法调用列表服务。
MVC XYZController(在 XYZEndpoint.cs 文件中) 的服务请求 (AJAX) 到达服务器,请求参数从 JSON 反序列化为 ListRequest 对象。
- XYZEndpoint 调用 XYZRepository.List 方法,并以检索到的 ListRequest 对象作为其参数。
- XYZRepository.List 方法创建一个 ListRequestHandler (XYZRepository.MyListHandler) 的子类并使用 ListRequest 作为参数调用其 Process 方法。
- ListRequestHandler.Process 方法根据 ListRequest、实体类型(Row)的元数据及其他信息构建动态 SQL 查询语句,并执行它。
- ListRequestHandler.Process 返回 ListResponse,它包含要返回行的 Entities 成员。
- XYZEndpoint 接收该 ListResponse,并从 action 中返回它。
- ListResponse 被序列化成 JSON 发送回客户端。
- Grid 接收实体,更新其显示的行及其他像分页状态的部件。
我们将在另一章中介绍如何生成网格和提交列表请求。现在,让我们集中于 ListRequestHandler。
列表请求对象
我们应该先看看 ListRequest 对象有哪些成员:
public class ListRequest : ServiceRequest, IIncludeExcludeColumns
{
public int Skip { get; set; }
public int Take { get; set; }
public SortBy[] Sort { get; set; }
public string ContainsText { get; set; }
public string ContainsField { get; set; }
public Dictionary<string, object> EqualityFilter { get; set; }
[JsonConverter(typeof(JsonSafeCriteriaConverter))]
public BaseCriteria Criteria { get; set; }
public bool IncludeDeleted { get; set; }
public bool ExcludeTotalCount { get; set; }
public ColumnSelection ColumnSelection { get; set; }
[JsonConverter(typeof(JsonStringHashSetConverter))]
public HashSet<string> IncludeColumns { get; set; }
[JsonConverter(typeof(JsonStringHashSetConverter))]
public HashSet<string> ExcludeColumns { get; set; }
}
ListRequest.Skip 和 ListRequest.Take 参数
这些参数用于分页,它们类似于 LINQ 中的 Skip 和 Page 扩展。
这里有一个需要指出的小区别。如果你使用 Take(0),LINQ 无记录返回,而 Serenity 将返回所有的记录。调用 LIST 服务并请求 0 条记录是毫无意义的。
所以,SKIP 和 TAKE 的默认值为 0,并且它们将忽略 0 / undefined。
// returns all customers as Skip and Take are 0 by default
CustomerService.List(new ListRequest
{
}, response => {});
如果你有页面大小为 50 的网格列表,切换到第 4 页,将跳过前 200 条记录,并选取 50 条记录。
// returns customers between row numbers 201 and 250 (in some default order)
CustomerService.List(new ListRequest
{
Skip = 200,
Take = 50
}, response => {});
这些参数根据 SQL 方言转换为有关的 SQL 分页语句。
ListRequest.Sort 参数
此参数接受数组并返回排序后的结果。排序是由生成的 SQL 执行。
SortBy 参数希望接收一个 SortBy 对象的列表:
[JsonConverter(typeof(JsonSortByConverter))]
public class SortBy
{
public SortBy()
{
}
public SortBy(string field)
{
Field = field;
}
public SortBy(string field, bool descending)
{
Field = field;
Descending = descending;
}
public string Field { get; set; }
public bool Descending { get; set; }
}
当需要调用服务端 XYZRepository 的 List 方法先对国家排序,然后按城市倒序,你可能会这样做:
new CustomerRepository().List(connection, new ListRequest
{
SortBy = new[] {
new SortBy("Country"),
new SortBy("City", descending: true)
}
});
SortBy 类有一个自定义的 JsonConverter,因此当在客户端构建一个列表请求,你应该使用一个简单的字符串数组:
// CustomerEndpoint and thus CustomerRepository is accessed from
// client side (YourProject.Script) through CustomerService class static methods
// which is generated by ServiceContracts.tt
CustomerService.List(connection, new ListRequest
{
SortBy = new[] { "Country", "City DESC" }
}, response => {});
这是由于 ListRequest 类定义在客户端,具有略微不同的结构:
[Imported, Serializable, PreserveMemberCase]
public class ListRequest : ServiceRequest
{
public int Skip { get; set; }
public int Take { get; set; }
public string[] Sort { get; set; }
// ...
}
这里使用的列名称应与字段的属性名称对应。不允许使用表达式。下面的做法是不可行的!
CustomerService.List(connection, new ListRequest
{
SortBy = new[] { "t0.FirstName + ' ' + t0.LastName" }
}, response => {});
ListRequest.ContainsText 和 ListRequest.ContainsField 参数
这是网格左上角搜索输入框的快速搜索功能所使用的参数。
当只指定 ContainsText 而 ContainsField 为空时,对所有含 [QuickSearch] 特性的字段执行搜索。
可以定义一些特定的字段列表,以便通过重写 GetQuickSearchField() 方法对客户端网格执行搜索。所以当在快速搜索输入框中选择这些字段,则只执行对所选列的搜索。
如果将 ContainsField 设置为没有快速搜索特性的字段名称,出于安全目的,系统将引发异常。
像往常一样,搜索使用动态 SQL 的 LIKE 语句完成。
CustomerService.List(connection, new ListRequest
{
ContainsText = "the",
ContainsField = "CompanyName"
}, response => {});
SELECT ... FROM Customers t0 WHERE t0.CompanyName LIKE '%the%'
如果 ContainsText 为 null 或空字符串,将忽略该值。
ListRequest.EqualityFilter 参数
EqualityFilter 是一个字典,允许按某些字段进行快速相等筛选,用于网格上面的下拉列表快速过滤器(用 AddEqualityFilter 帮助类定义)。
CustomerService.List(connection, new ListRequest
{
EqualityFilter = new JsDictionary<string, object> {
{ "Country", "Germany" }
}
}, response => {});
SELECT * FROM Customers t0 WHERE t0.Country = "Germany"
再次提醒:你应该使用属性名称作为相等字段键(equality field keys),而不能使用表达式。Serenity 不允许客户端有任何随心所欲的 SQL 表达式,以防止 SQL 注入。
请注意,类似于 ContainsText,它将忽略 null 值和空字符串值,因此不能在 EqualityFilter 使用空值或 null 值进行筛选,这样的请求将返回的所有记录:
CustomerService.List(connection, new ListRequest
{
EqualityFilter = new JsDictionary<string, object> {
{ "Country", "" }, // won't work, empty string is ignored
{ "City", null }, // won't work, null is ignored
}
}, response => {});
如果你试图用空的国家条件筛选客户,请使用的 Criteria 参数。
ListRequest.Criteria
此参数接受条件对象,类似于我们在流式 SQL 章节谈到的服务端 Criteria 对象。唯一不同的是,由于这些条件对象是发送自客户端,因此必须验证其不能包含任何随心所欲的 SQL 表达式。
下面的服务请求将返回国家和城市都为空的客户:
CustomerService.List(connection, new ListRequest
{
Criteria = new Criteria("Country") == "" |
new Criteria("City").IsNull()
}, response => {});
你可以设置生成 ListRequest 的 Criteria 参数,它将在 XYZGrid.cs 像下面这样提交:
protected override bool OnViewSubmit()
{
// only continue if base class didn't cancel request
if (!base.OnViewSubmit())
return false;
// view object is the data source for grid (SlickRemoteView)
// this is an EntityGrid so view.Params is a ListRequest
var request = (ListRequest)view.Params;
// we use " &= " here because otherwise we might clear
// filter set by an edit filter dialog if any.
request.Criteria &=
new Criteria(ProductRow.Fields.UnitsInStock) > 10 &
new Criteria(ProductRow.Fields.CategoryName) != "Condiments" &
new Criteria(ProductRow.Fields.Discontinued) == 0;
return true;
}
还可以在网格上类似地设置 ListRequest 的其他参数。
ListRequest.IncludeDeleted
此参数只对实现了 IIsActiveDeletedRow 接口的行才有用。如果有这样接口的行,列表处理程序默认只返回没有被删除的行 (IsActive != -1)。这些行并没有被实际删除,而是被标记为已删除。
如果此参数为 True,列表处理程序将不检索 IsActive 列而返回所有的行。
一些网格对这样的行在右上角有一个小橡皮擦图标来切换该标识,从而可显示或隐藏已删除的记录(默认)。
ListRequest.ColumnSelection 参数
Serenity 力图只从 SQL 服务器为实体加载必需的列,以使 SQL Server <-> WEB 服务器之间保持尽可能低的网络通信量。
ListRequest 有一个 ColumnSelection 参数使你可以控制从 SQL 加载的列集合。
ColumnSelection 枚举有如下的值定义:
public enum ColumnSelection
{
List = 0,
KeyOnly = 1,
Details = 2,
}
默认情况下,网格列表从 “ColumnSelection.List” 模式(可以更改)的列表服务中请求记录。因此,其列表请求看起来像这样:
new ListRequest
{
ColumnSelection = ColumnSelection.List
}
在 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 看起来像这样:
new ListRequest
{
ColumnSelection = ColumnSelection.List,
IncludeColumns = new List<string> {
"ProductID",
"ProductName",
"SupplierName",
"..."
}
}
因此,这些额外的视图字段也包含在 选择(selection)。
如果你有一个网格列表,出于性能考虑你应该只能加载可见的列,且它的 ColumnSelection 级别重写为 KeyOnly 。请注意,非可见表字段不会出现在客户端行(row)中。
ListRequest.ExcludeColumns 参数
IncludeColumns 的相反功能是 ExcludeColumns。比方说在网格列表的行中有一个永远也不会显示的类型为 nvarchar(max) 的 Notes 字段。为了降低网络流量,可以选择不在产品网格中加载此字段:
new ListRequest
{
ColumnSelection = ColumnSelection.List,
IncludeColumns = new List<string> {
"ProductID",
"ProductName",
"SupplierName",
"..."
},
ExcludeColumns = new List<string> {
"Notes"
}
}
OnViewSubmit 是设置此参数(及一些其他参数)的最佳场所:
protected override bool OnViewSubmit()
{
if (!base.OnViewSubmit())
return false;
var request = (ListRequest)view.Params;
request.ExcludeColumns = new List<string> { "Notes" }
return true;
}
在服务端控制加载
你可能想要从 ColumnSelection.List 排除一些像 Notes 这样的字段,而不是显式地在网格中排除它们。使用 MinSelectLevel 特性可以实现此目的:
[MinSelectLevel(SelectLevel.Details)]
public String Note
{
get { return Fields.Note[this]; }
set { Fields.Note[this] = value; }
}
这是加载字段时控制不同 ColumnSelection 级别的 SelectLevel 枚举:
public enum SelectLevel
{
Default = 0,
Always = 1,
Lookup = 2,
List = 3,
Details = 4,
Explicit = 5,
Never = 6
}
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 :表示永远不会加载此字段!用于不应该发送到客户端的字段(如,密码哈希值)。