65.9K
CodeProject 正在变化。 阅读更多。
Home

使用自跟踪实体生成器和 Visual Studio 2012 构建 WPF 应用程序 - 排序、分页和筛选

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2013 年 3 月 19 日

CPOL

13分钟阅读

viewsIcon

28257

本文介绍了如何使用自跟踪实体生成器和 Visual Studio 2012 实现排序、分页和筛选。

  • 这里 下载源代码
  • 请访问此 项目站点 以获取最新版本和源代码。

目录

引言

WPF/Silverlight 的自跟踪实体生成器可以选择生成 ClientQueryClientFilter<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 对象,我们需要另外两个类的帮助:ClientQuerySetClientQuery<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);
  }
}

局限性

ClientQueryClientFilter<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 月 - 首次发布。
© . All rights reserved.