使用自跟踪实体生成器和 Visual Studio 2012 构建 WPF 应用程序 - 排序、分页和筛选
本文介绍了如何使用自跟踪实体生成器和 Visual Studio 2012 实现排序、分页和筛选。
目录
- 引言
- 第一个示例
- ClientQuery 类
- 静态 ClientQuerySet 类
- ClientQuery<T> 类和 IClientQuery<T> 接口
- ObjectQuery
扩展方法 - 客户端示例 - CoursePageViewModel
- 服务器端示例 - SchoolService
- ClientFilter<T> 类
- 限制
- 总结
- 参考文献
- 历史
引言
WPF/Silverlight 的自跟踪实体生成器可以选择生成 ClientQuery
和 ClientFilter<T>
类实现,它们提供了动态构建类型安全查询以进行排序、分页和筛选的功能。这些查询可以用作 WCF 服务调用的一部分,并合并为 LINQ to Entities 查询的一部分。在本文中,我们将讨论这两个类的各种方法和属性,还将通过我们演示应用程序中的示例了解如何使用这两个类。但是,在我们深入研究之前,让我们先从一个简单的示例开始。
第一个示例
在我们的演示应用程序中,假设我们要查找所有符合以下条件的课程:
- 筛选条件:课程标题应以“CS”开头,课程注册人数应大于 20。
- 排序条件:所有匹配的课程首先按讲师姓名升序排序,然后按注册人数降序排序。
- 分页条件:仅显示第一页,每页大小为五。
要创建这样的搜索条件,我们需要构建一个 ClientQuery
对象,如下所示:
var clientQuery = ClientQuerySet.Courses
.Where(n => n.Title.StartsWith("CS") && n.Enrollments.Count > 20)
.OrderBy(n => n.Instructor.Name).ThenByDescending(n => n.Enrollments.Count)
.Skip((CurrentPage - 1) * PageSize).Take(PageSize)
.AsClientQuery();
上面的语句以静态类 ClientQuerySet
的静态属性 Courses
开始。Courses
属性返回 ClientQuery<Course>
的新实例,我们可以在其中开始链式调用我们的搜索方法。静态类 ClientQuerySet
是自动生成的,它包含 Entity Data Model 文件 *SchoolModel.edmx* 中定义的每个实体的静态属性。
第一个链式方法是 Where()
,它只有一个参数,即 C# 谓词 n => n.Title.StartsWith("CS") && n.Enrollments.Count > 20
,这正是我们的第一个筛选条件:课程标题应以“CS”开头,课程注册人数应大于 20。
接下来的两个链式方法是 OrderBy()
和 ThenByDescending()
。这两个方法指定所有匹配的课程首先按讲师姓名升序排序,然后按注册人数降序排序。
第四和第五个链式方法 Skip()
和 Take()
赋予我们设置分页条件的能力。最后一个方法 AsClientQuery()
将所有先前指定的条件组合起来,并返回一个 ClientQuery
对象。然后将此对象作为参数传递给以下异步方法 GetCoursesAsync()
。
_schoolModel.GetCoursesAsync(clientQuery, "CoursePage");
GetCoursesAsync()
方法最终会调用我们的 WCF 服务,并到达服务器端方法 GetCourses()
,定义如下:
public List<Course> GetCourses(ClientQuery clientQuery)
{
using (var context = new SchoolEntities())
{
return context.Courses.ApplyClientQuery(clientQuery).ToList();
}
}
在服务器端,唯一值得关注的方法是 ApplyClientQuery()
。此方法获取 ClientQuery
对象中反序列化的表达式树,并针对 ObjectQuery<Course>
对象应用我们上面指定的搜索条件。Entity Framework 生成的 SQL 语句如下所示:
我们可以清楚地看到,SQL 查询首先根据课程标题以“CS”开头且注册人数大于 20 来筛选课程。然后,它按讲师姓名升序和注册人数降序对所有匹配的课程进行排序。由于我们只需要显示第一页,每页大小为五,因此 SQL SELECT
语句返回前五行。
因此,第一个示例表明,我们在客户端指定的搜索条件正在通过网络传输,应用于 LINQ to Entities 查询,最后,它被转换为 SQL 查询并在数据库上执行。
如果第一个示例看起来很有趣,那么让我们继续详细介绍 ClientQuery
类。
ClientQuery 类
ClientQuery
类仅用于一个目的:它存储包含路径列表和序列化表达式树,它可以作为参数传递给 WCF 服务调用,并在服务器端合并为 LINQ to Entities 查询的一部分。为了创建具有指定搜索条件的 ClientQuery
对象,我们需要另外两个类的帮助:ClientQuerySet
和 ClientQuery<T>
。
静态 ClientQuerySet 类
静态类 ClientQuerySet
包含 Entity Data Model 文件 *SchoolModel.edmx* 中定义的每个实体的属性。这个类主要是一个便捷类,始终用作构建搜索条件的起点。类中的每个属性都返回类 ClientQuery<T>
的新实例,如下面的代码片段所示:
public static class ClientQuerySet
{
......
public static ClientQuery<Course> Courses
{
get { return new ClientQuery<Course>(); }
}
......
}
ClientQuery<T> 类和 IClientQuery<T> 接口
另一个辅助类 ClientQuery<T>
实现接口 IClientQuery<T>
,该接口包含以下成员:
Include()
指定要在客户端查询结果中包含的相关对象。查询路径是全包容的,可以多次在ClientQuery<T>
上调用以指定查询的多个路径。Where()
根据谓词筛选客户端查询结果。OrderBy()
按键升序排序客户端查询结果。OrderByDescending()
按键降序排序客户端查询结果。ThenBy()
按升序对客户端查询结果进行后续排序。ThenByDescending()
按降序对客户端查询结果进行后续排序。Skip()
跳过客户端查询结果中指定数量的元素,然后返回剩余的元素。Take()
从客户端查询结果的开头返回指定数量的连续元素。ApplyClientFilter()
将ClientFilter<T>
的搜索条件合并到调用方ClientQuery<T>
中。AsClientQuery()
返回ClientQuery<T>
的搜索条件作为ClientQuery
。
Include()
方法有两个重载形式。例如,如果我们想包含讲师教授的所有课程,我们可以使用 ClientQuerySet.Instructors.Include("Courses")
或 ClientQuerySet.Instructors.Include(n => n.Courses)
。由于后者在编译时启用了类型检查,因此通常是首选。
接下来的七个方法主要为我们提供了构建筛选、排序和分页条件的能力。ApplyClientFilter()
方法将 ClientFilter<T>
的搜索条件合并到调用方 ClientQuery<T>
对象中,我们将在下一节讨论 ClientFilter<T>
类。最后一个方法 AsClientQuery()
始终用作最后的链式方法。它结合了所有先前指定的条件,并返回一个 ClientQuery
对象。
ObjectQuery<T> 扩展方法
在服务器端,有一个自动生成的静态类 ObjectQueryExtension
,其中包含我们在第一个示例中讨论的 ApplyClientQuery()
方法。此类在我们的演示应用程序的 SchoolModel.Context.cs
中生成,并包含以下两个扩展方法:
ApplyIncludePath()
从ClientQuery
对象获取包含路径列表,并调用ObjectQuery<T>
上的Include()
。ApplyClientQuery()
获取ClientQuery
对象中的包含路径列表和反序列化的表达式树,调用Include()
并将搜索条件应用于ObjectQuery<T>
。
这两个方法都接受一个 ClientQuery
参数。前者仅为包含路径列表调用 Include()
,而后者同时应用包含路径列表和搜索条件。
现在我们已经介绍了 ClientQuery
类及其所有相关辅助类的规范,我们可以开始了解这些类和方法如何在我们的演示应用程序中使用。
客户端示例 - CoursePageViewModel
CoursePageViewModel
类承载了查询 Course
实体的绝大部分逻辑,并且是我们创建 ClientQuery
实例的地方。这通常是一个两步过程。首先,我们需要使用通用设置初始化 ClientQuery<Course>
实例。由于在此 ViewModel 类中,Course
实体的相关对象始终相同,因此我们创建一个包含“Enrollments.Student
”的 CourseClientQuery
对象,如下所示:
private static readonly ClientQuery<Course> CourseClientQuery =
ClientQuerySet.Courses.Include(n => n.Enrollments.Include(m => m.Student));
接下来,在构造函数中,我们可以通过首先对 CourseClientQuery
调用 ApplyClientFilter()
,然后调用 Skip()
和 Take()
来指定分页条件来简单地创建 ClientQuery
实例。
// set current page
CurrentPage = 1;
// set current filter to order by CourseId
_currentFilter = new ClientFilter<Course>().OrderBy(n => n.CourseId);
// get courses
var courseQuery = CourseClientQuery
.ApplyClientFilter(_currentFilter)
.Skip((CurrentPage - 1)*PageSize).Take(PageSize)
.AsClientQuery();
_schoolModel.GetCoursesAsync(courseQuery, "CoursePage");
从上面的代码片段可以看出,在 ViewModel 类中使用 ClientQuery
非常简单直接。事实上,大部分处理动态构建筛选和排序条件的逻辑是通过 ClientFilter<T>
类在 ViewModel 类之外完成的。
服务器端示例 - SchoolService
在服务器端,我们调用 ApplyClientQuery()
方法将包含路径列表和搜索条件都应用于 LINQ to Entities 查询,正如我们在第一个示例中所做的那样。
public List<Course> GetCourses(ClientQuery clientQuery)
{
using (var context = new SchoolEntities())
{
if (clientQuery.IncludeList.Count == 0)
{
return context.Courses.ApplyClientQuery(clientQuery).ToList();
}
var courseList = new List<Course>();
foreach (var course in context.Courses.ApplyClientQuery(clientQuery).ToList())
{
var currentCourse = course;
using (var innerContext = new SchoolEntities())
{
courseList.Add(
innerContext.Courses
.ApplyIncludePath(clientQuery)
.Single(n => n.CourseId == currentCourse.CourseId));
}
}
return courseList;
}
}
尚未涵盖的是,我们可以进一步指定服务器端默认筛选条件,以及从客户端传递的包含路径列表和搜索条件。为了实现这一点,我们可以使用 Entity SQL,如下面的代码示例所示:
public List<Course> GetCourses(ClientQuery clientQuery)
{
using (var context = new SchoolEntities())
{
var query = context.Courses
.Where("it.StartDate >= DATETIME'2012-01-01 00:00'")
.ApplyClientQuery(clientQuery);
if (clientQuery.IncludeList.Count == 0)
{
return query.ToList();
}
var courseList = new List<Course>();
foreach (var course in query.ToList())
{
var currentCourse = course;
using (var innerContext = new SchoolEntities())
{
courseList.Add(
innerContext.Courses
.ApplyIncludePath(clientQuery)
.Single(n => n.CourseId == currentCourse.CourseId));
}
}
return courseList;
}
}
在调用 ApplyClientQuery()
之前,调用了 Where("it.StartDate >= DATETIME'2012-01-01 00:00'")
,这基本上将所有课程结果限制在 2012 年初之后开始的。
此外,如果我们更喜欢使用 LINQ 谓词而不是 Entity SQL,我们可以选择下一个示例中列出的不同方法。这次,我们在 ApplyClientQuery()
之后调用 Where(n => n.StartDate >= new DateTime(2012, 01, 01))
,而生成的 SQL 语句在这两种情况下在功能上是相同的。
public List<Course> GetCourses(ClientQuery clientQuery)
{
using (var context = new SchoolEntities())
{
var query = context.Courses
.ApplyClientQuery(clientQuery)
.Where(n => n.StartDate >= new DateTime(2012, 01, 01));
if (clientQuery.IncludeList.Count == 0)
{
return query.ToList();
}
var courseList = new List<Course>();
foreach (var course in query.ToList())
{
var currentCourse = course;
using (var innerContext = new SchoolEntities())
{
courseList.Add(
innerContext.Courses
.ApplyIncludePath(clientQuery)
.Single(n => n.CourseId == currentCourse.CourseId));
}
}
return courseList;
}
}
到目前为止,我们已经涵盖了 ClientQuery
类的所有功能,我们的下一个主题是 ClientFilter<T>
类。
ClientFilter<T> 类
ClientFilter<T>
类也是仅为一个目的而创建的:它提供了动态构建类型安全查询以进行排序、分页和筛选的功能,这些查询随后可以合并为 ClientQuery
实例的一部分。此外,它还可以以字符串格式持久化以供将来重用。下面是该类方法和属性的列表:
And()
返回一个ClientFilter<T>
,它表示调用方ClientFilter<T>
与 where 谓词参数之间的条件 AND 操作。Or()
返回一个ClientFilter<T>
,它表示调用方ClientFilter<T>
与 where 谓词参数之间的条件 OR 操作。OrderBy()
按键升序排序序列中的元素。OrderByDescending()
按键降序排序序列中的元素。Skip()
跳过序列中指定数量的元素,然后返回剩余的元素。Take()
从序列的开头返回指定数量的连续元素。ToString()
以字符串形式返回当前的ClientFilter<T>
。Parse()
将ClientFilter<T>
值的指定字符串表示形式转换为其等效的ClientFilter<T>
。
WhereExpression
是只读的,并返回动态构建的 where 谓词。SortExpressions
是只读的,并返回动态构建的排序集合。SkipCount
是只读的,并保留在返回剩余元素之前要跳过的元素数量。TakeCount
是只读的,并保留要返回的元素数量。
动态构建筛选和排序条件
现在让我们通过一些示例来看看如何动态构建类型安全查询。第一个示例是查询所有开始日期在 2012 年初之后且结束日期在 2012 年底的课程。结果按课程标题升序排序,然后按课程开始日期降序排序。
// n => n.StartDate >= new DateTime(2012, 1, 1) &&
// n.EndDate <= new DateTime(2012, 12, 31)
var clientFilter = new ClientFilter<Course>(n => n.StartDate >= new DateTime(2012, 1, 1));
clientFilter = clientFilter.And(n => n.EndDate <= new DateTime(2012, 12, 31));
clientFilter = clientFilter.OrderBy(n => n.Title) // Ascending
clientFilter = clientFilter.OrderByDescending(n => n.StartDate); // Descending
上面的源代码显示,我们首先使用谓词 n => n.StartDate >= new DateTime(2012, 1, 1)
创建一个新的 ClientFilter<Course>
。然后,我们在新的 ClientFilter<Course>
上调用 And()
方法,并传入另一个谓词 n => n.EndDate <= new DateTime(2012, 12, 31)
。这两个步骤创建了我们刚刚指定的筛选条件。然后,接下来的两行代码一次添加一个排序条件,即按课程标题升序排序,然后按课程开始日期降序排序。请注意,ClientFilter<Course>
类中没有 ThenBy()
或 ThenByDescending()
方法。这使得动态构建排序顺序变得更加容易。
我们的第二个示例稍微复杂一些,我们需要构建一个查询,用于查找所有标题以“CS”开头,总注册人数小于 10 或大于 20,并且课程开始日期在 2012 年初的课程。问题是如何处理第二个条件周围的括号。
// n => n.Title.StartsWith("CS") &&
// (n.Enrollments.Count < 10 || n.Enrollments.Count > 20) &&
// n.StartDate >= new DateTime(2012, 1, 1)
var inner = new ClientFilter<Course>(n => n.Enrollments.Count < 10);
inner = inner.Or(n => n.Enrollments.Count > 20);
var outer = new ClientFilter<Course>(n => n.Title.StartsWith("CS"));
outer = outer.And(inner.WhereExpression);
outer = outer.And(n => n.StartDate >= new DateTime(2012, 1, 1));
如上面源代码的前两行所示,我们需要先构建带括号的 OR 表达式。之后,我们可以使用 And()
方法调用将内部筛选条件与外部条件组合起来:outer = outer.And(inner.WhereExpression)
。
将 ClientFilter<T> 对象持久化到数据库
ClientFilter<T>
类还可以以字符串格式持久化和加载回来。这对于需要执行一些常见搜索任务的应用程序非常有用。我们可以通过创建 ClientFilter<T>
实例,将其转换为字符串格式,然后保存到数据库,这样用户就可以在以后重用它们,而无需每天重复这些任务。为了实现这一点,我们需要以下两个方法:ToString()
和 Parse()
。
接下来的代码片段来自我们演示应用程序的 CoursePageViewModel
类,每当我们需要将 ClientFilter<T>
保存到数据库时,都会调用 OnCourseDefaultFilterMessage
方法。
/// <summary>
/// Message handler for CourseDefaultFilterMessage
/// </summary>
/// <param name="clientFilter"></param>
private void OnCourseDefaultFilterMessage(ClientFilter<Course> clientFilter)
{
if (clientFilter != null)
{
var defaultFilter = new DefaultFilter
{
ScreenName = "CoursePage",
Filter = clientFilter.ToString()
};
if (_defaultClientFilter == null) defaultFilter.MarkAsAdded();
else defaultFilter.MarkAsModified();
// persist the new default filtering conditions to database
_schoolModel.SaveDefaultFilterAsync(defaultFilter);
_defaultClientFilter = clientFilter;
}
}
首先,我们可以看到调用了 clientFilter.ToString()
,它将 clientFilter
转换为字符串格式。之后,我们只需调用 _schoolModel.SaveDefaultFilterAsync(defaultFilter)
将新的搜索条件保存到数据库。
为了从数据库加载已保存的搜索条件,我们使用以下代码行,它定义在 CoursePageViewModel
类的构造函数中:
// get default filter
_schoolModel.GetDefaultFilterByScreenNameAsync("CoursePage");
当 GetDefaultFilterByScreenNameAsync
调用完成时,将触发以下事件处理程序。在此事件处理程序中,我们首先检查是否返回了任何已保存的搜索条件。如果返回了,那么我们调用 ClientFilter<Course>.Parse(e.Results.Filter)
来解析该已保存的搜索条件。
/// <summary>
/// Event handler for GetDefaultFilterByScreenNameCompleted
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void _schoolModel_GetDefaultFilterByScreenNameCompleted(object sender, ResultArgs<DefaultFilter> e)
{
if (!e.HasError)
{
// clear any previous error after a successful call
_schoolModel.ClearLastError();
if (e.Results != null && e.Results.ScreenName == "CoursePage")
{
_defaultClientFilter = ClientFilter<Course>.Parse(e.Results.Filter);
}
}
else
{
// notify user if there is any error
AppMessages.RaiseErrorMessage.Send(e.Error);
}
}
局限性
ClientQuery
和 ClientFilter<T>
类使我们能够在客户端动态构建搜索条件,然后这些搜索条件通过 LINQ to Entities 查询在服务器端执行。但是,我们需要记住,这些动态构建的搜索条件最终将成为 LINQ to Entities 查询的一部分,并且它们必须遵循 LINQ to Entities 的限制。例如,如果我们创建以下搜索条件并调用到服务器端,我们将收到一个异常。
var clientQuery = ClientQuerySet.Courses
.Where(n => n.Title.StartsWith("CS"))
.Skip((CurrentPage - 1) * PageSize).Take(PageSize)
.AsClientQuery();
_schoolModel.GetCoursesAsync(clientQuery, "CoursePage");
来自 LINQ to Entities 的错误消息基本上表明,在首先创建排序条件之前,我们不能设置分页条件。这是有道理的,因为如果没有 ORDER BY
子句,SQL 查询将返回一个没有特定顺序的集合。所以,在这种情况下不能使用分页条件。
总结
我们已经完成了关于如何使用 WPF/Silverlight 的自跟踪实体生成器进行排序、分页和筛选的讨论。总而言之,ClientQuery
类存储包含路径列表和序列化表达式树,它可以作为参数传递给 WCF 服务调用,并在服务器端合并为 LINQ to Entities 查询的一部分。另一个类 ClientFilter<T>
提供了动态构建类型安全查询以进行排序、分页和筛选的功能,这些查询随后可以合并为第一个类的部分。
希望您觉得本文有用,请在下方评分和/或留下反馈。谢谢!
参考文献
历史
- 2013 年 3 月 - 首次发布。