- 下载Source Code - 3.7 MB
- 下载Service Website Package - 8 MB
- 下载Demo Data Samples - 1.6 MB
- 下载Full Text Indexing Tool - 1.5 MB
GitHub托管项目: 为本文转到GitHub项目GitHub分支 相关文章: ASP的会员商店。net服务为StackExchange:实现一个数据提供者会员+管理系统,第一部分:引导会员+管理系统,第二部分:帐户设置 注意:本系列的每一篇文章都可以为数据服务提供不同的数据模式。由于历史原因,它们位于不同的源代码控制分支上。因此,前面的文章可能不会与后面的文章同步。在其他文章中尝试重用数据服务和样例数据时应该小心。 注:(2014-05-06)数据服务现在运行在。net 4.5.1和Asp下。Net Mvc 5中包含了显著的扩展和改进。一个人应该用新的来代替旧的。web应用程序被升级为在最新的库下运行。增加了许多新的数据集,以支持未来可扩展的信号推送通知功能。 内容 1. 介绍2。背景3。概述 3.1查询智能3.2应用作为服务网关3.3统一接口 3.3.1接口3.3.2绕路和业务逻辑 4. 实现 4.1模型 4.1.2客户端 4.2的操作 4.2.1子集投影4.2.2获取集合信息4.2.3对令牌选项进行排序过滤4.2.4获取页面块4.2.5加载页面4.2.6加载用户 4.3视图 4.3.1查询表达式编辑器4.3.2页面窗口4.3.3用户列表4.3.4用户详细信息 4.4风格4.5和词汇表 4.5.1查询令牌和表达式4.5.2定制4.5.3基于配置的方法 4.5.3.1对javascript 4.5.3.2配置4.5.3.3上下文的依赖 4.5.4基于代码的方法 5. 准备数据服务 5.1全文索引 5.1.1本机的ks 5.1.2统一全文索引和ks 5.1.3的索引工具 6. 数据图,查询 6.1实体图导航6.2参考语法 6.2.2排序表达式 6.3举几个例子 7. 历史 1. 介绍^ 后开始发展并建立一些基础工作我在文章(见这里)和二(见这里),我们正处于一个合适的位置描述的方法之一,以满足一个足够大的系统的一个主要挑战,即如何有效地查找和组织信息系统中使用查询功能的后端数据服务。 这篇文章有点长。希望目录能帮助读者找到感兴趣的信息。 2. 背景^ 在结构化的系统中有许多组织信息的方法。从更静态的如分级文件或目录系统,到更动态的如搜索系统。对于会员,会员+系统还支持会员社交网络,这是另一种可以更有效地寻找人的方式。本文主要介绍搜索系统,其他方法留给以后的文章讨论。 几乎所有的管理系统和非常重要的内容表示系统都是结构化的。在某种意义上,结构化是进行计算的前提条件。结构化的系统,如感兴趣的会员+系统,包含高度结构化的信息,可以正式的(用数据模式)除了非结构化的网页,基于文本的文件,等,代表和编码某一领域的知识被认为是适用于所有系统旨在解决的问题。例如,创建会员+系统是为了处理管理成员的任务和组织中成员之间的互动,它的大多数一般元素和彼此之间的关系是众所周知的,可以形式化成结构。由于上述结构,使用某种形式的结构化查询(SQ)可以实现更高效、更准确的信息查找,该查询源自已知的信息。 由于各种原因,结构化系统很可能包含非结构化信息。这是因为系统越形式化,(正确地)分析、构建和维护就越复杂、越昂贵,就会变得越不灵活和/或甚至更自以为是。找到正确的平衡可能是一门艺术。例如,众所周知,一个成员的物理地址可以分解为一个类似树的结构,它包含一个属于几个集合的数据集合,比如国家集合,州/省集合,城市集合,…等。但是这样的结构可能会使数据库在这里显示起来更加复杂。因此,目前我们在Membership+系统中选择使用非结构化字符串表示地址,因为除了memb之外,我们不需要任何其他特性中的形式化地理信息现在是物理地址。当有需要时,可以在我们的系统中添加,没有太多的困难。此外,规范化的一些类型的信息可能还是实际当时由于缺乏标准化的知识或者由于缺乏可用的质量数据,就像在成员贡献文章,网页,积累了一代又一代的旧数据,等等,或者缺乏技术或金融手段支持大量的他们。可以考虑使用分布式文档存储。因此,我们必须根据系统的总体设计目标来平衡这两者。很久以前,我已经以某种抽象的形式讨论了上述关于信息系统的观点,我认为它更适合现在这篇更具体的文章(见这里的最后几段)。 在非结构化文本数据中查找信息的一个著名系统是关键字索引和搜索系统(KS)。 技术上,后端数据服务能够以统一的方式结合SQ和关键字搜索(SQ+KS)手段。目前的web应用程序所做的是提供自己的查询接口和基于数据服务打开的查询API的定制,为用户提供一个友好的查询前端来查找成员相关信息。这篇文章以尽可能一般的方式描述了哪些可能性,以及如何探索使其发生。这里获得的知识可以应用于根据特定自定义数据模式构建的任何数据服务。 平方的有效利用的前提是用户首先应该知道系统的结构在一定程度上可以制定正确的问题,那么他/她应该知道这样一个结构,表示平方可以正确表达。对于大多数用户来说,第一个问题可能不是大问题,因为这些知识都是通用的。例如,几乎会员系统的每个用户都知道它必须有用户、角色等等。问题有时是如何表现的。一开始,人们可能会有这样的问题:“我如何引用一个成员集,一个成员拥有哪些可以搜索的属性的名称,等等。”换句话说,用户必须学会如何与系统交互,即使他/她大概知道问题是什么。 这就是数据服务的查询智能系统发挥作用的地方。它可以用来指导用户制定正确的SQ+KS表达式,而不必知道关于系统的结构表示的准确信息。由于它的自动完成特性,它还可以大大加快查询表达式的输入速度。下图展示了使用SQ表达式中嵌入的关键字匹配KS,根据成员的注册地址找到一组成员,然后显示其详细信息的过程。用户界面引导用户输入复杂的排序过滤表达式,即查找物理地址中包含关键字“unicon”的用户,取前333个匹配项,然后将找到的条目按用户名升序排序。实际上,它可以在敲击几下键后就组成(n,a,co,k,m,p,a,unicons,f,333),这里“n”是自动完成的“名字”(即用户名),“a”->“提升”等。 图:寻找用户的过程。 注意,上面的查询按钮只有在表达式关闭时才启用,这样用户就不会向数据服务发送错误或不完整的表达式。 3.概述^ 3.1查询智能^ 数据服务为每个数据集的状态机提供一个接口,该接口根据当前查询表达式(语法上下文)提供下一个可能的输入令牌(选项)列表。为了在客户端与它进行交互,数据集的每个服务代理都有一对异步方法(以及它们对应的同步方法) 隐藏,复制Code
public async Task<TokenOptions> GetNextSorterOpsAsync(CallContext cntx,
List<QToken> sorters)
{
...
}
根据排序器和所表示的当前排序表达式生成可能的排序标记列表 隐藏,复制Code
public async Task<TokenOptions> GetNextFilterOps(CallContext cntx,
QueryExpresion qexpr,
string tkstr)
{
...
}
它根据qexpr表示的当前筛选器表达式和当前部分输入tkstr返回筛选器表达式的可能令牌列表。所述代理为实体-名称-服务类型代理,其中实体-名称>是对应数据集实体的类型名称。状态机被设计用来生成既完整又正确的选项(即不生成额外的选项)。这与许多集成开发环境(IDE)所拥有的所谓智能感知系统的实现不同,因为它们不完全对语法上下文敏感,导致生成额外的(即不正确的选项)或甚至丢失选项。 除了提供的选项之外,它还携带类中的其他信息 隐藏长p;复制Code
[DataContract]
public class TokenOptions
{
[DataMember]
public string Hint { get; set; }
[DataMember]
public string CurrentExpr { get; set; }
[DataMember]
public bool QuoteVal { get; set; }
[DataMember]
public bool CanBeClosed { get; set; }
[DataMember]
public List<QToken> Options { get; set; }
}
它包含选项、输入提示、当前表达式是否可以关闭等信息,这些信息为客户端描述了当前语法上下文。 3.2作为服务网关的应用^ 数据服务为客户端提供完整和通用的关系数据操作API。在大多数应用程序场景中,不应该允许外部用户直接调用它们。调用应该由web应用程序内部的某些层处理,这些层根据web应用程序的需求添加安全策略、业务逻辑和数据转换/投影。 图:通过Web应用程序层传递查询相关的调用。 轻量级的“数据代理”层主要负责处理与所有查询通用的请求相关的元信息,这是在MembershipPlusAppLayer45项目中包含的WCF服务中实现的。“安全+业务逻辑”层是在两个项目中实现的,即ArchymetaMembershipPlusStores项目和MembershipPlusAppLayer45项目。所有Asp通用的一些基本安全特性。Net应用程序在ArchymetaMembershipPlusStores项目中处理,其他更复杂的特定于应用程序的安全特性在MembershipPlusAppLayer45项目中实现。 3.3统一接口^ 上面列出的方法的名称和功能可以在我们的数据服务支持的所有数据源中的所有数据集的服务代理类型中找到。更具体地说,这些方法是特定于每个数据集的,CallContext、QToken、QueryExpresion和TokenOptions类型是特定于数据源的。它们确实包含需要在web应用程序所基于的数据服务内部相互区别的特定信息,通过检查源代码可以很容易地找到这些信息。 到目前为止,我们已经走在日益复杂的分析道路上。然而,如果有人试图从客户端的角度找出它们的差异,他/她几乎找不到任何差异,因为这些方法和类具有相同的对应名称的组件。那些已经将那些繁重的工作和肮脏的细节委托给服务处理的客户端,现在可以通过一个综合的简化过程来损失与其角色无关的信息,这个简化过程将在web应用程序中使用映射、投影和路由方法来处理。结果可以实现更简单的操作。 图:虽然我们知道每只鸭子都是独一无二的,但所有的鸭子都一样嘎嘎叫。 我们发现,经过综合后,我们实际上可以为所有的客户端建立一个单一的接口,从而达到一种统一。因为这是一个超越了现有类层次结构的过程,所以在纯强类型系统中建立和维护需要耗费大量精力,特别是在需要处理大量数据集和数据源的情况下。然而。net框架的动态类型和javascript的松散类型性质更容易因为组合在一起,他们支持所谓duck typing自然备份我们的统一方案,其中一个接口用于覆盖所有数据集和数据源的数据查询方面,web应用程序是基础。 3.3.1接口^ 与查询相关的请求,不是特定于应用程序或页面,通过JavaScript调用托管在web应用程序内部的WCF web服务发送到web服务器,即“数据代理”层,这些请求被转换并转发给适当的API方法来处理。WCF服务实现了下面的IDataServiceProxy接口的异步版本 隐藏,复制Code
[ServiceContract(Namespace = " ... ", SessionMode = SessionMode.Allowed)]
public interface IDataServiceProxy
{
[OperationContract]
[WebInvoke(Method = "POST", ...)]
Task<string> GetSetInfo(string sourceId, string set);
[OperationContract]
[WebInvoke(Method = "POST", ...)]
Task<string> GetNextSorterOps(string sourceId, string set, string sorters);
[OperationContract]
[WebInvoke(Method = "POST", ...)]
Task<string> GetNextFilterOps(string sourceId, string set, string qexpr);
[OperationContract]
[WebInvoke(Method = "POST", ...)]
Task<string> NextPageBlock(string sourceId, string set, string qexpr,
string prevlast);
[OperationContract]
[WebInvoke(Method = "POST", ...)]
Task<string> GetPageItems(string sourceId, string set, string qexpr,
string prevlast);
}
它有一个通用名称IDataServiceProxy。所有输入参数和返回值都是string类型。字符串没有预先建立的结构,因此它们的含义将取决于它是如何解析的。接口的实现将负责将调用路由到合适的解释器,解释器处理请求并返回相应的对象,该对象将被上述实现扁平化为字符串,并返回给负责解释结果的调用者。 由于web应用程序的目标客户端是JavaScript程序,因此上述实现接受和返回值的最佳方式是采用序列化JSON对象的形式。 下面是对所涉及的方法的简要描述。 整个GetSetInfo方法返回一组数据集信息。参数sourceId用于识别数据源请求处理,这样系统可以将请求路由到适当的处理程序,就像在我们的垂直搜索系统能够支持任意数量的后端数据服务类似于当前的一个(见演示)。因为只有一个数据源在th的成员+系统中e目前阶段,即会员+ 1,目前不使用。 GetNextSorterOps方法返回一个可能的标记列表,这些标记用于指定给定corrent排序表达式的集合的排序条件。 GetNextFilterOps方法返回一个可能的令牌列表,用于指定给定corrent查询的set的筛选条件(排序和筛选)qexpr“表达”。 NextPageBlock方法返回数据集集的页帧块,给定查询表达式qexpr和前一页帧块的最后一项prevlast。页面框架包含页面中的第一项和最后一项。 GetPageItems方法返回一个页面中数据集集的所有项目,给定查询表达式qexpr和上一页的最后一项(如果有prevlast的话)。 3.3.2迂回与业务逻辑^ 并不是所有的请求都路由到这个接口,因为它们非常通用,几乎没有安全性检查和/或结果定制。有些请求由“安全+业务逻辑”层内部的方法处理,以满足应用程序和/或页面的特定需求。例如GetPageItems方法最有可能不会直接调用,因为每个视图(页面)方面的某些实体集需要不同的属性子集和相关实体(即选定部分的实体图)加载,这样数据的预处理和后处理可能是不同的。下面几节将对此进行详细说明。 4. 实现^ 4.1模型^ 4.1.1服务方^ 服务端数据模型包含在MembershipPlusShared45项目的模型子目录中。 每个数据集对应的模型如下: & lt; Entity>集模型。它对应于一组实体的名称。对于用户数据集,类型名称为UserSet。它包含描述集合整体性质的属性。 & lt; Entity> PageBlock模型。它表示一个页帧块的分页实体的名称<Entity>在一定的排序条件下。对于用户数据集,类型名称为UserPageBlock。数据服务在请求时返回多个页帧。返回的页帧数量由Entity>Set类型的PageBlockSize属性的值决定。 & lt; Entity>页面模型。它代表了分页实体名称的页面框架。对于用户数据集,类型名称为UserPage。 & lt; Entity>模型。它表示一个名为name的实体。对于用户数据集,类型名称为user。 还有查询相关的数据模型,包含在MembershipPlusShared45项目的公共子目录中: CallContext模型。它表示数据服务的客户端。 QToken模型。它在查询表达式中表示一个标记。 QueryExpresion模型。它表示一个查询表达式。 TokenOptions模型。它表示当前查询上下文下一个可能的查询表达式标记。 它们由数据服务和使用数据服务的web应用程序共享。 4.1.2客户端^ 除了使用数据服务在数据服务和web应用程序之间交换的CallContext模型之外,所有其他数据模型都应该有一个相应的JavaScript数据模型和(KnockoutJS)视图模型。在可以使用的数据服务站点的Scripts\DbViewModels\MembershipPlus子目录下有一组这样的模型。 然而,KnockoutJS视图模型附带的数据服务很可能需要稍作修改,然后才能在web应用程序中使用 web应用程序的脚本编写环境可能与数据服务不同,因此可能需要付出一些努力使其适应新环境。例如,对服务器的一些调用是通过业务+安全层路由的,而不是通过数据代理层。由于安全原因,数据服务提供的完整数据视图模型可能需要以某种方式进行裁剪,以隐藏更多的内部细节,并突出显示特定应用程序页面中真正需要的内容。 所有与本文相关的JavaScript视图模型都包含在一个文件中,即web应用程序的Scripts\DataService子目录下的MemberSearchPage.js文件,其中的视图模型几乎被重写。例如用户视图模型: 隐藏,收缩,复制Code
function User(data) {
var self = this;
self.Initializing = true;
self.data = data.data;
self.member = data.member;
self.hasIcon = data.hasIcon;
self.iconUrl = appRoot + \'Account/GetMemberIcon?id=\' + self.data.ID;
self.IsEntitySelected = ko.observable(false);
self.more = ko.observable(null);
self.LoadDetails = function (callback) {
if (self.more() != null) {
callback(true);
}
$.ajax({
url: appRoot + "Query/MemberDetailsJson?id=" + self.data.ID,
type: "GET",
beforeSend: function () {
},
success: function (content) {
if (content.hasDetails && content.details.hasPhoto) {
content.details.photoUrl =
appRoot + \'Account/UserPhoto?id=\' + self.data.ID;
}
self.more(content);
callback(true);
},
error: function (jqxhr, textStatus) {
alert(jqxhr.responseText);
callback(false);
},
complete: function () {
}
});
}
self.Initializing = false;
}
它接受一个JSON用户实体数据作为输入,并在创建时将其属性映射到它自己的各种属性。这里几乎没有KnockoutJS可观察对象,因为视图是只读的。不需要编辑。 4.2动作^ 数据服务代理的实现包含在MembershipPlusAppLayer45项目的代理子目录下名为DataServiceProxy.cs的单个文件中。因为在当前的web应用程序中只有一个数据源,即general结构的实现是 隐藏,复制Code
public Task<string> MethodName(string sourceId, string set, ...)
{
// get typed set kind instance ...
switch(type)
{
case EntitySetType.User:
{
... call methods for the user set ...
}
break;
case ...
case ...
}
return null;
}
通常,上面的代码应该包装在与sourceId的switch语句等价的代码中。从JSON格式的参数中恢复数据集的类型 隐藏,复制Code
JavaScriptSerializer jser = new JavaScriptSerializer();
dynamic sobj = jser.DeserializeObject(set) as dynamic;
EntitySetType type;
if (Enum.TryParse<EntitySetType>(sobj["set"], out type))
{
... handler of valid types shown above
}
它包含了客户端JavaScripts设置的查询的各种参数,例如在和当前文章相关的web应用程序的Scripts\DataService子目录下的MemberSearchPage.js文件中 隐藏,复制Code
set: JSON.stringify({
set: setName,
pageBlockSize: self.PageBlockSize(),
pageSize: self.PageSize_(),
setFilter: self.SetFilter
})
是传递给web应用程序内部托管的WCF服务的AJAX调用的参数之一。它定义了可以从It中提取的有效参数。它在set中有集合的名称,在pageBlockSize中有页面块中的页数,在pageSize中有页面中的行数,最后通过setFilter中包含的set过滤器表达式定义感兴趣的项的子集。JSON对象在经过JSON处理后被序列化为字符串。在发送到服务器之前先检查。 什么是集合过滤器,为什么需要它? 4.2.1子集投影^ 由于数据服务可由多个应用程序使用,因此并不是数据服务中的所有注册用户都是当前web应用程序的成员。因此,对用户的查询应该涉及用户数据集中属于当前web应用程序成员的一组记录。用正常的方法处理这可能有点复杂。然而,数据服务有一种系统的子设置机制,可用于定义满足某些过滤条件的子集。然后,可以使用子集作为查询参数,就像使用整个集一样。它可以帮助我们隐藏所涉及的复杂性。例如,在处理对用户数据的查询请求时,设置该值 隐藏,复制Code
userSet.SetFilter = \'UserAppMember.Application_Ref.Name == "MemberPlusManager" &&
( UserAppMember.SearchListing is null || UserAppMember.SearchListing == true )\'
在加载完成后,在页面的JavaScript初始化处理程序中设置(参见下面的内容)。这是一个有效的过滤表达式,它意味着只选择那些关联的应用程序成员资格记录包含一个应用程序引用记录名称为“MemberPlusManager,并且在应用程序级别搜索中没有隐藏的用户记录。”这里的值“MemberPlusManager”是在Web内部定义的。配置配置文件。这个面向对象图的表达式,当翻译成SQL表达式,将涉及内部加入三个表:用户,UserAppMembers为每个查询和应用程序在当前应用程序上下文,它不是简单的跟随即使处理简单的查询,更不用说复杂的以下部分所示。该值被传递给各种查询方法,然后在发送给后端数据服务之前,这些查询方法被传递给集合数据模型实例的相应属性。 隐藏,复制Code
UserSet _set = new UserSet();
_set.PageBlockSize = int.Parse(sobj["pageBlockSize"]);
_set.PageSize_ = int.Parse(sobj["pageSize"]);
if (sobj.ContainsKey("setFilter"))
_set.SetFilter = sobj["setFilter"];
... call remote query methods ...
这就是当前方法中处理数据子集所需的全部内容。 一个人可能会问,当他/她试图用一个表达式来表示上面的绿色表述时,他/她怎么知道上面的表达式是正确的呢?答案是:他/她不需要记住它的确切形式,查询智能用户界面将帮助建立他/她,只要他/她有一个词法模糊但语义清楚他/她想要的是什么,即使是在他/她自己的母语(见下面的查询定制部分)。当然,如果一个人不知道他/她想要什么,这对他/她一点帮助都没有。也就是说,它根本没有那么糟糕。该系统的交互性可以帮助用户在接触一段时间后,越来越清楚地了解自己想要什么,就像一个人在加速学习时所做的那样。如果一个人不知道如何学习呢?那么这个系统不能为他/她做,很抱歉。 4.2.2获取集合信息^ 在加载客户端页面之后立即调用此方法。其中只有两个结构返回,即当前web应用程序的用户计数和页面的初始排序选项开始: 隐藏,复制Code
case EntitySetType.User:
{
string filter = null;
if (sobj.ContainsKey("setFilter"))
filter = sobj["setFilter"];
UserServiceProxy svc = new UserServiceProxy();
var si = await svc.GetSetInfoAsync(ApplicationContext.ClientContext,
filter);
JavaScriptSerializer ser = new JavaScriptSerializer();
string json = ser.Serialize(new {
EntityCount = si.EntityCount,
Sorters = si.Sorters
});
return json;
}
这里,调用返回的类型为UserSet的。net对象si使用单向JavaScriptSerializer序列化器转换为字符串,因为只需要返回si的一些属性,以JSON格式。对于本文感兴趣的成员查询页面,即SearchMembers。cshtml页面(在web应用程序的Views\Query子目录下),它是通过方法调用链调用的,从jQuery页面加载处理程序开始 隐藏,复制Code
<script type="text/javascript">
appRoot = \'@Url.Content("~/")\';
serviceUrl = appRoot + \'Services/DataService/DataServiceProxy.svc\';
dataSourceId = \'\';
setName = \'User\';
appName = \'@ViewBag.AppName\';
$(function () {
window.onerror = function () {
window.status = \'...\';
return true;
}
userSet = new UserSet(serviceUrl);
userSet.SetFilter =
\'UserAppMember.Application_Ref.Name == "\' + appName + \'" && \'
+ \'( UserAppMember.SearchListing is null || UserAppMember.SearchListing == true )\';
userSet.GetSetInfo();
ko.applyBindings(userSet);
initsortinput(userSet);
initfilterinput(userSet);
$(\'.ui-autocomplete\').addClass(\'AutoCompleteMenu\');
});
</script>
全局变量userSet的类型userSet是在web应用程序的Scripts\DataService子目录下的MemberSearchPage.js中定义的。页面加载的事件处理程序设置子集,然后在实例化之后调用UserSet的GetSetInfo方法 隐藏,收缩,复制Code
function UserSet(dataServiceUrl) {
var self = this;
self.BaseUrl = dataServiceUrl;
... other stuff ...
self.GetSetInfo = function () {
$.ajax({
url: self.BaseUrl + "/GetSetInfo",
type: "POST",
dataType: "json",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
sourceId: dataSourceId,
set: JSON.stringify({
set: setName,
setFilter: self.SetFilter
})
}),
beforeSend: function () {
},
success: function (content) {
var r = JSON.parse(content.GetSetInfoResult);
self.TotalEntities(r.EntityCount);
self.CurrentSorters(new TokenOptions());
for (var i = 0; i < r.Sorters.length; i++) {
var tk = r.Sorters[i];
if (tokenNameMap) {
if (tokenNameMap(tk, setName, false)) {
self.CurrentSorters().Options.push(tk);
}
} else {
self.CurrentSorters().Options.push(tk);
}
}
self.CurrentSorters().CanBeClosed = true;
self.CurrentSorters().isLocal = false;
},
error: function (jqxhr, textStatus) {
alert(jqxhr.responseText);
},
complete: function () {
}
});
};
}
的GetSetInfo方法在AJAX调用中,由本小节中显示的第一个代码块处理。web应用程序的用户总数是在UserSet的KnockoutJS observable属性TotalEntities中设置的,它将绑定到一个要显示的html元素。排序器选项的初始列表被压入CurrentSorters()中。选项KnockoutJS可观察数组,绑定到排序输入自动完成框,如下所述。 4.2.3排序和过滤令牌选项^ sorters参数被反序列化到。net列表中。对象使用DataContractJsonSerializer类型,并发送到远程服务以获取下一个可用选项: 隐藏,复制Code
public async Task<string> GetNextSorterOps(string sourceId, string set,
string sorters)
{
switch (...)
...
case EntitySetType.User:
{
var ser1 = new DataContractJsonSerializer(typeof(List<QToken>));
var ser2 = new DataContractJsonSerializer(typeof(TokenOptions));
System.IO.MemoryStream strm = new System.IO.MemoryStream();
byte[] sbf = System.Text.Encoding.UTF8.GetBytes(sorters);
strm.Write(sbf, 0, sbf.Length);
strm.Position = 0;
var _sorters = ser1.ReadObject(strm) as List<QToken>;
UserServiceProxy svc = new UserServiceProxy();
var result = await svc.GetNextSorterOpsAsync(
ApplicationContext.ClientContext,
_sorters
);
strm = new System.IO.MemoryStream();
ser2.WriteObject(strm, result);
string json = System.Text.Encoding.UTF8.GetString(strm.ToArray());
return json;
}
...
}
第二个相同类型的序列化器用于将结果序列化为返回给客户机的JSON字符串。这是由jQuery UI“source”自动完成选项处理程序调用的: 隐藏,收缩,复制Code
function (request, response) {
if (!s.CurrentSorters() || !s.CurrentSorters().Options)
return;
var opts = s.CurrentSorters().Options;
var arr = opts.filter(function (val) {
return val.DisplayAs.toLowerCase().indexOf(
request.term.toLowerCase()) == 0;
}).sort(tokenSortCmp);
if (arr.length != 1 || deleting) {
// having more than one options or is deleting? show the available list
response($.map(arr, function (item) {
return { label: item.DisplayAs == "this" ?
item.TkName : item.DisplayAs,
value: item.DisplayAs };
}));
} else {
// found a unique match, push the current token, clear the input and
// call the service for the next available options.
iobj.autocomplete("close");
var tk = arr[0];
s.SorterPath.push(tk);
iobj.val("");
...
s.GetNextSorterOps(function (ok) {
if (ok) {
iobj.removeAttr("disabled");
...
}
iobj.focus();
iobj.css("cursor", "");
});
return false;
}
}
它包含在MemberSearchPage.js内的initsortinput全局函数中。这里iobj是与web页面内的输入元素对应的jQuery对象,s是(JavaScript) UserSet的一个全局实例,它的GetNextSorterOps方法是 隐藏,收缩,复制Code
self.GetNextSorterOps = function (callback) {
var qtokens = [];
for (var i = 0; i < self.SorterPath().length; i++)
qtokens.push(self.SorterPath()[i]);
$.ajax({
url: self.BaseUrl + "/GetNextSorterOps",
type: "POST",
dataType: "json",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ sourceId: dataSourceId,
set: setName,
sorters: JSON.stringify(qtokens)
}),
success: function (content) {
// new options arrived, push the current options into stack
self.SortersStack.push(self.CurrentSorters());
self.CurrentSorters(new TokenOptions());
// recover the JSON object
var r = JSON.parse(content.GetNextSorterOpsResult);
self.CurrentSorters().Hint = r.Hint;
self.CurrentSorters().CurrentExpr(r.CurrentExpr);
self.CurrentSorters().QuoteVal = r.QuoteVal;
self.CurrentSorters().CanBeClosed = r.CanBeClosed;
self.CurrentSorters().IsLocal = false;
for (var i = 0; i < r.Options.length; i++) {
var tk = new QToken();
tk.CopyToken(r.Options[i]);
if (tokenNameMap) {
// when token customization exists, customize it
if (tokenNameMap(tk, setName, false)) {
self.CurrentSorters().Options.push(tk);
}
} else {
self.CurrentSorters().Options.push(tk);
}
}
callback(true);
},
error: function (jqxhr, textStatus) {
alert(jqxhr.responseText);
callback(false);
}
});
};
过滤器选项的情况类似,它们的实现都包含在相同的相应文件中。这里不再重复。 4.2.4获取页面块^ 在SQ+KS系统中,分页是一次从数据源加载有限数量的实体的过程,这样可以节省计算资源。如果没有底层数据库的帮助(如Microsoft SQL数据库中的row_number()函数),在不首先加载所有行的任意排序条件下进行真正的分页将很难处理。然而,对于像当前这样的数据库不可知或无数据库的解决方案,它不能假设存在任何这样的非标准特性。因此,数据服务内部的分页必须依赖于来自页面边界信息的复杂查询条件,这些边界信息由包含在页面框架对象中的页面的第一个和最后一个实体组成。 系统通过两个步骤检索实体列表:首先,它在特定的排序条件下获取一个页面框架列表,然后它根据需要一次加载一个页面(比如当页面要显示时)属于页面的实体。 隐藏,收缩,复制Code
public async Task<string> NextPageBlock(string sourceId,
string set,
string qexpr,
string prevlast)
{
switch (type)
...
case EntitySetType.User:
{
var ser1 = new DataContractJsonSerializer(typeof(QueryExpresion));
var ser2 = new DataContractJsonSerializer(typeof(User));
var ser3 = new DataContractJsonSerializer(typeof(UserPageBlock));
var strm = new System.IO.MemoryStream();
byte[] sbf = System.Text.Encoding.UTF8.GetBytes(qexpr);
strm.Write(sbf, 0, sbf.Length);
strm.Position = 0;
var _qexpr = ser1.ReadObject(strm) as QueryExpresion;
UserServiceProxy svc = new UserServiceProxy();
UserSet _set = new UserSet();
_set.PageBlockSize = int.Parse(sobj["pageBlockSize"]);
_set.PageSize_ = int.Parse(sobj["pageSize"]);
if (sobj.ContainsKey("setFilter"))
_set.SetFilter = sobj["setFilter"];
User _prevlast = null;
if (!string.IsNullOrEmpty(prevlast))
{
strm = new System.IO.MemoryStream();
sbf = System.Text.Encoding.UTF8.GetBytes(prevlast);
strm.Write(sbf, 0, sbf.Length);
strm.Position = 0;
_prevlast = ser2.ReadObject(strm) as User;
}
var result = await svc.NextPageBlockAsync(
ApplicationContext.ClientContext,
_set,
_qexpr,
_prevlast);
strm = new System.IO.MemoryStream();
ser3.WriteObject(strm, result);
string json = System.Text.Encoding.UTF8.GetString(strm.ToArray());
return json;
}
...
}
对于给定的查询条件,它缓存已经加载到视图模型用户集的PageBlocks数组中的页面框架。如果在缓存中没有找到页帧块,它会对web应用程序中托管的WCF服务进行AJAX调用,以加载页帧块并将其保存在本地缓存中。显示的页面列表存储在UserSet视图模型的PageWindow可观察数组中,并绑定到视图。 这是在客户端在一个方法调用链中调用的,从MemberSearchPage.js内部的两个全局方法showlist和nextPageBlock开始。第一个是由用户在完成查询表达式的构造后单击“Start querying”按钮触发的,第二个是在单击下一页块按钮和未加载块时调用的。这些方法将在下一小节中更详细地描述。 然后这两个方法调用全局userSet变量的NextPageBlock方法 隐藏,收缩,复制Code
self.NextPageBlock = function (qexpr, last, callback) {
if (self.IsQueryStateChanged())
self.ResetPageState();
if (self.CurrBlockIndex() < self.PageBlocks().length) {
callback(true, false);
return;
}
$.ajax({
url: self.BaseUrl + "/NextPageBlock",
type: "POST",
dataType: "json",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
sourceId: dataSourceId,
set: JSON.stringify({
set: setName,
pageBlockSize: self.PageBlockSize(),
pageSize: self.PageSize_(),
setFilter: self.SetFilter
}),
qexpr: JSON.stringify(qexpr),
prevlast: last == null ?
null : JSON.stringify(last)
}),
success: function (content) {
var data = JSON.parse(content.NextPageBlockResult);
self.EntityCount(data.TotalEntities);
self.PageCount(data.TotalPages);
if (data.Pages.length == 0) {
var lpb = self.LastPageBlock();
if (lpb != null) {
lpb.IsLastBlock(true);
var lp = lpb.LastPage();
if (lp != null) {
lp.IsLastPage(true);
self.CurrBlockIndex(self.CurrBlockIndex() - 1);
}
} else {
self.PagesWindow.removeAll();
}
}
else {
var idx0 = 0;
for (var i = 0; i < self.CurrBlockIndex() ; i++) {
idx0 += self.PageBlocks()[i].BlockCount;
}
var pb = new UserPageBlock(idx0, data);
pb.BlockIndex = self.PageBlocks().length;
self.PageBlocks.push(pb);
self.PagesWindow.removeAll();
for (var i = 0; i < pb.Pages().length; i++) {
self.PagesWindow.push(pb.Pages()[i]);
}
}
self.IsQueryStateChanged(false);
callback(true, true);
},
error: function (jqxhr, textStatus) {
...
}
});
};
它调用上面提到的DataServiceProxy类的方法。 4.2.5加载页面^ GetPageItems方法是在MembershipPlusAppLayer45项目内部的DataServiceProxy类中实现的。然而,它很可能不会在web应用程序中使用。原因是上述泛型方法只返回用户类型的完整详细信息列表,而不返回任何其他用户相关信息。 在应用程序中,根据视图的用途,最好加载投影列表用户实体,只返回有关某些相关实体的选定属性子集和信息。 当在视图中单击以前未选择的页码时,它调用全局loadpage方法 隐藏,收缩,复制Code
function loadpage(index) {
if (loadingPage) {
return;
}
loadingPage = true;
var p = null;
var p0 = null;
var blk = userSet.PageBlocks()[userSet.CurrBlockIndex()];
for (var i = 0; i < blk.Pages().length; i++) {
var _p = blk.Pages()[i];
if (_p.Index_() == index) {
p = _p;
} else if (_p.IsPageSelected()) {
p0 = _p;
}
}
setWait(true)
if (p != null) {
if (!p.IsDataLoaded()) {
p.GetPageItems(userSet, function (ok) {
if (ok) {
updateCurrPage(p, p0);
}
loadingPage = false;
setWait(false)
});
} else {
updateCurrPage(p, p0);
loadingPage = false;
setWait(false)
}
} else {
loadingPage = false;
setWait(false)
}
}
在MemberSearchPage.js文件中,它调用UserPage视图模型的GetPageItems方法,如果页面中的项目尚未加载,即 隐藏,收缩,复制Code
function UserPage() {
var self = this;
...
self.GetPageItems = function (s, callback) {
if (self.IsDataLoaded())
return;
var qexpr = getQueryExpr();
var lastItem = null;
var ipage = self.Index_();
if (self.Index_() > 0) {
var blk = s.PageBlocks()[s.CurrBlockIndex()];
if (blk.Pages()[0].Index_() != ipage) {
for (var i = 0; i < blk.Pages().length; i++) {
if (blk.Pages()[i].Index_() == ipage - 1) {
lastItem = blk.Pages()[i].LastItem();
break;
}
}
} else {
var prvb = s.PageBlocks()[s.CurrBlockIndex() - 1];
lastItem = prvb.Pages()[prvb.Pages().length - 1].LastItem();
}
}
$.ajax({
url: appRoot + "Query/GetMembers",
type: "POST",
dataType: "json",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
set: JSON.stringify({
set: setName,
pageBlockSize: s.PageBlockSize(),
pageSize: s.PageSize_(),
setFilter: s.SetFilter,
appName: appName }),
qexpr: JSON.stringify(qexpr),
prevlast: lastItem == null ?
null : JSON.stringify(lastItem)
}),
beforeSend: function () {
self.Items.removeAll();
},
success: function (content) {
var items = JSON.parse(content);
for (var i = 0; i < items.length; i++)
self.Items.push(new User(items[i]));
self.IsDataLoaded(true);
callback(true);
},
error: function (jqxhr, textStatus) {
alert(jqxhr.responseText);
callback(false);
},
complete: function () {
}
});
}
}
它不是调用DataServiceProxy类中的对应方法,而是调用url appRoot +“Query/GetMembers”,这是由QueryController类的GetMembers方法处理的(在文件Controllers\QueryController.cs中)。它将任务委托给在MembershipPlusAppLayer45项目的MemberViewContext.cs文件中定义的MemberViewContext类的GetMembers方法 隐藏,收缩,复制Code
public static async Task<string> GetMembers(string set, string qexpr,
string prevlast)
{
var jser = new JavaScriptSerializer();
dynamic sobj = jser.DeserializeObject(set) as dynamic;
var ser1 = new DataContractJsonSerializer(typeof(QueryExpresion));
var ser2 = new DataContractJsonSerializer(typeof(User));
var ser3 = new JavaScriptSerializer();
System.IO.MemoryStream strm = new System.IO.MemoryStream();
byte[] sbf = System.Text.Encoding.UTF8.GetBytes(qexpr);
strm.Write(sbf, 0, sbf.Length);
strm.Position = 0;
var _qexpr = ser1.ReadObject(strm) as QueryExpresion;
UserServiceProxy svc = new UserServiceProxy();
UserSet _set = new UserSet();
_set.PageBlockSize = int.Parse(sobj["pageBlockSize"]);
_set.PageSize_ = int.Parse(sobj["pageSize"]);
if (sobj.ContainsKey("setFilter"))
_set.SetFilter = sobj["setFilter"];
User _prevlast = null;
if (!string.IsNullOrEmpty(prevlast))
{
strm = new System.IO.MemoryStream();
sbf = System.Text.Encoding.UTF8.GetBytes(prevlast);
strm.Write(sbf, 0, sbf.Length);
strm.Position = 0;
_prevlast = ser2.ReadObject(strm) as User;
}
var result = await svc.GetPageItemsAsync(Cntx, _set, _qexpr,
_prevlast);
var ar = new List<dynamic>();
string appId = ApplicationContext.App.ID;
UsersInRoleServiceProxy uirsvc = new UsersInRoleServiceProxy();
foreach (var e in result)
{
var membs = svc.MaterializeAllUserAppMembers(Cntx, e);
var memb = (from d in membs where
d.ApplicationID == appId select d
).SingleOrDefault();
ar.Add(new
{
data = e, // there is no property projection made,
// but it can be added
member = memb, // same as above
hasIcon = memb != null &&
!string.IsNullOrEmpty(memb.IconMime)
}
);
}
string json = ser3.Serialize(ar);
return json;
}
这里不仅检索用户实体,还检索并返回与当前web应用程序相对应的每个用户的会员记录: 隐藏,复制Code
foreach (var e in result)
{
var membs = svc.MaterializeAllUserAppMembers(Cntx, e);
var memb = (from d in membs where
d.ApplicationID == appId select d
).SingleOrDefault();
...
}
如果数据服务中只有几个应用程序,那么上面的代码是可以接受的。否则,它的效率不是很高,因为它将用户拥有的所有成员记录加载到web应用程序并在本地过滤掉,这会浪费带宽和本地内存。另一种方法是使用约束查询(参见这里) 隐藏,复制Code
foreach (var e in result)
{
UserAppMemberServiceProxy mbsvc = new UserAppMemberServiceProxy();
var cond = new UserAppMemberSetConstraints
{
ApplicationIDWrap = new ForeignKeyData<string> { KeyValue = appId },
UserIDWrap = new ForeignKeyData<string> { KeyValue = e.ID }
};
var memb = (await mbsvc.ConstraintQueryAsync(Cntx,
new UserAppMemberSet(),
cond,
null)).SingleOrDefault();
...
}
</string></string>
它只为每个用户加载一个成员记录(如果有的话)。 4.2.6加载一个user ^ 当页面中的一个用户被选中时,将调用全局JavaScript方法selectUser,该方法在经过一些处理后,将调用全局updateEntityDetails方法 隐藏,复制Code
function updateEntityDetails(user) {
if (user.more() == null) {
user.LoadDetails(function(ok){
if (ok) {
... scroll to the user details
}
})
} else {
... scroll to the user details
}
}
当用户细节(more())未初始化时,用户(KnockoutJS)视图模型的LoadDetails方法是什么 隐藏,复制Code
function (callback) {
if (self.more() != null) {
callback(true);
}
$.ajax({
url: appRoot +
"Query/MemberDetailsJson?id=" +
self.data.ID,
type: "GET",
success: function (content) {
if (content.hasDetails &&
content.details.hasPhoto) {
content.details.photoUrl = appRoot +
\'Account/UserPhoto?id=\' +
self.data.ID;
}
self.more(content);
callback(true);
},
error: function (jqxhr, textStatus) {
alert(jqxhr.responseText);
callback(false);
}
});
}
将被调用。AJAX调用由QueryController类的MemberDetailsJson方法处理。该方法将任务委托给MemberViewContext类的GetBriefMemberDetails方法,该方法将在对服务的一次调用中加载与用户相关的选中记录集合,即通过调用UserServiceProxy类的LoadEntityGraphRecurs方法。 以下内容部分摘录自该文档的备注部分,并补充了本文的其他信息 此方法设计用于在对从给定实体(id)开始的服务的一次调用中从数据源递归地加载选定的子实体图。它可以用于提高性能和降低客户端代码的复杂性,有时是显著的。 选择由两个参数控制,即excludedSets和futherDrillSets。 excludedSets参数用于排除一列实体集和依赖于它的所有其他集。这可以更好的理解如果一看数据集模式的示意图所示,即如果一个日期设置(节点)排除它指向的所有设置将无法联系到,尽管他们中的一些人仍然可以达到其他路线。 实体子图的加载方式有很多种,本文的实现基于下面给出的规则。也就是说,它从entry元素开始,递归地向下加载依赖于它的所有实体(即沿着schema视图中的箭头)。它还加载向下递归访问的任何元素所依赖的所有元素,递归向上(即在模式视图中沿箭头的相反方向),但如果没有显式指令,就不会再向下。 futherDrillSets参数用于控制何时再次向下,由SetType成员表示,依赖于它的数据集的集合,由RelatedSets成员表示,应该进一步递归地向下钻取。 请注意,数据服务有内在限制,不允许在一次调用中传输太大的实体图,因此必须选择在每次对数据服务的调用中加载整个图的哪一部分, 对于给定的实体,它所依赖的实体由与每个外键对应的成员对象表示。然而,实体的设置取决于说实体存储到相应的集合成员的“改变”前缀(见第二最后实体图形细节部分),这些实体没有参考的实体进行序列化时,为了避免循环引用这样回可以添加引用图后物化在客户端,如果必要的。 图:数据模式示意图。 根据数据模式,Announcement、Communication、EventCalendar、MemberNotification、UserAppMember、UserAssociation、UserAssocInvitation、UserDetail、UserGroupMember、UserProfile、UsersInRole和UsersRoleHistory数据集直接依赖于或与用户数据集关联,有些甚至是多次。当然,我们不希望在用户查询视图中加载它们。根据当前视图上下文,在构建数据图时,我们可以排除除Communication、UserAppMember和UserDetail数据集之外的所有数据集: 隐藏,收缩,复制Code
public static async Task<dynamic> GetBriefMemberDetails(string id)
{
UserServiceProxy usvc = new UserServiceProxy();
EntitySetType[] excludes = new EntitySetType[]
{
EntitySetType.Announcement,
EntitySetType.EventCalendar,
EntitySetType.MemberNotification,
EntitySetType.UserAssociation,
EntitySetType.UserAssocInvitation,
EntitySetType.UserGroupMember,
EntitySetType.UserProfile,
EntitySetType.UsersInRole,
EntitySetType.UsersRoleHistory
};
var cctx = Cntx;
var graph = await usvc.LoadEntityGraphRecursAsync(cctx, id,
excludes, null);
// select only those items that belongs to the current application
var member = (from d in graph.ChangedUserAppMembers
where d.ApplicationID == ApplicationContext.App.ID
select d).Single();
var Details = (from d in graph.ChangedUserDetails
where d.ApplicationID == ApplicationContext.App.ID
select d).FirstOrDefault();
var Communications = (from d in graph.ChangedCommunications
where d.ApplicationID == ApplicationContext.App.ID
select d).ToArray();
dynamic obj = null;
... build the dynamic object to be converted to json on return base
... on these values
return obj;
}
接下来,将以动态类型重新组装与用户相关的记录集合,以便在返回时将其转换为JSON对象。 4.3意见^ 本文中描述的视图用于提供通用用户搜索,它位于名为SearchMembers.cshtml的web应用程序的Views\查询子目录下。 4.3.1查询表达式编辑器^ 由于预期数据查询将在web应用程序的许多部分中使用,并且如上面所述,它独立于数据集和数据源,因此查询表达式编辑器放置在视图\S中作为一个名为_QueryComposerPartial.cshtml的部分视图共享web应用程序的子目录。任何需要它的视图都可以从那里包含它。 在设置了合适的KnockoutJS视图模型之后,表达式编辑器实际上非常简单。例如,筛选器表达式编辑器就是这种形式的 隐藏,复制Code
<!-- ko foreach: FilterPath --> <div data-bind="text: DisplayAs, css: TkClass"></div> <!-- /ko --> <input id="filterOpts" />
也就是说,它只是一个div绑定到UserSet视图模型的可观察数组过滤器路径的数组。每个& lt; div>绑定到QToken类型的对应对象,该对象具有由TkClass属性确定的css类和由DisplayAs属性确定的内容。TkClass的值将被设置为根据标记的类型引用适当的css类,这样我们就可以实现语法突出显示或更高级的样式化效果。 这& lt; div>级数后面跟着一个输入。绑定到jQuery UI自动完成控件的元素。用户使用它来进行令牌输入。这基本上是视图层面上的所有内容。 每次用户输入或删除一个令牌时,过滤器路径都会更新。因为它是一个KnockoutJS可观察数组,所以一旦数组发生变化,视图就会被更新。 编辑器(一个用于分类器,另一个用于筛选器)在页面加载的事件处理程序中初始化 图:查询表达式编辑器。打开可用选项菜单。 隐藏,复制Code
$(function () {
...
userSet = new UserSet(serviceUrl);
userSet.SetFilter = \'...\';
userSet.GetSetInfo();
ko.applyBindings(userSet);
initsortinput(userSet); // init sorter editor
initfilterinput(userSet); // init filter editor
...
});
两个全局方法,即CustomMembershipPlus.js文件中的initsortinput和initfilterinput初始化和设置jQuery UI自动完成输入控件和编辑状态相关更新。 例如,这里描述了排序器的“source”函数,而“select”函数是 隐藏,收缩,复制Code
function (event, ui) {
var tk = null;
var opts = s.CurrentSorters().Options;
for (var i = 0; i &tl; opts.length; i++) {
if (opts[i].DisplayAs == ui.item.value) {
tk = opts[i];
break;
}
}
if (tk != null) {
// push the selected token into the SorterPath that
// will automatically displayed in the view
s.SorterPath.push(tk);
iobj.val("");
iobj.attr("disabled", "disabled");
iobj.css("cursor", "progress");
if (tk == null || tk.TkName != "asc" &&
tk.TkName != "desc") {
// incomplete, disable the query button
// and hide the input box for the filter.
enableQuery(false);
$("#filterOpts").hide();
} else {
enableQuery(true);
$("#filterOpts").show();
s.IsQueryStateChanged(true);
}
// load the next options
s.GetNextSorterOps(function (ok) {
if (ok) {
... update state, visuals, etc.
}
iobj.focus();
iobj.css("cursor", "");
});
return false;
}
}
当查询表达式处于“关闭”状态时,“Start querying”按钮被启用。当单击该按钮时,该按钮将触发对MemberSearchPage.js文件内的全局方法showlist的调用,该方法将加载从第一个页面开始的页面框架的初始块。 隐藏,复制Code
function showlist(e) {
if (!queryCompleted)
return;
var qexpr = getQueryExpr();
if (userSet.IsQueryStateChanged()) {
userSet.ResetPageState();
}
userSet.NextPageBlock(qexpr, null, function (ok, ch) {
if (ch && userSet.CurrentPage() != null &&
!(typeof userSet.CurrentPage().Items === \'undefined\')) {
userSet.CurrentPage().Items.removeAll();
}
if (ok) {
userSet.IsQueryInitialized(true);
if (ch && userSet.PageBlocks().length > 0 &&
userSet.PageBlocks()[0].Pages().length > 0) {
loadpage(0);
}
}
});
}
4.3.2页面窗口^ 当前显示的页面框架被绑定到UserSet视图模型的可观察数组PagesWindow (KnockoutJS)。为了简单起见,只添加了两个按钮来更改显示页面框架的内容,如下所示 隐藏,收缩,复制Code
<ul class="pagination-sm">
<!-- ko if: PrevBlock() != null -->
<li>
<a href="javascript:prevPageBlock()"
title="Load previous page block ...">
<span class="glyphicon glyphicon-chevron-left"></span>
</a>
</li>
<!-- /ko -->
<!-- ko foreach: PagesWindow -->
<!-- ko if: IsPageSelected() -->
<li class="active">
<span class="selected" data-bind="text: PageNumber"></span>
</li>
<!-- /ko -->
<!-- ko ifnot: IsPageSelected() -->
<li>
<a data-bind="attr: {href: PageLink}">
<span data-bind="text: PageNumber"></span>
</a>
</li>
<!-- /ko -->
<!-- /ko -->
<!-- ko if: MoreNextBlock() -->
<li>
<a href="javascript:nextPageBlock()"
title="Load next page block ...">
<span class="glyphicon glyphicon-chevron-right"></span>
</a>
</li>
<!-- /ko -->
</ul>
图:页面、窗口和项目列表。左下角显示的是匹配项的数量。 一个按钮用于加载前面的页面块,只有在有前面的块要加载时才可见。它由全局方法处理 隐藏,复制Code
function prevPageBlock() {
if (loadingPage) {
return;
}
var idx = userSet.CurrBlockIndex();
if (idx > 0) {
userSet.CurrBlockIndex(idx - 1);
userSet.PagesWindow.removeAll();
var ipage = -1;
for (var i = 0;
i < userSet.PageBlocks()[idx - 1].Pages().length;
i++) {
var p = userSet.PageBlocks()[idx - 1].Pages()[i];
userSet.PagesWindow.push(p);
if (p.IsPageSelected()) {
ipage = p.Index_();
}
}
loadpage(ipage == -1 ? 0 : ipage);
}
}
另一个按钮用于加载页面的下一个块,也只有在有下一个块要加载时才可见。它由全局方法处理 隐藏,收缩,复制Code
function nextPageBlock() {
if (loadingPage) {
return;
}
var idx = userSet.CurrBlockIndex();
if (idx < userSet.PageBlocks().length - 1) {
userSet.CurrBlockIndex(idx + 1);
userSet.PagesWindow.removeAll();
var ipage = -1;
for (var i = 0;
i < userSet.PageBlocks()[idx + 1].Pages().length;
i++) {
var p = userSet.PageBlocks()[idx + 1].Pages()[i];
userSet.PagesWindow.push(p);
if (p.IsPageSelected()) {
ipage = p.Index_();
}
}
loadpage(ipage == -1 ? 0 : ipage);
} else {
idx = userSet.PageBlocks().length - 1;
var b = userSet.PageBlocks()[idx];
if (!b.IsLastBlock()) {
userSet.CurrBlockIndex(idx + 1);
var p = b.LastPage();
if (p == null) {
return;
}
var qexpr = getQueryExpr();
userSet.NextPageBlock(qexpr,
p.LastItem(),
function (ok, ch) {
if (ok) {
if (userSet.PageBlocks().length > 0 &&
userSet.PageBlocks()[idx + 1].Pages().length > 0) {
loadpage(userSet.PageBlocks()[idx + 1].Pages()[0].Index_());
}
}
});
}
}
}
它检查页面块是否已经加载。如果是,可用的块被推到全局userSet变量的PagesWindow中。否则,它将从web应用程序加载下一个页面块。 注意,假设PagesWindow的最大大小与从web应用程序下载的页面块的最大大小相同。事情不一定是这样的。只是,如果不这样做,上述两种方法将比当前的方法更加复杂。 今后可以增加按页码转到页面、转第一页块、转最后页块等更高级的页面块显示方式,如页面帧窗口的平滑移动等。 4.3.3用户列表^ 当单击当前未选中的页面时,它将调用全局方法 隐藏,收缩,复制Code
function loadpage(index) {
if (loadingPage) {
return;
}
loadingPage = true;
var p = null;
var p0 = null;
var blk = userSet.PageBlocks()[userSet.CurrBlockIndex()];
for (var i = 0; i < blk.Pages().length; i++) {
var _p = blk.Pages()[i];
if (_p.Index_() == index) {
p = _p;
} else if (_p.IsPageSelected()) {
p0 = _p;
}
}
setWait(true)
if (p != null) {
if (!p.IsDataLoaded()) {
p.GetPageItems(userSet, function (ok) {
if (ok) {
}
loadingPage = false;
setWait(false)
});
} else {
updateCurrPage(p, p0);
loadingPage = false;
setWait(false)
}
} else {
loadingPage = false;
setWait(false)
}
}
如上所述(见此处)。在获得页面项之后,它将全局userSet变量的CurrentPage (KnockoutJS)可观察性设置为所单击的页面框架。因为项目列表被绑定到CurrentPage()。userSet物品: 隐藏,收缩,复制Code
<table class="gridview table-hover table-striped table-bordered">
...
<tbody data-bind="foreach: CurrentPage().Items">
<tr data-bind="css: {selected: IsEntitySelected()},
click: function(data, event) {
selectUser(data, event);
}">
<td>
<!-- ko if: hasIcon -->
<img data-bind="attr: {src: iconUrl}" />
<!-- /ko -->
<!-- ko ifnot: hasIcon -->
<span class="ion-person"></span>
<!-- /ko -->
<span data-bind="text: data.Username"></span>
</td>
<td style="width:25px; white-space:nowrap;">
<a href="#" data-bind="click: function(data, event) {
ShowUser(data, event); }" title="...">
<span class="ion-navicon"></span>
</a>
</td>
<td>
<span data-bind="text: member.Email"></span>
</td>
<td>
<span data-bind="text: member.MemberStatus"></span>
</td>
<td>
<span data-bind="localdatetime: data.LastLoginDate"></span>
</td>
<td>
<span data-bind="localdatetime: member.LastActivityDate"></span>
</td>
</tr>
</tbody>
</table>
列表会自动更新。 4.3.4用户详细信息^ 当单击上面列表中的一行时,它调用selectUser全局方法 隐藏,复制Code
function selectUser(data, event) {
for (var i = 0; i <
userSet.CurrentPage().Items().length; i++) {
var e = userSet.CurrentPage().Items()[i];
if (e.IsEntitySelected() && e != data) {
e.IsEntitySelected(false);
}
}
userSet.CurrentSelectedUser(data);
data.IsEntitySelected(true);
userSet.CurrentPage().CurrentItem(data);
updateEntityDetails(data);
event.stopPropagation();
return false;
}
用户详细信息是在对updateEntityDetails全局方法的调用中加载的,该方法如上所述(参见此处)。 SearchMembers的最后一部分。cshtml页面用于显示所选成员的详细信息: 隐藏,复制Code
<div id="user-details" data-bind="with: CurrentPage">
<!-- ko if: typeof CurrentItem != \'undefined\' -->
<!-- ko if: CurrentItem() != null -->
<div class="user-details" data-bind="with: CurrentItem">
@Html.Partial("_MemberDetailsPartial")
</div>
<!-- /ko -->
<!-- /ko -->
</div>
包含的部分视图_MemberDetailsPartial。cshtml仅在全局变量userSet的CurrentPage可观察值不为空且该CurrentPage可观察值也不为空时可见。_MemberDetailsPartial部分视图。cshtml包含用户详细信息显示的详细布局,如下所示。 图:用户详细信息显示。 它将通过调用MemberViewContext类的GetBriefMemberDetails方法获得的JSON对象绑定到th中相应的html元素e _MemberDetailsPartial。cshtml文件。由于所涉及的代码相当长,所以不会在这里显示。感兴趣的用户可以转到引用的文件以获得详细信息。 4.4风格^ 正如在第一篇文章中所描述的,LESS system用于创建和维护最终的CSS样式文件。 如果没有适当的CSS样式,上面描述的视图中的视觉效果将不会是现在的样子。实际上,很大一部分时间用来创建一个功能强大的SearchMembers。cshtml页面用于交互地调整.less文件,以获得足够好的外观。 但是,本文不打算对其进行更详细的描述,因为本文将重点放在软件方面。样式部分可以自己写一篇文章。有兴趣的读者也可以亲自动手调整自己的风格。 4.5和词汇 数据服务提供的查询词汇表有一些缺点 它们是根据某些固定的规则(参见实体图第二部分最后一节)从数据模式中生成的,这些规则适用于所有数据服务,这些服务对于特定的应用程序来说可能过于通用。不能体现应用的特殊性:上下文、性质等,不能为用户提供友好的交互环境或体验。全球化可能由数据服务提供。数据系统的设计者通常使用特定的语言模式(如英语)来设计数据模式,从而生成查询词汇表。然而,至少系统的一些用户可能无法有效地使用该语言。尽管全球化可以在数据服务级别上完成,但出于上述原因,最好还是将任务委托给应用程序。 4.5.1查询令牌和表达式^ 查询表达式由一系列记号组成。这里的记号是一个结构,它有两组关键属性:1)它的值是什么;2)它是什么,就像它是如下所示。 隐藏,复制Code
function QToken(val) {
var self = this;
// -- its value ---
self.TkType = "";
self.TkName = val;
// -- what it\'s known as ---
self.DisplayAs = val;
self.TkClass = "filternode";
...
}
“值”部分由计算机使用,“称为”部分由人类用于理解表达式的意义。除了几例常见的所有数据服务(如{TkName = asc, DisplayAs =“提升”},{TkName = desc, DisplayAs =“降序”},等等,这也是全球化的},DisplayAs分配相同的值作为一个令牌时分配给TkName最初产生的数据服务。 这里所说的词汇是指人类与系统正确交互的词汇。这是表达式的一部分,可以在使用数据服务的应用程序内部定制。 如果检查自动完成表达式输入控件的“源”函数(见,例如,这里),可以发现以下可视化选项: 隐藏,复制Code
var opts = s.CurrentSorters().Options;
var arr = opts.filter(function (val) {
return val.DisplayAs.toLowerCase().indexOf(
request.term.toLowerCase()) == 0;
}).sort(tokenSortCmp);
if (arr.length != 1 || deleting) {
response($.map(arr, function (item) {
return {
label: item.DisplayAs == "this" ?
item.TkName : item.DisplayAs,
value: item.DisplayAs
};
}));
}
也就是说,用作用户输入选项的是DsiaplayAs的值,而不是TkName的值。因此,如果可以更改DsiaplayAs的值,则可以更改令牌的输入方式,同时不改变机器的令牌含义。 4.5.2定制^ 定制由两部分组成: 令牌过滤。可以从用户可用的令牌选项中筛选出一些令牌。令牌名称的改变。更改DsiaplayAs的值,以便更好地针对用户理解、使用等。 为此,可以在UserSet视图模型的GetNextSorterOps和GetNextFilterOps方法内部通过一个名为tokenNameMap(如果定义的话)的全局方法对令牌选项进行预处理。例如,在GetNextSorterOps中我们有 隐藏,复制Code
// after load the options from the service
for (var i = 0; i < r.Options.length; i++) {
var tk = new QToken();
tk.CopyToken(r.Options[i]);
if (tokenNameMap) {
// when token customization exists, customize it
if (tokenNameMap(tk, setName, false)) {
self.CurrentSorters().Options.push(tk);
}
} else {
self.CurrentSorters().Options.push(tk);
}
}
如果定义了tokenNameMap,那么只有那些被它接受的选项(即返回true值)才会添加到用户的选项中。tokenNameMap是在一个附加的JavaScript响应中定义的。 4.5.3基于方法的配置^ 优点:更结构化,更少技术性。它可以全球化。 缺点:不太灵活。当配置文件发生更改时,服务器将重新启动,因此它不能动态添加规则。 4.5.3.1 JavaScript ^ 包含的定制JavaScript是在服务器端根据Web自定义部分中指定的信息动态生成的。配置文件,它将在下面的小节中详细描述。在应用程序级查询页面上以以下方式引用JavaScript 隐藏,复制Code
<script src="@Url.Content("~/JavaScript/QueryCustomization?src=")"></script>
应用程序级定制通常比管理级定制包含更多的限制性规则,管理级定制通过不同的url包含: 隐藏,复制Code
<script src="@Url.Content("~/JavaScript/QueryAdminCustomization?src=")"></script>
这里的空src参数src=表示Web中定义的默认数据源名称。应该使用配置文件。例如,对于当前的web应用程序 隐藏,复制Code
<appSettings> ... ... <add key="DefaultDataSource" value="MembershipPlus" /> ... </appSettings>
如图所示,JavaScript生成器是由QueryCustomization方法或QueryAdminCustom处理的JavaScriptController控制器类的化方法 隐藏,收缩,复制Code
public class JavaScriptController : BaseController
{
private static QueryCustomization QueryTokenMap = null;
public JavaScriptController()
{
if (QueryTokenMap == null)
{
QueryTokenMap = ConfigurationManager.GetSection(
"query/customization")
as QueryCustomization;
}
}
[HttpGet]
public ActionResult QueryAdminCustomization(string src)
{
if (QueryTokenMap == null || !QueryTokenMap.ConfigExists)
return new HttpStatusCodeResult(404, "Not Found");
StringBuilder sb = new StringBuilder();
if (string.IsNullOrEmpty(src))
src = ConfigurationManager.AppSettings["DefaultDataSource"];
_queryCustomization(sb, src, QueryTokenMap.GetAdminFilters);
return ReturnJavascript(sb.ToString());
}
[HttpGet]
public ActionResult QueryCustomization(string src)
{
if (QueryTokenMap == null || !QueryTokenMap.ConfigExists)
return new HttpStatusCodeResult(404, "Not Found");
StringBuilder sb = new StringBuilder();
if (string.IsNullOrEmpty(src))
src = ConfigurationManager.AppSettings["DefaultDataSource"];
_queryCustomization(sb, src, QueryTokenMap.GetAppFilters);
return ReturnJavascript(sb.ToString());
}
private void _queryCustomization(StringBuilder sb, string src,
<Funcstring, string, SetFilters> getfilters)
{
...
}
...
}
这里不打算详细描述。感兴趣的读者可以在读完下一小节之后直接进入源文件,以了解它是如何完成的。 web应用程序的Scripts/DataService子目录下的JavaScript文件CustomMembershipPlus.js包含根据当前web生成的内容的副本。配置文件。 4.5.3.2配置^ 在基于配置的方法中,定制可以在Web的定制部分中完成。配置文件: 隐藏,复制Code
<configSections>
<sectionGroup name="query">
<section name="customization"
type="...Configuration.QueryCustomizationHandler,
MembershipPlusAppLayer" />
</sectionGroup>
</configSections>
即定制信息存储在Web的查询/定制节点下。配置文件,并由QueryCustomizationHandler类处理。所有相关的类型目前都定义在一个文件中,即MembershipPlusAppLayer45项目的配置子目录下的QueryCustomizationCfg.cs文件。QueryCustomizationHandler类将定制部分的解析委托给同一个文件中定义的QueryCustomization类,该类构建用于生成定制JavaScript的数据结构。 下面是一个定制示例: 隐藏,收缩,复制Code
<query>
<customization>
<global>
<maps>
<map from="&&" to="and" />
<map from="||" to="or" />
<map from="asc" to="asc" />
<map from="desc" to="desc" />
</maps>
</global>
<datasource name="MembershipPlus">
<set name="User">
<filters type="admin" allow-implied="false">
<filter target="sorting"
expr="{0}.indexOf(\'Password\') == -1" />
<filter target="filtering"
expr="{0}.indexOf(\'Password\') == -1 ||
{0}.indexOf(\'Password\') != -1 &&
{0}.indexOf(\'Failed\') != -1" />
</filters>
<filters type="app">
<filter target="sorting" expr="*password*"
case-sensitive="false" />
<filter target="filtering" expr="*password*"
case-sensitive="false" />
</filters>
<maps>
<map from="UserAppMember." to="Membership." />
<map from="UserDetail." to="Detail." />
<map from="TextContent" to="keywords" />
<map from="AddressInfo" to="Address" />
<map from="Username" to="Name"
to-resId="92f1b1481fa6ff46c4a3caae78354dac"
globalize="false" />
</maps>
</set>
</datasource>
</customization>
</query>
可选& lt; global>节点包含所有数据源和数据集的通用mapps。它后面是一系列数据源。节点,其名称为数据源的名称,在本例中为“MembershipPlus”。每个& lt; datasource>包含一个集合的集合子节点,其名称为集合的实体类名。包含& lt; maps>持有的子节点节点和& lt; filters>包含过滤器的子节点子节点。 & lt; map>节点具有以下属性: 从字符串名称类型可选默认描述没有,TkName值。它是对应的实体集合的一个属性的名称。串no 映射的DisplayAs值。to- resid guid是十六进制编码形式的16字节Guid的全球化资源为“to”。如果“to- resid”被初始化为一个有效值,并且这个属性被设置为“true”,那么映射的显示将尝试使用全球化资源而不是“to”值。 & lt; filters>节点具有以下属性: 名称类型可选默认描述enum类型没有,它确定web应用程序的过滤器集的类型。当前允许的值是“admin”和“app”。类型为“admin”的过滤器适用于管理页面,而类型为“app”的过滤器适用于一般更严格的页面。随着应用程序变得更加复杂,这个列表肯定会扩展。它定义了为这种类型的过滤器解释缺失或未指定的过滤器时的默认行为。如果为真,则允许所有不匹配的令牌,否则不允许所有不匹配的令牌。 & lt; filter>节点具有以下属性: 名称类型可选默认描述目标enum yes,它决定过滤器应用查询表达式的哪一部分。允许的值是:“排序”,“过滤”和“全部”。如果不指定,则假定值“all”。允许的布尔值,允许-暗示它决定如何处理匹配标记。如果为真,则允许匹配的令牌,否则不允许。如果未指定,则父节点的“allow-implied”属性的值使用。大小写敏感布尔值,是假,无论过滤器匹配是否区分大小写。默认值为false。expr字符串没有其值的模式决定了它使用什么类型的匹配方法: 如果有模式"*"+<str>+"*":过滤器匹配,如果令牌名称TkName包含<str>例如“*abc*”->包含“abc”。如果标记名称TkName以<str>开头,则过滤器匹配。如果标记名称TkName以<str>结尾,则过滤器匹配。如果有模式"["+<str>+"]":如果标记名称TkName匹配(JavaScript)正则表达式,过滤器匹配。例如,如果expr="[/BobIsKool/g]",那么如果TkName包含"BobIsKool",则过滤器匹配。如果它匹配。net正则表达式模式"\{\d+\}",那么filter matches是JavaScript表达式模板,其中字符串格式的holder{0},{1}等被JavaScript局部变量或参数替换。否则,滤波器maTkName的值是否与TkName相同。 4.5.3.3上下文依赖^ 与数据服务支持的查询智能系统不同,当前的定制系统是一个简单的系统,它依赖于视图上下文。因此,对于数据集的给定视图,令牌映射和筛选应用于与该集合相关的任何其他数据集的属性。 例如,假设当在令牌细节后面遇到描述令牌时,正在查询用户数据集。,将使用用户集的规则,而不是UserDetail集的规则。这里的细节。指UserDetail数据集中与当前用户关联的用户详细信息记录集(通过外键)。注意,更简洁的显示名称细节。因为令牌是从原始值UserDetail映射的。因为在Web.config的定制部分有一个映射: 隐藏,复制Code
<map from="UserDetail." to="Detail." />
为用户集。也就是说,如果一个添加 隐藏,复制Code
<map from="Description" to="Descr" />
上面的兄弟节点,然后是细节。描述不再是正确的输入。而不是细节。Descr是正确的。但是,现在转到用于查询UserDetail集的页面(假设已经实现)时,新添加的规则将在那里不起作用,因为它只映射在用户查询上下文下。 4.5.4基于代码的方法^ 另一种方法是手动编写映射脚本文件。web应用程序的Scripts\DataService子目录下的CustomMembershipPlus.js文件就是一个示例,它等价于根据上述配置生成的文件。 默认方法是基于上述方法的配置。要使用当前方法,请使用SearchMembers。应该修改Views\查询子目录下的cshtml页面文件。这条线 隐藏,复制Code
<script src="@Url.Content("~/JavaScript/QueryCustomization?src=")"></script>
应改为 隐藏,复制Code
<script src="@Url.Content("~/Scripts/DataService/CustomMembershipPlus.js")"></script>
优点:灵活。 缺点:需要JavaScript编程知识。它不能全球化。 5. 准备数据服务^ 本文的数据服务扩展到支持全文索引和对几个文本属性的搜索: UserDetail实体的描述属性。此属性通常包含最好使用KS方法搜索的文本块。通信实体的AddressInfo和Comment属性。它们还可能包含最好使用KS方法搜索的文本块。 这些可全文搜索属性的添加并没有涵盖所有的可能性。例如,公告实体的标题和描述属性也适合用于执行全文索引和搜索等。只包含现在需要的内容的原因是为了简单,当我们稍后将要讨论它时,增量地添加新的内容并不困难。 示例数据包含1478个随机选择的用户可以查询的成员。 5.1全文索引 5.1.1本地KS ^ 有一种用于搜索本地全文索引的通用语法。对于任何文本属性,都有一个称为native-matches的操作符,可用于指示后端数据库引擎在目标属性上进行全文搜索,但只有在为上述属性设置全文索引和搜索时,该操作符才有效。例如,当公告实体的Title属性已经建立了全文索引时,下面的表达式将对它们进行全文搜索,即 隐藏,复制Code
Title native-matches "keyword"
其中“关键字”是搜索的关键字。 5.1.2统一的全文索引和KS ^ 全文搜索不是SQL标准。对于与数据库无关的数据服务,我们不能假设 实际上有一个关系数据库引擎后端。后端支持全文本索引和搜索。后端全文搜索具有通用的搜索语法(即用于涉及多个关键字的更高级的搜索)。后端全文索引可以从一个后端传输到另一个后端,也就是说它们可以兼容。等。 数据服务全文统一索引和检索系统就是为了解决这些问题而设计的。 对特定数据集进行统一全文搜索的一般语法是 隐藏,复制Code
TextContent matches pattern { <query-expr> } <paging opts>
在& lt; query-expr>是关键字搜索表达式,使用查询智能系统提供可用选项的分页选项来选择匹配的数据。当前系统使用Lucene。Net实现文本索引,因此语法查询-expr>是Lucene查询语法(详细信息请参见这里)。该系统将指导用户制定简单的查询规则。将来会变得更好的。对于不适合现有模式的更复杂的表达式,可以在表达式的开始处加上$的前缀,之后用户可以输入a纽约自由表达。注意,统一全文搜索选项(即TextContent expression关键字)仅对那些在生成系统时声明了该选项的数据集可用。 注意,因为我们有一个规则 隐藏,复制Code
<map from="TextContent" to="keywords" />
在Web中用户数据集的查询自定义部分中。配置文件,任何表达式开始一个统一的全文搜索开始关键字匹配…而不是TextContent匹配…在用户查询上下文中。例如,当试图从与用户关联的所有通信记录中搜索AddressInfo属性的全文索引时,应该使用以下表达式,即 隐藏,复制Code
Communication.keywords matches pattern { AddressInfo "unicons" } first 333
而不是 隐藏,复制Code
Communication.TextContent matches pattern { AddressInfo "unicons" } first 333
由于上面描述的视图上下文依赖关系,如果没有如上所述的显式映射规则添加到集合的相应定制节点,则此规则不适用于其他查询上下文。 5.1.3索引工具^ 该数据服务附带一个名为ServiceDataSync.exe的索引、数据源间同步程序。可以用来构建统一的全文索引。它包含在上面的下载中。 要建立索引,请在程序启动后转到“主操作/文本索引页”。首先应该选择一个数据服务基url,然后设置输出目录,例如,如下所示。 图:索引构建过程的进展快照。 提供用户感兴趣的数据服务(会员+系统)基url,然后将输出目录设置为同一个数据服务网站的App_Data\MembershipPlus\Indices子目录。这个目录是数据服务查找全文搜索索引的地方。 完成以上步骤后,按下start按钮,等待过程完成,如上所示。 注意:演示数据包中包含的样例索引是从App_Data\MembershipPlus\数据子目录中的样例数据构建的,请不要将其用于任何其他数据集。 6. 数据图,查询^ 数据服务支持对相应关系数据源的操作。与其他形式的有向图数据结构相比,关系数据最好使用有向图数据结构建模,比如在面向对象(OO)世界中以各种形式常用的树结构。然而,大多数OO框架可以支持数据图结构而不存在任何技术问题。 因此,在我们的方法中查询关系数据源涉及到表达式的构造,这些表达式引用给定语法上下文的其他相关实体。 6.1实体图形导航^ 关系数据源中属于一种数据集的每个实体可能依赖于属于其他数据集的其他实体,包括属于它自己的类型的实体。这个实体也可能与属于其他类型数据集的其他实体(包括它自己的类型)有依赖关系。这些相互依赖关系创建了实体的有向图,人们可以按照续集中描述的一些导航规则从一个节点“游走”到另一个节点。 对于会员+系统,这里给出了数据图的示意图。它提供了一个可视化的示意图,可以帮助用户更容易地浏览图表。其中关系链接的方向代表一种依赖,即链接进入的实体(A)依赖于链接出来的实体(B),即A依赖于B或B被A依赖于。此处B->A是一种多对多关系。 为了简化视图,该图不包含很多细节,特别是在存在多个依赖关系时(参见下面的内容)。对于一个实体如何与其他实体关联的更精确的信息,可以通过两种方式找到它: 随数据服务一起提供的客户机API文档包含依赖项的名称和性质。对于一种实体的每个数据模型,类级文档的“备注”部分包含一列“此实体依赖的实体”(如果有的话)和一列“依赖于此实体的实体集”(如果有的话)。 一种实体的数据模型的源代码包含相同类型的信息。它们位于当前所依赖的区域#区域实体和依赖当前区域的#区域实体中。 相关实体和实体集的命名约定取决于依赖项是单个的还是多个的。当一个数据集一次或多次依赖于另一个数据集时,从技术上讲,它只有一个引用数据模式中的另一个数据集的外键,那么依赖关系就是single。否则,它对另一个集合有多重依赖关系。 下面是命名约定的规范。让我们为给定的一个相关实体定义一个基对象标识,即base-object-id: 单一依赖项:base-object-id:= <实体-name>,其中<实体-name>是th的对应名字吗e相关的实体。多重依赖:base-object-id:= <entity-name>+ "_" + <foreignKey_name>该依赖项对应的外键属性的名称。 根据依赖关系的性质,我们有以下命名约定 当前实体所依赖的实体的名称为base-object-id + "Ref",例如,UserDetail数据集中的实体依赖于User数据集中的实体,根据规则,数据模型实体的相应属性的名称为UserRef。但有两个例外:1)当相关实体与当前实体属于同一数据集(即自引用),如角色数据集,则名称为UpperRef;2)当它是多重依赖项时,“Ref”后缀不会被追加(这个异常可以在将来删除)。对于那些依赖于当前实体的实体,由于是一对多的关系,它们被记录在不同的实体记录集中: basic -object-id + "s":类型为<实体-名称>+“套”。它以声明的方式将相关集定义为实体的子集(参见这里),而不是加载的子集本身。表示整个相关集合。主要用于服务端。“Changed”+ base-object-id +“s”:属于相关集合的一些成员的数组。可用于在涉及实体图的添加或更新操作中保存已更改的或新的实体。它还可以用于在递归加载部分实体图时保存实体图元素(参见这里)。 例如,userassocinvite实体多次依赖于用户实体,因为(社会)关联邀请具有发送方和接收方,因此关系的两个方面具有以下内容 userassocinvite实体具有User_FromUserID属性(类型为User),表示发送邀请的用户,以及具有相同类型的User_ToUserID属性,表示邀请的目标用户。用户实体有{UserAssocInvitation_FromUserIDs、UserAssocInvitation_FromUserIDEnum ChangedUserAssocInvitation_FromUserIDs}和{UserAssocInvitation_ToUserIDs、UserAssocInvitation_ToUserIDEnum ChangedUserAssocInvitation_ToUserIDs}属性集代表邀请用户发送和接收到的用户。 6.2引用语法 6.2.1过滤器表达式^ 在熟悉实体图导航(命名)约定之后,用于在查询中引用相关实体的语法就变得非常简单了。根据参考方向的不同,它们遵循以下规则: 当引用当前语法上下文中的实体所依赖的实体时,使用相应的属性名作为基名,在下面称为base-name。有两种情况 如果对应的外键不可为空,那么有一种方法可以引用,即base-name + "."。后端将在遇到此令牌后将实体上下文更改为它所引用的内容。如果对应的外键可为空,那么除了上面的外键之外,还有另一个外键,即base-name,它用于构造谓词,判断被引用的实体是否为空。但是,实现此目的的最佳方法是使用外键而不是相应的实体。 当引用在当前语法上下文中依赖于实体的实体时,使用实体集合的属性名称,不带“s”后缀和点字符“。”例如,使用UserAssocInvitation_FromUserID。引用用户在用户(语法)上下文中发送的一组关联邀请。后端将在遇到此令牌后将实体上下文更改为它所引用的内容。 从一个给定的入口语法上下文开始,一个表达式可以被扩展到使用上面的“导航”规则与当前表达式连接的任何其他表达式。好消息是,用户不必精确地记住规则,因为查询智能系统将在导航过程中引导他/她。 此外,令牌如UserAssocInvitation_FromUserID。在一个视图上下文中,许多部分的含义是明显的,而没有感知上的歧义,因此,可能看起来冗长而难看。如果是这种情况,您总是可以使用这里描述的定制系统为它创建别名,以“缩短或简化”它。例如,在用户社交连接邀请管理页面中,规则UserAssocInvitation_FromUserID。→发送。比原来的好得多,因为其他的一切都是由视图上下文假设的。 6.2.2排序表达式 排序也可以根据给定语法上下文的相关实体的属性来执行。然而,与筛选不同的是,它只能在与curre对应的相关实体上执行nt语法上下文取决于。相反的方向没有太大意义,因为它们有一对多的关系。 由于用户数据集不依赖于其他集,因此不能将其用作当前情况的示例。为了便于演示,让我们以当前未实现的UserDetail数据集查询视图为例。假设有人想排序 隐藏,复制Code
CreateDate asc UserRef Username asc
这意味着按升序排序创建(UserDetail实体的),然后按升序排序创建(User entity的)UserRef的用户名(注意UserRef后面没有“。”)。如果他/她只是停留在那里就好了。 如果他/她想继续,下一步有什么选择? 它是用户数据集的排序选项,因为语法上下文由于输入了UserRef令牌而被更改为它。 怎么回去?答案是使用这个操作符。这个操作符将把语法上下文带回到条目1,无论当前条目达到了多深。所以下面的表达是正确的 隐藏,复制Code
CreateDate asc UserRef Username asc this BirthDate asc ID desc
其中,BirthDate和ID引用UserDetail实体的属性,而不是User实体的属性。 举几个例子 下面所有的查询表达式,虽然看起来很冗长,但实际上可以使用本文中描述的查询输入接口在敲几下键之后轻松地构造。 从用户实体集开始,用于选择属于应用程序成员的用户的查询表达式为 隐藏,CodeUserAppMember.Application_Ref副本。Name == "MemberPlusManager" & (UserAppMember。SearchListing是null || UserAppMember。SearchListing == true) 这里UserAppMember。指向UserAppMember实体设置上下文和Application_Ref。导致Application_ entity设置上下文,其中要求对应实体的Name属性与“MemberPlusManager”相同。 从用户实体集开始,用于选择家庭地址匹配关键字“unicons”的用户的查询表达式为 隐藏,CodeCommunication副本。TypeID == 1和通信。关键词匹配模式 {地址“unicons”}前100 其中and运算符映射自&一(参见Web的用户全局定制部分。配置文件)。注意,字符串值周围的引号是自动生成的,用户不应该键入!在这里,它只选择通信通道的类型1(即HomeAddress类型),还要求地址(从AddressInfo映射,请参阅Web的用户查询定制部分)。配置文件)通道的属性,以包含关键字“unicons”。如果发现在查询中使用硬编码的类型id不理想,也可以使用通道类型名构造更复杂的查询,也就是 隐藏,CodeCommunication.CommunicationTypeRef副本。TypeName ==家庭地址和 沟通。关键字匹配模式{地址“unicons”}前100 在这里,HomeAddress值没有引用,因为在构建数据服务时,它被视为一个离散的(可枚举的)值。 如果读者不想要那么多的准确性,或者想要得到一些“出乎意料”的东西,他/她就不用那么严格的过滤 隐藏,CodeCommunication副本。关键字匹配模式{地址“unicons”}前100 如果读者希望在关键字匹配方面更加复杂,他可以构造下面的搜索短语 隐藏,CodeCommunication副本。关键字匹配模式{ 地址为“加利福尼亚”或“加拿大”或评论为“ok” }位于页1,其中页的大小为100 发现自己被描述为“极客”: 隐藏,CodeDetail副本。关键词匹配模式{“极客”}前100 在这里,由于UserDetail数据集只有一个属性全文索引,所以该属性的名称不会出现在表达式中。 发现自己被描述为“极客”或“书呆子”: 隐藏,CodeDetail副本。关键字匹配模式{“$geek OR nerd”}前100 这里使用$操作符转义下面的表达式,以便可以指定任意的Lucene关键字搜索模式。在这里,系统不会引导用户构造,也不会检查Lucene搜索子表达式的有效性。 7. 历史^ 2014-03-19。文章版本1.0.0,初始版本。2014-03-25。文章1.0.5版本。数据模式更改:添加了MemberNotificationType、MemberNotification数据集。添加了UserAppMember数据集的ConnectionID, code>AcceptLanguages和SearchListing属性。为UserGroup数据集添加了ApplicationID外键属性。对服务文档进行了微小的更改。2014-05-06。文章1.2.0版本。数据服务现在运行在。net 4.5.1和Asp下。Net Mvc 5中包含了显著的扩展和改进。web应用程序被升级为在最新的库下运行。增加了许多新的数据集,以支持未来可扩展的信号推送通知功能。 如果一个读者对git源码控制系统有足够的了解,项目可以在github.com上查看git库。本文的源代码是在codeproject-3分支上维护的,也就是这里。 本文转载于:http://www.diyabc.com/frontweb/news19404.html