30 октября 2011 г.

Подгрузка контента при прокрутке страницы, для ASP.Net MVC

Большинство современных сайтов стремятся к максимальной динамичности. Считается, что чем реже страничка полностью обновляется, тем работа с сайтом удобнее для конечного пользователя.

Сегодня мы будем делать динамическую подгрузку данных на страницу, по мере ее прокрутки скроллом вниз. Это должно заменить для нас нудные числовые панели навигации, типа такой:

Вообщем, наша цель получить такую себе "бесконечную страничку".  Уточню лишь, что "бесконечная" - в нашем случае, это пока в базе не закончатся данные для показа пользователю.

Обрисуем ситуацию.
При запуске проекта, на главной странице мы увидим список продуктов. Изначально список будет ограничен определенным количеством элементов. При прокрутке страницы с помощью скролла вниз, а именно по достижении конца списка, динамически будет подгружена новая часть продуктов. И так будет происходить до тех пор, пока не будет достигнут последний продукт в базе данных.

Пойдем по-порядку.

Начнем с того, что создадим строго типизированный контрол, который будет представлять один продукт из БД. Назовем его _Product.ascx:
   1:  <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Product>" %>
   2:  <%@ Import Namespace="NorthwindMvcApplication.Models" %>
   3:  <div style="border: 1px solid Grey; padding: 10px; margin-top: 20px;" class="product"
   4:      id="<%= Model.ProductID %>">
   5:      <div class="display-field">
   6:          ProductName:<b>
   7:              <%: Model.ProductName %></b></div>
   8:      <div class="display-field">
   9:          QuantityPerUnit:<b>
  10:              <%: Model.QuantityPerUnit %></b></div>
  11:      <div class="display-field">
  12:          UnitPrice:<b>
  13:              <%: String.Format("{0:F}", Model.UnitPrice) %></b></div>
  14:  </div>

Почему именно контрол? Потому, что так проще представить единицу продукции. Тем более, мы будем его использовать как шаблон для подгрузки новых данных.

Теперь необходимо реализовать вывод изначального списка продуктов на главную страницу.
В контроллере будем брать из базы данных первых 10 записей:
   1:  public ActionResult Index()
   2:  {
   3:      NorthwindDataContext dbContext = new NorthwindDataContext();
   4:   
   5:      var products = dbContext.Products.OrderBy(p => p.ProductID).Take(10);
   6:   
   7:      return View(products);
   8:  }

Моделью для представления Home является список продуктов: IEnumerable<Product>. В разметке представления мы проходим по списку модели, и отрисовываем для каждого его элемента созданный ранее контрол:
   1:  <h2>
   2:      Products</h2>
   3:  <div id="productsDiv">
   4:      <% foreach (var product in Model)
   5:          {%>
   6:      <% Html.RenderPartial("_Product", product); %>
   7:      <%} %>
   8:  </div>

С главной страницей закончили. Теперь на ней можно увидеть следующую картину:

10 первых продуктов появляются при старте.

Нужно реализовать самое главное. Для этого, необходимо в Javascript-е отловить событие достижения нижнего края страницы, и в этот момент выполнить AJAX-запрос, который получить новую часть контента, и вставит его в конец общего списка продуктов.

Новая часть продуктов не должна выделяться из общего списка, поэтому, для ее отображения мы воспользуемся контролом _Product.ascx. Но ведь _Product.ascx отражает всего один продукт, а нам нужно подгрузить их несколько?! Для решения этой проблемы мы создадим еще один контрол _ProductsPart.ascx:
   1:  <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl
<IEnumerable<Product>>" %>
   2:  <%@ Import Namespace="NorthwindMvcApplication.Models" %>
   3:  <% foreach (var p in Model)
   4:     {%>
   5:  <% Html.RenderPartial("_Product", p); %>
   6:  <%} %>

Думаю, тут все понятно. Этот контрол получит набор продуктов, и для каждого из них вставит наш первый контрол. Таким образом мы получим несколько контролов в одном.

Напишем метод контроллера, для заполнения модели последнего контрола. Метод будет срабатывать после POST-запроса через AJAX, и возвращать частичное представление в виде контрола _ProductsPart.ascx:
   1:  [HttpPost]
   2:  public ActionResult ProductsPart(int lastId)
   3:  {
   4:      NorthwindDataContext dbContext = new NorthwindDataContext();
   5:   
   6:      var productsPart = dbContext.Products.OrderBy(p => p.ProductID).
Where(p => p.ProductID > (int)lastId).Take(2);
   7:   
   8:      return PartialView("_ProductsPart", productsPart);
   9:  }

ProductsPart принимает один параметр - ID последнего показанного продукта. Нам необходимо знать его, чтобы взять следующие 2 продукта из базы.

Как всегда, за нами остался лишь Javascript для работы на клиенте.
После загрузки объектной модели страницы мы инициализируем событие прокрутки скроллом. В обработчике отлавливаем момент достижения нижнего предела. После этого узнаем ID последнего div-а на странице (он же будет и ID последнего показанного продукта), и вызываем функцию подгрузки новой партии данных, передав в нее полученный айдишник:
   1:  $(document).ready(function () {
   2:      $(window).scroll(function () {
   3:          if (($(window).scrollTop() + 1) ==
($(document).height() - $(window).height())) {
   4:              var lastProductId = $('#productsDiv div.product:last').attr('id');
   5:              GetProducts(lastProductId);
   6:          };
   7:      });  
   8:  });

А вот, собственно, и сам метод получения новой партии данных:
   1:  function GetProducts(lastId) {
   2:      $('div#lastPostsLoader').html('<img src="/Content/bigLoader.gif" />');
   3:      $.ajax({
   4:          type: 'POST',
   5:          url: '/home/ProductsPart',
   6:          data: 'lastId=' + lastId,
   7:          dataType: "html",
   8:          success: function (result) {
   9:              var domElement = $(result);
  10:              $('#productsDiv').append(domElement);
  11:          }
  12:      });
  13:      $('div#lastPostsLoader').empty();
  14:  };

Тут мы делаем ajax-запрос  в контроллер передав в качестве параметра последний ID. Полученные из контроллера результат приводим к DOM-обьекту, и вставляем его вконец div-а продуктов. Ну и пока все это дело происходит - показываем пользователю крутящеюся анимацию для визуализации процесса получения новых данных.

Вот и все.
Пачка новых данных получается после достижения конца страницы с помощью скролла.
Можно проверять.
_____
Исходники