在 ASP.Net MVC 应用程序中实现分页的两种(并非如此)不同的方法(纯 MVC 和 Mustache)






4.44/5 (4投票s)
这种方法不采用传统的数字/文本分页,而是使用单个按钮在同一页面加载更多数据。
引言
无论如何,我们都习惯于在应用程序中提供分页功能。传统的分页方法可能包括以下一种:
- 数字分页器
- 带有“下一页”、“上一页”、“第一页”和“最后一页”链接的文本分页器
- 或者两者的组合
随着许多新技术的出现,许多网站不再遵循这些传统方法。需要分页的页面会在您滚动时自动为您完成(例如“Twitter”)。以前,它们会提供一个“更多”按钮,该按钮除了滚动之外,还可以通过用户交互完成加载更多数据的任务。您可能已经猜到了,本文不是关于传统方法的。也不是关于用户滚动页面时加载数据的方法,因为我不是这种方法的粉丝:)。在本文中,我将讨论使用单个按钮完成分页的 2 种方法。那么,让我们开始吧!!!
背景
我正在进行一个需要分页的新项目。我想摆脱传统方法,因此创建了这个仅使用一个按钮进行分页的简单方法。附带的示例项目使用 bootstrap 作为网站的基本布局,我认为当您看到上面的截图时就会猜到这一点!
方法 1:[大部分] 纯 MVC 分页
在此方法中,我将主要使用 MVC 局部视图和一点 JavaScript。下面是 `Index` 操作的内容,它将启动一切。
@{ ViewBag.Title = "Index"; } @section pageScripts { @Scripts.Render("~/bundles/paging") } @{ Html.RenderAction("PostsPartial", "MvcPaging"); }
首先,我有一个 `BasePagingController` 类,其中包含两个分页方法共享的操作结果和方法,如下所示:
public class BasePagingController : Controller { public ActionResult PostsPartial() { var model = GetModel(0); return PartialView("PostsPartial", model); } public JsonResult CheckPostsStatus(int currentPageNumber) { var posts = GetPosts(); var totalPages = (int)Math.Ceiling((decimal)posts.Count() / 5); return Json((currentPageNumber + 1) < totalPages, JsonRequestBehavior.AllowGet); } protected PostViewModel GetModel(int currentPageNumber) { var posts = GetPosts(); if (currentPageNumber == 0) { return new PostViewModel { Posts = posts.Take(NumberOfPosts).ToList() }; } var currentPage = currentPageNumber + 1; return new PostViewModel { Posts = posts.Skip((currentPage - 1) * NumberOfPosts).Take(NumberOfPosts).ToList() }; } protected ListGetPosts() { // -- snip -- // - return some dummy posts - } private const int NumberOfPosts = 5; }
`PostPartial` 是一个操作方法,它返回带有所需模型的局部视图。此方法由此分页方法和下一个分页方法共享,用于创建页面的初始视图。`CheckPostsStatus` 是另一个共享的操作方法,用于由这两种方法“查看”是否还有更多页面。在接下来的章节中,我将讨论我们如何完全摆脱这个操作方法。但现在,让我们继续!在第一行,我调用 `GetModel` 方法来获取所需的模型。我使用 0 调用此方法,因为此操作在页面首次加载时“渲染”,显示第 1 页的项目。
`GetModel` 方法首先检查传递的页码,如果为 0,则很简单,获取预定义的(`NumberOfPosts`)数量的文章并返回它们。如果不是,我使用一个简单的方法来根据传递的页码获取相关的文章并返回它们。
回到 `PostPartial` 操作方法,一旦收到模型,就会返回一个名为 `PostPartial` 的局部视图,并附带上一步收到的模型。接下来让我们看看这个局部视图!
@model MvcPaging.Models.PostViewModel <div id="content"> @Html.Partial("PostDisplayer", Model) </div> <div id="placeholder"> </div> <div> <a class="btn btn-more" id="moreBtn"> More <img id="moreBtnImg" src="@Url.Content("~/Content/images/loading.gif")" alt="loading..."/> </a> </div> <input type="hidden" id="currentPage" name="currentPage" value="1"/>
这个局部视图是最有趣的之一,因为它为整个想法奠定了基础!第一个部分(`div[id="content"]`)包含调用渲染 `PostDisplayer` 局部视图。这个局部视图会收到一个包含特定数量文章的“模型”。文章列表被迭代,为每篇文章创建一个条目。这是此局部视图的内容:
@model MvcPaging.Models.PostViewModel @foreach (var post in Model.Posts) { <div class="post-entry" id="@string.Format("post-{0}", post.PostId)"> <div> <div class="pull-left" title="@post.PostTitle"> @post.PostTitle </div> <div class="pull-right" style="color: #BDBDBD"> <b>@post.Category</b> </div> </div> <br/><br/><br/> @Html.ActionLink("View", "ViewPost", "Details", new { questionId = post.PostId }, new { @class = "btn btn-primary" }) </div> <hr/> }
如前所述,对于文章列表中的每个条目,我渲染一个 `div`,其中第一行是标题以及类别。然后,在下一行,我显示一个“查看”该条目的按钮。
现在,让我们回到 `PostPartial` 局部视图!请注意 `div[id="placeholder"]`。这个 `div` 将用于加载后续页面的内容,这些内容是通过“更多”按钮加载的。首先,它是空的。最后,最后一个 `div` 代表将用于加载后续页面的“更多”按钮。这部分只显示一个带有“更多”文本的简单按钮。每当单击按钮时,此文本旁边都会出现一个图像,该图像显示直到请求成功完成或失败(参见下图)。到目前为止我们所看到的一切都相当琐碎。因此,让我们进入有趣的部分。每当用户单击“更多”按钮时,我都会使用 ajax 发出异步请求,传递当前的页码(可在 `currentPage` 隐藏变量中找到,初始设置为 1)。
$(function () { showHideMoreBtn('mvcpaging/checkpostsstatus'); $(document).on('click', '#moreBtn', function (e) { e.preventDefault(); var currentPgNumber = parseInt($('#currentPage').val()); $('#moreBtnImg').show(); $.ajax({ type: 'GET', url: siteRoot + 'mvcpaging/loadmoreposts', data: { 'currentPageNumber': currentPgNumber }, dataType: 'html', success: function (data) { $('#placeholder').append(data); $('html, body').animate({ scrollTop: $('#moreBtn').offset().top }, 1000); showHideMoreBtn('mvcpaging/checkpostsstatus'); incrementPageNumber(); $('#moreBtnImg').hide(); }, error: function (a, b, ctx) { $('#moreBtnImg').hide(); alert('error ' + ctx); } }); }); });
示例项目使用 jQuery 2.0.3,它不支持 `.live` 方法,该方法用于绑定事件到将来的元素。因此,我使用了 `.on` 方法来完成相同的任务。在这种情况下,第 4 行意味着,从 `document` 对象开始,即所有内容,通过调用下一个传递的回调来绑定 `click` 事件到 id 为 `moreBtn` 的元素。因此,当单击按钮并引发回调时,事件参数 e 用于防止默认操作,在这种情况下,对于按钮是 click 事件。然后使用隐藏变量 `currentPage` 找到当前页码。然后,我显示“更多”按钮内的图像,以便用户知道正在进行某些活动。
然后,使用 jQuery 提供的 `$.ajax` 方法发出 ajax 请求。`LoadMorePosts` 是一个操作方法,它根据传递的页码查找文章并返回它们。此方法如下所示:
public ActionResult LoadMorePosts(int currentPageNumber) { var model = GetModel(currentPageNumber); return PartialView("PostDisplayer", model); }
一旦方法成功返回局部视图(还记得吗?这与 `PostsPartial` 局部视图中使用的局部视图是同一个),就会调用 `$.ajax` 方法的 `success` 回调。在这里,我只需将返回的 html 附加到 `div[id="placeholder"]` 元素。然后,在下一行,我进行一些简单的动画来滚动到页面底部。在下一行,我调用 `showHideMoreBtn` 方法,将用于发出请求的 url 传递给它,该 url 定义在 `paging-base.js` 文件中。此函数的作用是将当前页码传递给一个名为 `CheckPostsStatus` 的方法,然后使用返回值来确定“更多”按钮是否出现。下面的代码是此方法:
public JsonResult CheckPostsStatus(int currentPageNumber) { var posts = GetPosts(); var totalPages = (int)Math.Ceiling((decimal)posts.Count() / 5); return Json((currentPageNumber + 1) < totalPages, JsonRequestBehavior.AllowGet); }
最后,我只需增加页码,然后隐藏“更多”按钮内的图像。另一方面,如果 ajax 请求失败,图像将被隐藏,然后会显示一个警告,告知用户错误。如果您认为此方法还有更多内容,那您就错了!就是这样,您现在拥有了一个完全正常运行的页面,可以以非传统的方式处理分页!让我们进入下一个方法!
方法 2:使用 Mustache.js 进行分页
在下一个方法中,我将解释如何使用 mustache.js,一个模板引擎,来进行分页。
Mustache 简介
Mustache 非常易于使用!假设您需要向用户打招呼。您可以定义以下模板:
Hello {{name}}
然后,当您传递数据如下时:
{ "name" : "karthik" }
到这个模板,结果如下:
Hello karthik
很简单,不是吗? 这里有一个您可以用来了解更多的 fiddle!为了完整起见,这里是所需的 JavaScript:
$(function(){ var data = { "name": "karthik" }; var template = "Hello {{name}}"; var func = Mustache.compile(template); var output = func(data); $('#content').html(output); });
在这里,第二行定义了所需的数据。在第 2 行,定义了模板。现在在下一行,使用 `Mustache` 工厂方法 *compile*,编译了前面定义的模板,以便以后可以使用。此函数返回一个“函数”,可以通过传递相关数据来调用该函数,这在下一行中完成。当通过传递数据调用此函数时,会返回带有数据的修改后的模板。然后我将其附加到定义的 div。下面是另一个处理数组的示例,这里是 fiddle:
$(function(){ var data = { "countries": [ {"name" : "India"}, {"name" : "USA"}, { "name": "Sweden"}] }; var template = "<ul>{{#countries}}<li>{{name}}</li>{{/countries}}"; var func = Mustache.compile(template); var output = func(data); $('#content').html(output); });
在这种情况下,传递的数据包含一个对象数组。数组中的每个对象都包含一个 `name` 属性,并带有相应的值。对于此模板,mustache 使用 `{{#property_name}}...{{/property_name}}` 来指示后面是列表的模板。在上例中,`countries` 是一个列表(数组)。上面的模板创建一个无序列表,列表中的每个项都是数组中的一项。`{{name}}` 如第一个示例所示,用于打印数组中每个项中对象的值。
回到方法 2!
前面的部分是对 mustache.js 的简短介绍!现在让我解释一下我如何将其投入使用。 `MustachePagingController` 负责此方法所需的代码。首先,`Index` 操作方法仅返回一个视图。在此视图中,`PostsPartial` 局部视图用于显示初始文章集。这已经在第一个方法中讨论过了。代码列表显示了 `Index` 视图:
@{ ViewBag.Title = "Index"; } @section pageScripts { @Scripts.Render("~/bundles/paging-mustache") } @{ Html.RenderAction("PostsPartial", "MustachePaging"); }
为了通过 mustache 模板库处理分页,我在 `paging-mustache.js` 文件中有相关代码。`PostsPartial` 显示“更多”按钮,并且此按钮在 javascript 文件中连接到将后续页面内容附加到 `“placeholder”`,如下所示:
$(function () { showHideMoreBtn('mvcpaging/checkpostsstatus'); $(document).on('click', '#moreBtn', function (e) { e.preventDefault(); var currentPgNumber = parseInt($('#currentPage').val()); $('#moreBtnImg').show(); $.ajax({ type: 'GET', url: siteRoot + 'mustachepaging/loadmoreposts', data: { 'currentPageNumber': currentPgNumber }, dataType: 'json', success: function (data) { var tmpl = Mustache.compile("{{#Posts}}<div class=\"post-entry\" id=\"post-{{PostId}}\"><div><div class=\"pull-left\" title=\"{{PostTitle}}\">{{PostTitle}}</div><div class=\"pull-right\" style=\"color: #BDBDBD\"><b>{{Category}}</b></div></div><br><br><br><a class=\"btn btn-primary\" href=\"/Details/ViewPost?questionId={{PostId}}\">View</a></div><hr/>{{/Posts}}"); var output = tmpl(data); $('#placeholder').append(output); $('html, body').animate({ scrollTop: $('#moreBtn').offset().top }, 1000); showHideMoreBtn('mvcpaging/checkpostsstatus'); incrementPageNumber(); $('#moreBtnImg').hide(); }, error: function (a, b, ctx) { $('#moreBtnImg').hide(); alert('error ' + ctx); } }); }); });
在上一个代码列表中,第 14 到 19 行非常重要。模板是列表中每篇文章条目所需的 html:第一行是文章标题和类别,然后是几行空白,最后是“查看”按钮和水平线,如下图所示。对 `LoadMorePosts` 的调用返回一个文章列表(数组),位于名为 `Posts` 的属性下。这就是为什么在模板字符串中使用格式 `{{#Posts}}...{{/Posts}}` 来指示数组的原因!这也是在 `PostViewModel` 中将文章列表作为 `Posts` 属性返回而不是仅返回列表的原因!
在第 18 行调用 `Mustache.compile` 之后,我们得到一个函数,可以通过传递作为 ajax 调用一部分接收的数据(这是一个列表(数组))来获取最终的 html 结果。一旦收到结果,就会将其附加到 `placeholder`,并且执行与方法 1 类似的步骤。
如果您不喜欢将模板放在 .js 文件中,您可以将其作为页面中的 `script` 标签,如下所示:
<script type="text/template" id="tmpl"> {{#Posts}}<div class=\"post-entry\" id=\"post-{{PostId}}\"><div><div class=\"pull-left\" title=\"{{PostTitle}}\">{{PostTitle}}</div><div class=\"pull-right\" style=\"color: #BDBDBD\"><b>{{Category}}</b></div></div><br><br><br><a class=\"btn btn-primary\" href=\"/Details/ViewPost?questionId={{PostId}}\">View</a></div><hr/>{{/Posts}} </script>
然后,在 .js 文件中,您可以使用以下方式获取此模板:
var tmpl = $('#tmpl').html();
现在,而不是将混乱的代码放在 .js 文件中,它已经保存在变量中了,借助脚本模板!可以使用与以前相同的步骤来编译并替换相关值!
使用方法 2 的优点
使用第二种方法的主要优点之一是限制通过网络传输的数据量。考虑下图:
这张图片来自在 fiddler 中捕获的示例运行。第一个和第三个条目的数据量大致相同,所以我们暂时忽略它们。关于第二和第四个条目,方法 1 的响应大小约为 405 字节。而方法 2 中的相同调用仅需要 198 字节!这大约节省了 50% 的传输数据量!虽然这听起来可能不是什么大事,但它确实是!考虑一下您分页 10 到 15 次会发生什么!现在您大概明白了!节省的任何数据传输都是好事 :) 例如,考虑 html 量大于数据本身的情况。在这种情况下,我猜您可以想象避免了多少数据传输,因为只传输了数据,而不是 html 本身!
还有什么?
现在我们已经看到了两种方法,让我抛出还可以做些什么!首先,可以删除加载初始文章集的这部分,并在文档加载事件期间用另一个方法调用替换它,其中第 0 页的内容可以异步加载,而不是在单击按钮时加载。这将使很多事情变得更简单!
另一个改进是,不必有两个方法——一个用于获取更多数据,一个用于检查“是否”还有更多数据——可以将它们合并为一个方法,通过返回一个包含数据和“更多”按钮状态的对象来实现。
我将这部分留给读者!其他内容包括处理空文章列表、更多的错误处理(而不仅仅是警报消息)等等!
历史
1.0 版本发布