Implementare una suddivisione in pagine efficiente dei dati

da Microsoft

Scarica il PDF

Questo è il passaggio 8 di un'esercitazione gratuita sull'applicazione "NerdDinner" che illustra come creare un'applicazione Web di piccole dimensioni, ma completa, usando ASP.NET MVC 1.

Il passaggio 8 illustra come aggiungere il supporto di paging all'URL /Dinners in modo che invece di visualizzare 1000 cena in una sola volta, verranno visualizzati solo 10 cene imminenti alla volta e consentire agli utenti finali di tornare e inoltrare l'intero elenco in modo descrittivo SEO.

Se si usa ASP.NET MVC 3, è consigliabile seguire le esercitazioni di Introduzione With MVC 3 o MVC Music Store.

NerdDinner Passaggio 8: Supporto per il paging

Se il nostro sito ha successo, avrà migliaia di cene imminenti. È necessario assicurarsi che le scalabilità dell'interfaccia utente vengano gestite tutte queste cene e consenta agli utenti di sfogliarle. Per abilitare questa operazione, si aggiungerà il supporto per il paging all'URL /Dinners in modo che invece di visualizzare 1000 cena in una sola volta, verranno visualizzati solo 10 cena imminenti alla volta e consentire agli utenti finali di tornare e inoltrare l'intero elenco in modo descrittivo SEO.

Metodo di azione Index() Recap

Il metodo di azione Index() all'interno della classe DinnersController è attualmente simile al seguente:

//
// GET: /Dinners/

public ActionResult Index() {

    var dinners = dinnerRepository.FindUpcomingDinners().ToList();
    return View(dinners);
}

Quando viene effettuata una richiesta all'URL /Dinners , recupera un elenco di tutte le prossime cene e quindi esegue il rendering di un elenco di tutti:

Screenshot della pagina Elenco cena imminente nerd.

Informazioni su IQueryable<T>

Iqueryable<T> è un'interfaccia introdotta con LINQ come parte di .NET 3.5. Consente potenti scenari di "esecuzione posticipata" che è possibile sfruttare per implementare il supporto per il paging.

Nella nostra cenaRepository viene restituita una sequenza IQueryable<> Dinner dal metodo FindUpcomingDinners():

public class DinnerRepository {

    private NerdDinnerDataContext db = new NerdDinnerDataContext();

    //
    // Query Methods

    public IQueryable<Dinner> FindUpcomingDinners() {
    
        return from dinner in db.Dinners
               where dinner.EventDate > DateTime.Now
               orderby dinner.EventDate
               select dinner;
    }

L'oggetto IQueryable<> Dinner restituito dal metodo FindUpcomingDinners() incapsula una query per recuperare gli oggetti Dinner dal database usando LINQ to SQL. Importante, non eseguirà la query sul database fino a quando non si tenta di accedere/eseguire l'iterazione dei dati nella query o fino a quando non si chiama il metodo ToList(). Il codice che chiama il metodo FindUpcomingDinners() può facoltativamente scegliere di aggiungere altre operazioni o filtri "concatenati" all'oggetto IQueryable<> Dinner prima di eseguire la query. LINQ to SQL è quindi abbastanza intelligente per eseguire la query combinata sul database quando vengono richiesti i dati.

Per implementare la logica di paging, è possibile aggiornare il metodo di azione Index() di DinnersController in modo da applicare operatori aggiuntivi "Skip" e "Take" alla sequenza IQueryable<> Dinner restituita prima di chiamare ToList() su di esso:

//
// GET: /Dinners/

public ActionResult Index() {

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();
    var paginatedDinners = upcomingDinners.Skip(10).Take(20).ToList();

    return View(paginatedDinners);
}

Il codice precedente ignora le prime 10 cene imminenti nel database e quindi torna indietro 20 cene. LINQ to SQL è abbastanza intelligente per costruire una query SQL ottimizzata che esegue questa logica di ignoramento nel database SQL e non nel server Web. Ciò significa che anche se nel database sono presenti milioni di cena imminenti, solo i 10 che vogliamo verranno recuperati come parte di questa richiesta (rendendolo efficiente e scalabile).

Aggiunta di un valore "page" all'URL

Invece di scrivere un intervallo di pagine specifico, si vuole che gli URL includano un parametro "page" che indica quale intervallo di cena richiede un utente.

Uso di un valore querystring

Il codice seguente illustra come aggiornare il metodo di azione Index() per supportare un parametro querystring e abilitare URL come /Dinners?page=2:

//
// GET: /Dinners/
//      /Dinners?page=2

public ActionResult Index(int? page) {

    const int pageSize = 10;

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();

    var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)
                                          .Take(pageSize)
                                          .ToList();

    return View(paginatedDinners);
}

Il metodo di azione Index() precedente ha un parametro denominato "page". Il parametro viene dichiarato come intero nullable , ovvero che cos'è int? indica. Ciò significa che l'URL /Dinners?page=2 causerà il passaggio di un valore "2" come valore del parametro. L'URL /Dinners (senza un valore querystring) causerà il passaggio di un valore Null.

Si moltiplica il valore della pagina in base alle dimensioni della pagina (in questo caso 10 righe) per determinare il numero di cene da ignorare. Si usa l'operatore "coalescing" null C# (??) utile quando si gestiscono tipi nullable. Il codice precedente assegna alla pagina il valore 0 se il parametro della pagina è Null.

Uso di valori URL incorporati

Un'alternativa all'uso di un valore querystring consiste nell'incorporare il parametro di pagina all'interno dell'URL effettivo stesso. Ad esempio: /Dinners/Page/2 o /Dinners/2. ASP.NET MVC include un potente motore di routing degli URL che semplifica il supporto di scenari come questo.

È possibile registrare regole di routing personalizzate che esegue il mapping di qualsiasi URL o formato URL in ingresso a qualsiasi classe controller o metodo di azione desiderato. Tutto ciò che dobbiamo fare è aprire il file Global.asax all'interno del progetto:

Screenshot dell'albero di navigazione Nerd Dinner. Il punto globale di una x è selezionato e evidenziato.

Registrare quindi una nuova regola di mapping usando il metodo helper MapRoute() come la prima chiamata alle route. MapRoute() seguente:

public void RegisterRoutes(RouteCollection routes) {

   routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(                                        
        "UpcomingDinners",                               // Route name
        "Dinners/Page/{page}",                           // URL with params
        new { controller = "Dinners", action = "Index" } // Param defaults
    );

    routes.MapRoute(
        "Default",                                       // Route name
        "{controller}/{action}/{id}",                    // URL with params
        new { controller="Home", action="Index",id="" }  // Param defaults
    );
}

void Application_Start() {
    RegisterRoutes(RouteTable.Routes);
}

Sopra viene registrata una nuova regola di routing denominata "ImminenteDinners". Si indica che ha il formato URL "Dinners/Page/{page}" - dove {page} è un valore di parametro incorporato all'interno dell'URL. Il terzo parametro del metodo MapRoute() indica che è necessario eseguire il mapping degli URL che corrispondono a questo formato al metodo di azione Index() nella classe DinnersController.

È possibile usare lo stesso codice Index() esatto precedentemente usato con lo scenario querystring, ad eccezione del parametro "page" dall'URL e non dalla querystring:

//
// GET: /Dinners/
//      /Dinners/Page/2

public ActionResult Index(int? page) {

    const int pageSize = 10;

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();
    
    var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)
                                          .Take(pageSize)
                                          .ToList();

    return View(paginatedDinners);
}

E ora quando si esegue l'applicazione e si digita in /Dinners verranno visualizzati i primi 10 cena imminenti:

Screenshot dell'elenco Cena nerd imminenti.

E quando digitamo /Dinners/Page/1 visualizzeremo la prossima pagina delle cene:

Screenshot della pagina successiva dell'elenco Prossimi cena.

Aggiunta dell'interfaccia utente di spostamento pagina

L'ultimo passaggio per completare lo scenario di paging consiste nell'implementare l'interfaccia utente di spostamento "successiva" e "precedente" all'interno del modello di visualizzazione per consentire agli utenti di ignorare facilmente i dati di Cena.

Per implementare correttamente questa operazione, è necessario conoscere il numero totale di cene nel database, nonché il numero di pagine di dati che si traduce. Sarà quindi necessario calcolare se il valore "page" richiesto è all'inizio o alla fine dei dati e mostrare o nascondere l'interfaccia utente "precedente" e "successiva". È possibile implementare questa logica all'interno del metodo di azione Index(). In alternativa, è possibile aggiungere una classe helper al progetto che incapsula questa logica in modo più utilizzabile.

Di seguito è riportata una semplice classe helper "PaginatedList" che deriva dalla classe di raccolta List<T> incorporata in .NET Framework. Implementa una classe di raccolta rieseguibile che può essere usata per impaginare qualsiasi sequenza di dati IQueryable. Nell'applicazione NerdDinner sarà possibile usare i risultati di IQueryable Dinner, ma potrebbe essere usato facilmente in IQueryable Product o IQueryable<><< Customer> risultati in altri scenari dell'applicazione:>

public class PaginatedList<T> : List<T> {

    public int PageIndex  { get; private set; }
    public int PageSize   { get; private set; }
    public int TotalCount { get; private set; }
    public int TotalPages { get; private set; }

    public PaginatedList(IQueryable<T> source, int pageIndex, int pageSize) {
        PageIndex = pageIndex;
        PageSize = pageSize;
        TotalCount = source.Count();
        TotalPages = (int) Math.Ceiling(TotalCount / (double)PageSize);

        this.AddRange(source.Skip(PageIndex * PageSize).Take(PageSize));
    }

    public bool HasPreviousPage {
        get {
            return (PageIndex > 0);
        }
    }

    public bool HasNextPage {
        get {
            return (PageIndex+1 < TotalPages);
        }
    }
}

Si noti sopra come calcola e quindi espone le proprietà come "PageIndex", "PageSize", "TotalCount" e "TotalPages". Espone inoltre due proprietà helper "HasPreviousPage" e "HasNextPage" che indicano se la pagina dei dati nella raccolta è all'inizio o alla fine della sequenza originale. Il codice precedente causerà l'esecuzione di due query SQL, ovvero il primo per recuperare il numero totale di oggetti Dinner (questo non restituisce gli oggetti , invece esegue un'istruzione "SELECT COUNT" che restituisce un intero) e il secondo per recuperare solo le righe di dati necessari dal database per la pagina corrente dei dati.

È quindi possibile aggiornare il metodo helper DinnersController.Index() per creare una cena PaginatedList<> dal risultato DinnerRepository.FindUpcomingDinners() e passarla al modello di visualizzazione:

//
// GET: /Dinners/
//      /Dinners/Page/2

public ActionResult Index(int? page) {

    const int pageSize = 10;

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();
    var paginatedDinners = new PaginatedList<Dinner>(upcomingDinners, page ?? 0, pageSize);

    return View(paginatedDinners);
}

È quindi possibile aggiornare il modello di visualizzazione \Views\Dinners\Index.aspx per ereditare da ViewPage NerdDinner.Helpers.PaginatedList>>< Dinner anziché ViewPage<<IEnumerable>>< Dinner e quindi aggiungere il codice seguente alla fine del modello di visualizzazione per visualizzare o nascondere l'interfaccia utente di spostamento successiva e precedente:

<% if (Model.HasPreviousPage) { %>

    <%= Html.RouteLink("<<<", "UpcomingDinners", new { page = (Model.PageIndex-1) }) %>

<% } %>

<% if (Model.HasNextPage) {  %>

    <%= Html.RouteLink(">>>", "UpcomingDinners", new { page = (Model.PageIndex + 1) }) %>

<% } %>

Si noti sopra come si usa il metodo helper Html.RouteLink() per generare i collegamenti ipertestuali. Questo metodo è simile al metodo helper Html.ActionLink() usato in precedenza. La differenza è che viene generato l'URL usando la regola di routing "UpcomingDinners" configurata all'interno del file Global.asax. Ciò garantisce che verranno generati URL al metodo di azione Index() con il formato : /Dinners/Page/{page} – dove il valore {page} è una variabile specificata in precedenza in base all'oggetto PageIndex corrente.

E ora quando si esegue di nuovo l'applicazione verranno visualizzate 10 cene alla volta nel browser:

Screenshot dell'elenco Delle prossime cene nella pagina Cena nerd.

Abbiamo anche <<< e >>> l'interfaccia utente di spostamento nella parte inferiore della pagina che consente di ignorare i dati e indietro sui dati usando URL accessibili dal motore di ricerca:

Screenshot della pagina Cena nerd con l'elenco Prossimi cena.

Argomento lato: Comprensione delle implicazioni di IQueryable<T>
IQueryable<T> è una funzionalità molto potente che consente un'ampia gamma di scenari di esecuzione posticipati interessanti,ad esempio paging e query basate su composizione. Come per tutte le funzionalità potenti, si vuole essere attenti a come usarlo e assicurarsi che non sia abusato. È importante riconoscere che la restituzione di un risultato T IQueryable<T> dal repository consente di chiamare il codice per aggiungere metodi di operatore concatenati e quindi partecipare all'esecuzione della query finale. Se non si vuole fornire il codice chiamante questa possibilità, è necessario restituire i risultati IList<T o IEnumerable<T>>, che contengono i risultati di una query già eseguita. Per gli scenari di impaginazione, è necessario eseguire il push della logica di impaginazione dei dati effettiva nel metodo repository chiamato. In questo scenario è possibile aggiornare il metodo FindUpcomingDinners() per avere una firma che ha restituito un elemento PaginatedList: PaginatedList<> Dinner FindUpcomingDinners(int pageIndex, int pageSize) { } O tornare indietro in una cena IList e usare un param out "totalCount" per restituire il numero totale di cene: IList<<>> Dinner FindUpcomingDinners(int pageIndex, int pageSize, out int totalCount) { }

passaggio successivo

Verrà ora illustrato come aggiungere il supporto per l'autenticazione e l'autorizzazione all'applicazione.