X.PagedList e Knockout JS
Oggi utilizziamo il pacchetto NuGet X.PagedList e Knockout js per crare una griglia con paginazione caricata completamente in ajax per un progetto ASP.NET MVC.
Cominciamo con creare un progetto ASP.NET MVC e installiamo i seguenti pacchetti NuGet e tutte le loro dipendenze:
- X.PagedList.MVC v. 5.3.0.5300
- knockoutjs v. 3.4.2
- jQuery v. 3.1.1
Per caricare una griglia ed effettuare una paginazione avremo bisogno di una base dati. Per semplicità utilizzeremo un IEnumerable generato al momento per simulare una base dati tuttavia lo stesso identico codice funziona con una base dati IQueryable interrogata con EF.
Se non lo avete nel progetto create il controller "ValuesController" e inserite il seguente codice:
using System.Linq; using System.Web.Mvc; using X.PagedList; namespace PaginazioneAjax.Controllers { public class ValuesController : Controller { // Fisso la page size private readonly int PageSize = 10; // Questa action ci permetterà di caricare la view "Index.cshtml" public ActionResult Index() { return View(); } // Il metodo che chiameremo con jQuery public JsonResult Ajax(int? page = 1) { // Il datasource fittizio var source = Enumerable.Range(1, 1000).Select(p => new { Index = p, Text = $"Elemento {p}" }); // Qua utilizziamo la libreria X.PagedList var paged = source.ToPagedList(page.GetValueOrDefault(), 10); // Costruiamo un JSON contenente la pagina da visualizzare e alcuni metadati sulla paginazione return Json(new { source = paged.ToArray(), pager = paged.GetMetaData() }, JsonRequestBehavior.AllowGet); } } }
Il codice mostreremo per la parte di UI e javascript è una versione adattata a knockoutjs di un esempio presente nella libreria X.PagedList. Per far funzionare il tutto assicuratevi che le librerie jQuery e knockoutjs siano referenziate nella vostra pagina html. Di seguito il codice da inserire nella view Index di Values:
@{ ViewBag.Title = "Index"; } <h2>Index</h2> <!-- Inizio della tabella con le testate fisse --> <table class="table"> <thead> <tr> <th> Row </th> <th> Title </th> </tr> </thead> <!-- Qui bindiamo con ko il datasource che recuperiamo con una chiamata Ajax al controller--> <tbody data-bind="foreach: source"> <tr> <td data-bind="text: Index"></td> <td data-bind="text: Text"></td> </tr> </tbody> </table> <!-- Qui costruiamo la paginazione, non abbiamo fatto nessuno sforzo per migliorare il codice presente nella libreria, ci siamo limitati ad adattarlo a ko --> <ul class="pagination"> <li data-bind="if: pager.IsFirstPage() " class="disabled"><a>««</a></li> <li data-bind="ifnot: pager.IsFirstPage() "><a href="/values/ajax?page=1">««</a></li> <li data-bind="if: pager.HasPreviousPage() "><a data-bind="attr: { href : '/values/ajax?page=' + pager.PreviousPageNumber() }">«</a></li> <li data-bind="ifnot: pager.HasPreviousPage() " class="disabled"><a>«</a></li> <li data-bind="ifnot: pager.FirstPageIsVisible() " class="disabled"><a>...</a></li> <!-- ko foreach: pager.Pages --> <li data-bind="if: Selected" class="active"><a data-bind="text: PageNumber"></a></li> <li data-bind="ifnot: Selected"><a data-bind="attr: { href : '/values/ajax?page=' + PageNumber }, text: PageNumber "></a></li> <!-- /ko --> <li data-bind="ifnot: pager.LastPageIsVisible() " class="disabled"><a>...</a></li> <li data-bind="if: pager.HasNextPage() "><a data-bind="attr: { href : '/values/ajax?page=' + pager.NextPageNumber() }">»</a></li> <li data-bind="ifnot: pager.HasNextPage() " class="disabled"><a>»</a></li> <li data-bind="if: pager.IsLastPage() " class="disabled"><a>»»</a></li> <li class="next" data-bind="ifnot: pager.IsLastPage() "><a data-bind="attr: { href : '/values/ajax?page=' + pager.PageCount() }">»»</a></li> </ul> @section Scripts{ <script type="text/javascript"> // costruzione del view model da bindare con ko, utilizziamo numerosi observable in modo che ad ogni // cambiamento della struttura dati cambi anche la UI var viewModel = function () { this.source = ko.observable(); this.pager = { Pages: ko.observableArray(), IsFirstPage: ko.observable(), IsLastPage: ko.observable(), HasPreviousPage: ko.observable(), HasNextPage: ko.observable(), LastPageIsVisible: ko.observable(), FirstPageIsVisible: ko.observable(), NextPageNumber: ko.observable(), PreviousPageNumber: ko.observable(), PageCount: ko.observable() }; } var myModel = new viewModel(); ko.applyBindings(myModel); // funzione per recuperare i dati dal server var fetchData = function (url) { $.get({ url: url, cache: false }).then(function (data) { // un po' di conti per gestire la paginazione var start, end; var numberOfPagesToShowAtOnce = 10; var halfOfPagesToShowAtOnce = Math.floor(numberOfPagesToShowAtOnce / 2); start = data.pager.PageNumber - halfOfPagesToShowAtOnce; if (start < 1) start = 1; if ((start + numberOfPagesToShowAtOnce) > data.pager.PageCount) end = data.pager.PageCount; else end = start + numberOfPagesToShowAtOnce; myModel.pager.Pages.removeAll(); for (var i = start; i <= end; i++) { myModel.pager.Pages.push({ PageNumber: i, Selected: i === data.pager.PageNumber }); } myModel.pager.FirstPageIsVisible(start === 1); myModel.pager.LastPageIsVisible(end === data.pager.PageCount); myModel.pager.PreviousPageNumber(data.pager.PageNumber - 1); myModel.pager.NextPageNumber(data.pager.PageNumber + 1); myModel.pager.HasNextPage(data.pager.HasNextPage); myModel.pager.HasPreviousPage(data.pager.HasPreviousPage); myModel.pager.IsFirstPage(data.pager.IsFirstPage); myModel.pager.IsLastPage(data.pager.IsLastPage); myModel.pager.PageCount(data.pager.PageCount); myModel.source(data.source); // al click sugli elementi della paginazione andiamo ad effettuare una chiamata al controller $('ul.pagination li a').click(function (event) { event.preventDefault(); var newPageUrl = $(event.target).attr('href'); if (newPageUrl) { fetchData(newPageUrl); } }); }); } // prima chiamata per costruire la pagina al primo caricamento fetchData("/values/ajax"); </script> }
Sarebbe interessante modificare il codice per gestire eventuali filtri di ricerca da passare in query string al controller. Ma questo lo lasciamo ad un'altra volta!