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!

comments powered by Disqus