使用自跟踪实体生成器和 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 月 - 首次发布。


