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

RustyLazyLoad:ASP.NET MVC / jQuery 滚动加载器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (2投票s)

2013年7月24日

CPOL

5分钟阅读

viewsIcon

42574

downloadIcon

1253

使用 jQuery 和 ASP.NET MVC 在窗口滚动时加载项目的延迟加载控件。

 

引言

在花了些时间四处浏览但没有找到合适的东西后,我决定编写这个控件。我并不是说现有的延迟加载控件不好,而是我觉得它们要么过于复杂,要么有点难用。 

我只想用一个无序列表 `<ul>` 来显示我的项目列表,并且能够轻松地修改每个列表项 `<li>` 的布局。所以,我认为最合理的方式是利用 jQuery 的 AJAX 功能调用一个返回 PartialView 的 Controller Action,然后用它来填充无序列表的内容。本文将更详细地介绍 RustyLazyLoad 控件是如何实现这一点的。

以下是 RustyLazyLoad 的一些主要优点:

  • 易于使用 
  • jQuery 和 Razor 的清晰分离
  • 可扩展 - 您可以将其绑定到任何对象,不限于无序列表
  • 无需 jQuery 编程

由于此控件旨在简单易用,我们应该意识到它的局限性:

  • 它不知道您的所有项目是否都已加载
  • 需要一个 PartialView 作为项目列表模板(或者我们可以创建一个用于提供所有延迟加载数据的模板)
  • 它与 ASP.NET MVC 和 Razor 紧密绑定
  • 调用非 ASP.NET MVC Action 的情况尚未经过测试
  • 用于延迟加载的服务方法必须具有 `limit` 和 `fromRowNumber` 作为强制参数,以便利用 LINQ 查询的 `.Skip(fromRowNumber)` 和 `.Take(limit)`。

使用代码 

RustyLazyLoad 由六个主要组件组成:

  1. rustylazyload.js
  2. rustylazyload.css
  3. RustyLazyLoadViewModel.cs
  4. _RustyLazyLoad.cshtml
  5. 您的 Controller 延迟加载 Action 方法和相应的 ViewModel
  6. 您的 PartialView 模板

首先,我们快速浏览一下 `rustylazyload.js`。

function LazyLoad(uniqueId) {
    
    var _uniqueId = uniqueId;

    var _containerId = "";
    var _ajaxLoadContainerId = "";
    var _ajaxActionUrl = "";
    var _parameters = {};

    this.init = function(option) {

        _containerId = option.containerId;
        _ajaxLoadContainerId = option.ajaxLoadContainerId;
        _ajaxActionUrl = option.ajaxActionUrl;
        _parameters = option.parameters;

        // Enable scroll event handler
        bindScrollHandler();

        // Load initial items
        load();
    };

    var bindScrollHandler = function() {
        $(window).scroll(function() {
            if ($(window).scrollTop() + $(window).height() > $(document).height() - 200) {
                load();
            }
        });
    };

    var unbindScrollHandler = function() {
        $(window).unbind("scroll");
    };

    var load = function() {
        $.ajax({
            type: "POST",
            url: _ajaxActionUrl,
            data: _parameters,
            beforeSend: load_beforeSend,
            success: load_success,
            error: load_error
        });
    };

    var load_beforeSend = function() {
        // Disable scroll event handler
        unbindScrollHandler();

        // Show loading message
        $(_ajaxLoadContainerId).toggleClass("lazyload-hidden").html("Loading..");
    };
    var load_success = function(result) {

        // Delay a bit before displaying the result and re-enabling scroll event handler
        setTimeout(function() {
            // Display result with fade in effect
            if (result != null && result != "") {
                $(_containerId).append(result, { duration: 500 });
                // Add ui-first-child to the first child
                $(_containerId).find(">:first-child").removeClass("ui-first-child");
                $(_containerId).find(">:first-child").addClass("ui-first-child");
                // Remove ui-last-child from the old last child
                $(_containerId).find(">:nth-child(" + _parameters.fromRowNumber + ")").removeClass("ui-last-child");
                // Add ui-last-child to the new last child
                $(_containerId).find(">:last-child").addClass("ui-last-child");

                // Update fromRowNumber
                _parameters.fromRowNumber = $(_containerId).children().length;
            }

            if (_parameters.fromRowNumber == 0) {
                // Use loading container to display 'no item' message
                $(_ajaxLoadContainerId).html("There is no data to display");
            } else {
                // Remove loading message
                $(_ajaxLoadContainerId).toggleClass("lazyload-hidden").html("");
            }

            // Re-enable scroll handler
            bindScrollHandler();
        }, 500);

    };
    var load_error = function(result) {
        var message = result.responseText.substring(1, result.responseText.length - 2);
        $(_ajaxLoadContainerId).html("Error: " + message);
    };
} 

调用 `init()` 时,我们需要指定 4 个强制字段:

  • _containerId - 数据容器对象的 ID(`<ul id="thisId"></ul>`)
  • _ajaxLoadContainerId - “加载中”消息容器对象的 ID(`<div id="thisId">Loading..</div>`)
  • _ajaxActionUrl - 将使用 `$.ajax()` 调用 Action 的 URL
  • _parameters - 一个 JSON 对象,包含 2 个强制字段:`limit`(按需加载的项目数量)和 `fromRowNumber`(标记已加载项目的 N 条,以避免重复条目)。

我们不会逐行讨论上面的代码,而是重点介绍重要部分。

  • `init()` 函数执行三项操作:映射参数,将滚动事件处理程序绑定到窗口,并调用 `load()` 来显示第一批数据。
  • `bindScrollHandler()` 非常简单——它只是确保在窗口接近底部时调用 `load()`。
  • `load()` 使用 jQuery AJAX 调用 `_ajaxActionUrl`,并将 `_parameters` 变量中指定的所有参数传递过去——ASP.NET MVC 会智能地将这些参数与 Controller Action 参数进行匹配。
  • 当 Controller Action 执行时,`load_beforeSend()` 会暂时禁用窗口滚动事件处理程序,这样我们就不会因为过多的 AJAX 请求而使服务器过载,同时显示存储在 `_ajaxLoadContainerId` 中的加载消息 HTML 对象。
  • 成功时,`load_success()` 应该将结果绑定到 `_containerId` HTML 对象,用加载的项目数更新 `_parameters.fromRowNumber`(记住 `fromRowNumber` 是 `_parameters` 的强制项之一),并重新启用窗口滚动事件处理程序。
  • 任何错误都将在 `load_error()` 中处理,并在 `_ajaxLoadContainerId` HTML 对象中显示。
  • 如果您使用的是默认的 ASP.NET MVC4 移动应用程序模板,您应该不需要修改此文件。

接下来是 `rustylazyload.css`,它应该相当直接。

.lazyload-loading-container {
    margin: 0;
    padding: 15px;
    text-align: center;
}
.lazyload-hidden {
    display: none;
}

现在是 ViewModel `RustyLazyLoadViewModel.cs`。

using System.Collections.Generic;

namespace RustyLazyLoadTester.Mobile.Models
{
    public class RustyLazyLoadViewModel
    {
        public RustyLazyLoadViewModel()
        {
            Parameters = new Dictionary<string, object>();
        }
        public RustyLazyLoadViewModel(int limit, int fromRowNumber, string containerId,
            string ajaxActionUrl, IDictionary<string, object> parameters = null)
        {
            Limit = limit;
            FromRowNumber = fromRowNumber;
            ContainerId = containerId;
            AjaxActionUrl = ajaxActionUrl;
            if (parameters != null)
                Parameters = parameters;
        }

        public int Limit { get; set; }
        public int FromRowNumber { get; set; }
        public string ContainerId { get; set; }
        public string AjaxActionUrl { get; set; }
        public IDictionary<string, object> Parameters { get; set; }
    }
}

正如您所见,这个 ViewModel 捕获的参数与 `rustylazyload.js` 的 `.init()` 函数几乎相同,只是缺少 `_ajaxLoadContainerId`。为什么呢?让我们看看 View 文件。

_RustyLazyLoad.cshtml:

@using System.Text
@model RustyLazyLoadTester.Mobile.Models.RustyLazyLoadViewModel
@{
    var containerId = Model.ContainerId;
    var ajaxLoadContainerId = string.Format("{0}Load", containerId);
 
    // Convert parameters to JSON
    var sbParameters = new StringBuilder();
    if (Model.Parameters != null && Model.Parameters.Any())
    {
        foreach (var parameter in Model.Parameters)
        {
            sbParameters.AppendFormat("\"{0}\": \"{1}\", ", parameter.Key, parameter.Value);
        }
    }
    var parameters = sbParameters.ToString();
    // Remove trailing ', ' from parameters
    if (!string.IsNullOrWhiteSpace(parameters))
    {
        parameters = parameters.Substring(0, parameters.Length - 2);
    }
}
<ul id="@containerId" data-role="listview" 
             data-inset="true"></ul>
<div id="@ajaxLoadContainerId"
     class="lazyload-loading-container lazyload-hidden
            ui-listview ui-listview-inset
            ui-corner-all ui-shadow ui-li-static
            ui-btn-down-b ui-first-child ui-last-child"></div>
<script type="text/javascript">
    $(document).ready(function () {
        var limit = @Model.Limit;
        var fromRowNumber = @Model.FromRowNumber;
        var containerId = '@string.Format("#{0}", containerId)';
        var ajaxLoadContainerId = '@string.Format("#{0}", ajaxLoadContainerId)';
        var ajaxActionUrl = '@Model.AjaxActionUrl';
        var parameters = { limit: limit, fromRowNumber: fromRowNumber, @Html.Raw(parameters) };
 
        var lazyLoad = new LazyLoad(containerId);
        lazyLoad.init({
            containerId: containerId,
            ajaxLoadContainerId: ajaxLoadContainerId,
            ajaxActionUrl: ajaxActionUrl,
            parameters: parameters
        });
    });
</script>

为了简单起见,`_ajaxLoadContainerId` 实际上是 `_containerId` 加上一个后缀,但实际上它可以是任何值。如果我们觉得需要手动指定 AJAX 加载消息容器 ID,我们只需要在 `RustyLazyLoadViewModel.cs` 中添加 `AjaxLoadContainerId` 属性,并将其传递给变量 `ajaxLoadContainerId`(本页第 5 行)。

延迟加载项容器是这个:

<ul id="@containerId" data-role="listview" data-inset="true"></ul> 

延迟加载加载消息容器是这个:

<div id="@ajaxLoadContainerId" ...></div>  

然后,我们使用 Razor 引擎将参数转换为 JSON,并将其传递给延迟加载控件。

var parameters = { limit: limit, fromRowNumber: fromRowNumber, @Html.Raw(parameters) };
 
var lazyLoad = new LazyLoad(containerId);
lazyLoad.init({
    containerId: containerId,
    ajaxLoadContainerId: ajaxLoadContainerId,
    ajaxActionUrl: ajaxActionUrl,
    parameters: parameters
}); 

最后,第五个和第六个组件通过一个例子来解释最好。

假设数据库中有 15 条 `User` 条目,具有以下字段:`Id, FirstName, LastName, Status`,并映射到下面的模型。我们希望使用延迟加载控件以渐进式的方式在主页上显示这些条目。

using System.ComponentModel;

namespace RustyLazyLoadTester.Mobile.Services.Models
{
    public class User
    {
        public User() { }
        public User(long id, string firstName, string lastName, UserStatus status)
            : this()
        {
            Id = id;
            FirstName = firstName;
            LastName = lastName;
            Status = status;
        }

        public long Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public UserStatus Status { get; set; }
    }

    public enum UserStatus
    {
        [Description("All")]
        All = 0,
        [Description("Inactive")]
        Inactive = 1,
        [Description("Active")]
        Active = 2,
        [Description("Deactivated")]
        Deactivated = 3
    }
}

我们需要做的第一件事是创建服务方法:

using System.Collections.Generic;
using System.Linq;
using RustyLazyLoadTester.Mobile.Services.Models;

namespace RustyLazyLoadTester.Mobile.Services
{
    public interface IQueryService
    {
        IEnumerable<User> GetAllUsers(UserStatus status = UserStatus.All,
                                      int limit = 0, int fromRowNumber = 0);
    }

    class QueryService : IQueryService
    {
        public IEnumerable<User> GetAllUsers(UserStatus status, int limit, int fromRowNumber)
        {
            // Assume we have 15 users
            var users = new List<User>();
            for (var i = 0; i < 15; i++)
            {
                var userFirstName = string.Format("firstName_{0}", i);
                var userLastName = string.Format("lastName_{0}", i);
                var userStatus = i % 2 == 0 ? UserStatus.Active : UserStatus.Inactive;
                users.Add(new User(i, userFirstName, userLastName, userStatus));
            }

            if (limit <= 0)
            {
                users = users.Where(x => x.Status == status)
                            .Skip(fromRowNumber)
                            .ToList();
            }
            else
            {
                users = users.Where(x => x.Status == status)
                            .Skip(fromRowNumber)
                            .Take(limit)
                            .ToList();
            }
            return users;
        }
    }
}

在我们的 `HomeController` 中,我们需要为 `Index` 页面创建默认的 `[HttpGet]` Controller Action 方法 `Index()`,以及用于服务延迟加载器的 `[HttpPost]` Controller Action 方法 `GetNextUsers()`。

using System;
using System.Linq;
using System.Net;
using System.Web.Mvc;
using RustyLazyLoadTester.Mobile.Services;
using RustyLazyLoadTester.Mobile.Services.Models;
 
namespace RustyLazyLoadTester.Mobile.Controllers
{
    public class HomeController : Controller
    {
        private readonly IQueryService _query;

        public HomeController()
        {
            _query = new QueryService();
        }

        [HttpGet]
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult GetNextUsers(UserStatus status, int limit, int fromRowNumber)
        {
            try
            {
                var users = _query.GetAllUsers(status, limit, fromRowNumber);
 
                if (!users.Any())
                    return Json(string.Empty);
 
                return PartialView("_UserList", users);
            }
            catch (Exception ex)
            {
                Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                return Json(ex.Message);
            }
        }
    }
}

在 `Index.cshtml`(对应于 `[HttpGet]` Controller Action 方法 `Index()` 的 View)中,我们会有类似这样的内容:

@using RustyLazyLoadTester
@using RustyLazyLoadTester.Mobile.Models
@using RustyLazyLoadTester.Mobile.Services.Models
@{
    ViewBag.PageTitle = "Home";
    ViewBag.Title = string.Format("RustyLazyLoadTester - {0}", ViewBag.PageTitle);
    
    var parameters = new Dictionary<string, object>();
    parameters.Add("status", UserStatus.All);
}
@Scripts.Render("~/bundles/lazyload") @* points to /Scripts/rustylazyload.js *@
 
@Html.Partial("_RustyLazyLoad", new RustyLazyLoadViewModel(
    5, 0, "ulUsers", Url.Action("GetNextUsers", "Home"), parameters))

那里的两条粗体线将激活延迟加载控件,并在需要时触发 `GetNextUsers()`。

如果我们仔细查看第二条粗体线:

@Html.Partial("_RustyLazyLoad", new RustyLazyLoadViewModel(
    5, 0, "ulUsers", Url.Action("GetNextUsers", "Home"), parameters)) 

值 5 是 **limit**。这决定了每次加载检索多少个项目。值 0 是 **fromRowNumber**。这表示结果中需要忽略的第 N 个项目。随着加载更多数据,这个数字将根据已加载的项目而增加,因此我们不必担心重复项(除非我们的代码涉及复杂的排序,这可能导致新项目出现在列表中间)。

当调用 `GetNextUsers()` 方法时,它只是渲染下面的 PartialView `_UserList.cshtml`。

@using Humanizer
@using RustyLazyLoadTester.Mobile.Services.Models
@model IEnumerable<User>
 
@foreach (var user in Model)
{
    <li class="ui-li ui-li-static ui-btn-up-b">
        <div>@string.Format("First name: {0}", user.FirstName)</div>
        <div>@string.Format("Last name: {0}", user.LastName)</div>
        <div>@string.Format("Status: {0}", user.Status.Humanize())</div>
        <div>---</div>
        <div>---</div>
        <div>---</div>
        <div>---</div>
        <div>---</div>
        <div>---</div>
        <div>---</div>
        <div>---</div>
        <div>---</div>
        <div>---</div>
        <div>---</div>
        <div>---</div>
        <div>---</div>
        <div>---</div>
    </li>
}

请注意,内容被包装在 `<li>` 中。原因是父容器(`_containerId` HTML 对象)是一个 `<ul>`。但我们可以随时轻松地更改此实现,只要我们保持以下层次结构:

<parentContainer>
    <childContainer>
        [Content]
    </childContainer>
</parentContainer> 

这是因为 RustyLazyLoad 控件使用父容器的子节点数量来更新 `_fromRowNumber` 属性,这确保了下一次加载不会有重复条目。

以下是结果:

First load

First load

Next load

© . All rights reserved.