数据点

使用 Entity Framework 和 ASP.NET MVC 3 进行服务器端分页

Julie Lerman

下载代码示例

image: Julie Lerman
在我的二月份数据点专栏中,我展示了 jQuery DataTables 插件,及其在客户端无缝处理海量数据的能力。这非常适合您要切片和切块大量数据的 Web 应用程序。本月,我将重点讲述使用返回较小负载的查询来与数据之间进行不同类型的交互。如果您以移动应用程序为目标,则这一点尤为重要。

我将利用 ASP.NET MVC 3 中引入的功能,并演示如何将它们与 Entity Framework 的高效服务器端分页功能结合使用。这个任务有两项挑战。首先是为 Entity Framework 查询提供正确的分页参数。其次,是通过提供指示这里有过多待检索数据的可视线索以及触发检索的链接,来模拟客户端分页。

ASP.NET MVC 3 有许多新功能,如新 Razor 视图引擎、验证改进功能以及许多的 JavaScript 功能。MVC 的启动页位于 asp.net/mvc,您可以从这里下载 ASP.NET MVC 3 并找到能帮助您加快上手速度的博客文章与培训视频的链接。我将使用的新功能之一是 ViewBag。如果您以前使用 ASP.NET MVC,则会知道 ViewBag 是 ViewData 类的增强功能,使您可以使用动态创建的属性。

ASP.NET MVC 3 为表带来的另一个新元素是特殊的 System.Web.Helpers.WebGrid。尽管该网格的功能之一是分页,但我会在本示例中使用这个新网格,却不会使用它的分页功能,因为该分页是客户端分页,换言之,它会对提供给它的数据集进行分页,与 DataTables 插件类似。我要使用的是服务器端分页。

对于这个小应用程序,您需要使用一个实体对象模型。我使用是从 Microsoft AdventureWorksLT 示例数据库创建的一个模型,但我只需要将 Customer 和 SalesOrderHeaders 带入模型中。我已经将 Customer rowguid、PasswordHash 和 PasswordSalt 属性移到另一个实体中,这样我在编辑的时候就不用担心它们了。除了这个小更改之后,我没有更改模型的默认值。

我使用默认的 ASP.NET MVC 3 项目模板创建了一个项目。这会预填充许多控制器和视图,我将让默认的 HomeController 呈现 Customers。

我将使用一个简单的 DataAccess 类来提供与模型、上下文以及随后的数据库的交互。在此类中,我的 GetPagedCustomers 方法提供服务器端分页。如果 ASP.NET MVC 应用程序的目标是允许用户与所有客户交互,则单个查询返回的和在浏览器中管理的将是大量的客户。相反,我们将让应用程序每次呈现 10 行,而 GetPagedCustomers 将提供该筛选器。我最终需要执行的查询将如下所示:

context.Customers.Where(c => 
c.SalesOrderHeaders.Any()).Skip(skip).Take(take).ToList()

视图将会知道请求哪个页面,并将该信息提供给控制器。 控制器将负责了解每页提供多少行。 控制器将使用页码和每页行数计算“skip”值。 当控制器调用 GetPagedCustomers 方法时,它会传入计算出的 skip 值以及每页行数,即“take”值。 所以,如果您在第 4 页上并且每页显示 10 行,则 skip 将为 40,而 take 将为 10。

分页查询首先创建一个筛选器,只请求那些具有任意 SalesOrder 的客户。 然后,使用 LINQ Skip 和 Take 方法,产生的数据将是这些客户的一个子集。 然后,完整的查询,包括分页,将在数据库中执行。 数据库只返回 Take 方法指定的行数。

该查询分为几个部分编写,使我们能在过程中不断添加一些技巧。 以下是第一部分,将从 HomeController 调用的 GetPagedCustomers 方法:

public static List<Customer> GetPagedCustomers(int skip, int take)
    {
      using (var context = new AdventureWorksLTEntities())
      {
        var query = context.Customers.Include("SalesOrderHeaders")
          .Where(c => c.SalesOrderHeaders.Any())
          .OrderBy(c => c.CompanyName + c.LastName + c.FirstName);

        return query.Skip(skip).Take(take).ToList();
      }
    }

调用此方法的控制器 Index 方法将使用我称为 pageSize 的变量确定将要返回的行数,这个变量将成为 Take 的值。Index 方法还会根据将作为参数传入的页码指定开始位置,如下所示:

public ActionResult Index(int?
page)
    {
      const int pageSize = 10;
      var customers=DataAccess.GetPagedCustomers((page ??
0)*pageSize, pageSize);
      return View(customers);
    }

这使我们获得了一个很好的部分。服务器端分页已经完全就绪。通过 Index 视图标记中的 WebGrid,我们可以显示从 GetPagedCustomers 方法返回的客户。在标记中,您需要声明和实例化该网格、传入 Model,它代表控制器创造视图时提供的 List<Customer>。然后,使用 WebGrid GetHtml 方法,您可以设置网络的格式,并指定显示哪些列。我将只显示三个客户属性:CompanyName、FirstName 和 LastName。您会很开心地发现在您键入标记时会获得完全的 IntelliSense 支持,无论您使用与 ASPX 视图关联的语法,还是使用新的 MVC 3 Razor 视图引擎语法(如以下示例所示)。在第一列中,我将提供一个 Edit ActionLink,以使用户可以编辑显示的任何客户:

@{
  var grid = new WebGrid(Model); 
}
<div id="customergrid">
  @grid.GetHtml(columns: grid.Columns(
    grid.Column(format: (item) => Html.ActionLink
      ("Edit", "Edit", new { customerId = item.CustomerID })),
  grid.Column("CompanyName", "Company"), 
  grid.Column("FirstName", "First Name"),
  grid.Column("LastName", "Last Name")
   ))
</div>

结果如图 1 所示。

image: Providing Edit ActionLinks in the WebGrid

图 1 在 WebGrid 中提供 Edit ActionLink

到目前为止一切顺利。但这没有为用户提供导航到另一页数据的方法。有多种方法可以实现这一目的。一种方法是在 URI 中指定页码,例如,http://adventureworksmvc.com/Page/3。您肯定不会要求最终用户来做这个。更易于发现的机制是使用分页控件,如页码链接“1 2 3 4 5 …”或指示向前或向后的链接,例如“<<      >>”。

启用分页链接目前的障碍是 Index 视图页不知道将会获取过多的客户。它只知道它要显示的客户范围是 10。通过向数据访问层添加一些额外的逻辑,然后通过控制器将其向下传递给视图,您可以解决这个问题。让我们从数据访问逻辑开始。

为了知道有没有当前客户集以外的记录,您将需要在以 10 个为一组分页前对查询将会返回的所有可能的客户进行计数。这里就是在 GetPagedCustomers 中编写查询起作用的地方。注意,第一个查询返回到 _customerQuery 中,这是一个在类级别声明的变量,如以下所示:

_customerQuery = context.Customers.Where(c => c.SalesOrderHeaders.Any());

您可以将 Count 方法附加在该查询的末尾,以在应用分页之前获得匹配查询的客户的计数。Count 方法会强制立即执行一个相当简单的查询。以下就是在 SQL Server 中执行的查询,这个查询会返回一个值作为响应:

    SELECT 
    [GroupBy1].[A1] AS [C1]
    FROM ( SELECT 
           COUNT(1) AS [A1]
           FROM [SalesLT].[Customer] AS [Extent1]
           WHERE  EXISTS (SELECT 
                  1 AS [C1]
                  FROM [SalesLT].[SalesOrderHeader] AS [Extent2]
                  WHERE [Extent1].[CustomerID] = [Extent2].[CustomerID]
           )
    )  AS [GroupBy1]

您得到计数后,可以确定当前客户页是第一页、最后一页还是二者中间的页。 然后,您可以使用该逻辑确定要显示哪些链接。 例如,如果您在第一页客户之后,则当然要显示一个链接来访问之前的客户数据页面,而这个链接为前一页,例如。“<<”。

我们可以计算值以在数据访问类中表示此逻辑,然后将其在包装类中和客户一起公开。 以下我们将要使用的新类:

public class PagedList<T>
  {
    public bool HasNext { get; set; }
    public bool HasPrevious { get; set; }
    public List<T> Entities { get; set; }
  }

GetPagedCustomers 方法现在将返回 PagedList 类,而不是 List。 图 2 显示新版 GetPagedCustomers。

图 2 新版 GetPagedCustomers

public static PagedList<Customer> GetPagedCustomers(int skip, int take)
    {
      using (var context = new AdventureWorksLTEntities())
      {
        var query = context.Customers.Include("SalesOrderHeaders")
          .Where(c => c.SalesOrderHeaders.Any())
          .OrderBy(c => c.CompanyName + c.LastName + c.FirstName);

        var customerCount = query.Count();

        var customers = query.Skip(skip).Take(take).ToList();
      
        return new PagedList<Customer>
        {
          Entities = customers,
          HasNext = (skip + 10 < customerCount),
          HasPrevious = (skip > 0)
        };
      }
    }

填充新的变量后,让我们看一看 HomeController 中 Index 方法如何能将它们推回视图。 在这里,您可以使用 ViewBag。 我们将仍在视图中返回客户查询的结果,但是您可以额外补充一些值,以帮助确定 ViewBag 中下一页和上一页链接的标记是什么样的。 它们随后可在运行时用于该视图:

public ActionResult Index(int?
page)
    {
      const int pageSize = 10;
      var customers=DataAccess.GetPagedCustomers((page ??
0)*pageSize, pageSize);
      ViewBag.HasPrevious = DataAccess.HasPreviousCustomers;
      ViewBag.HasMore = DataAccess.HasMoreCustomers;
      ViewBag.CurrentPage = (page ??
0);
      return View(customers);
    }

重要的是,必须了解 ViewBag 是动态的,而不是强类型的。ViewBag 并不真的带有 HasPrevious 和 HasMore。我刚刚在键入代码时才创建了它们。所以不要吃惊 IntelliSense 没有向您提供提示。您可以创建自己喜欢的任何动态属性。

如果您已经在使用 ViewPage.ViewData 字典并且疑惑这有什么不同,会发现 ViewBag 实际 执行相同的作业。除了使代码变得美观外,还键入了属性。例如,HasNext 是 dynamic{bool},而 CurrentPage 是 dynamic{int}。以后检索这些值时,就不用再转换它们了。

在标记中,我在 Model 变量中仍有一个客户列表,但还有一个 ViewBag 变量。您要自己在标记中键入动态属性。工具提示会提醒您属性是动态的,如图 3 所示。

image: ViewBag Properties Aren’t Available Through IntelliSense Because They’re Dynamic

图 3 ViewBag 属性不会通过 IntelliSense 提供,因为它们是动态属性

以下是使用 ViewBag 变量确定是否显示导航链接的标记:

@{ if (ViewBag.HasPrevious)
  {
    @Html.ActionLink("<<", "Index", new { page = (ViewBag.CurrentPage - 1) })
  }
}

@{ if (ViewBag.HasMore)
   { @Html.ActionLink(">>", "Index", new { page = (ViewBag.CurrentPage + 1) }) 
  }
}

此逻辑是对 NerdDinner 应用程序教程中所用标记的一种修改,这个教程位于 nerddinnerbook.s3.amazonaws.com/Intro.htm 上。

现在当我运行应用程序时,我就可以从一页客户导般到下一页客户。

如果我在第一页上时,我有一个可以导航到下一页的链接,但是没有导航到前一页的链接,因为没有前一页(请参见图 4)。

image: The First Page of Customer Has Only a Link to Navigate to the Next Page

图 4 第一页客户只有一个导航到下一页的链接

当我单击该链接并导航到下一页时,您可以看到这里分别有转到上一页和下一页的链接(请参见图 5)。

image: A Single Page of Customers with Navigation Links to Go to Previous or Next Page of Customers

图 5 具有导航到上一页或下一页客户的导航链接的单个客户页

 当然,下一步将是使用设计器使这个分页更具吸引力。

工具箱中不可或缺的部分

总而言之,尽管有多种工具可以简化客户端分页,如 jQuery DataTables 扩展和新的 ASP.NET MVC 3 WebGrid,您的应用程序需求却不一定能从带回大量数据中获益。能够执行有效的服务器端分页是工具箱中不可或缺的部分。Entity Framework 和 ASP.NET MVC 共同协作,能够提供优秀的用户体验,同时也能够简化您的开发任务,以至成功完成任务。

Julie Lerman是 Microsoft MVP、.NET 导师和顾问,住在佛蒙特州的山区。您可以在全球的用户组和会议中看到她对数据访问和其他 Microsoft .NET 主题的演示。Lerman 是《Programming Entity Framework》(O'Reilly Media,2009)一书的作者,该书受到广泛称赞,她的博客地址是 thedatafarm.com/blog。您可以通过 Twitter.com/julielerman 了解她。

衷心感谢以下技术专家对本文的审阅:Vishal Joshi