Share via


Implementieren einer effizienten Datenauslagerung

von Microsoft

PDF herunterladen

Dies ist Schritt 8 eines kostenlosen "NerdDinner"-Anwendungstutorial , in dem Sie eine kleine, aber vollständige Webanwendung mit ASP.NET MVC 1 erstellen.

Schritt 8 zeigt, wie Sie unserer /Dinners-URL Pagingunterstützung hinzufügen, sodass statt 1000 Abendessen gleichzeitig nur 10 anstehende Abendessen gleichzeitig angezeigt werden und Endbenutzer die gesamte Liste auf SEO-freundliche Weise durchlaufen können.

Wenn Sie ASP.NET MVC 3 verwenden, empfehlen wir Ihnen, die Tutorials Erste Schritte Mit MVC 3 oder MVC Music Store zu befolgen.

NerdDinner Schritt 8: Pagingunterstützung

Wenn unsere Website erfolgreich ist, wird es Tausende von bevorstehenden Abendessen geben. Wir müssen sicherstellen, dass unsere Benutzeroberfläche skaliert wird, um alle diese Abendessen zu verarbeiten, und es Benutzern ermöglicht, sie zu durchsuchen. Um dies zu ermöglichen, fügen wir unserer /Dinners-URL Pagingunterstützung hinzu, sodass statt 1000 Abendessen gleichzeitig nur 10 bevorstehende Abendessen gleichzeitig angezeigt werden und Endbenutzer die gesamte Liste auf SEO-freundliche Weise durchlaufen können.

Recap der Index()-Aktionsmethode

Die Index()-Aktionsmethode in unserer DinnersController-Klasse sieht derzeit wie folgt aus:

//
// GET: /Dinners/

public ActionResult Index() {

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

Wenn eine Anforderung an die /Dinners-URL gestellt wird, ruft sie eine Liste aller anstehenden Abendessen ab und rendert dann eine Liste aller Folgenden:

Screenshot der Listenseite

Grundlegendes zu IQueryable<T>

Iqueryable<T> ist eine Schnittstelle, die mit LINQ als Teil von .NET 3.5 eingeführt wurde. Es ermöglicht leistungsstarke Szenarien für verzögerte Ausführung, die wir nutzen können, um paging-Unterstützung zu implementieren.

In unserem DinnerRepository geben wir eine IQueryable<Dinner-Sequenz> aus unserer FindUpcomingDinners()-Methode zurück:

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;
    }

Das IQueryable<Dinner-Objekt>, das von unserer FindUpcomingDinners()-Methode zurückgegeben wird, kapselt eine Abfrage, um Dinner-Objekte mithilfe von LINQ to SQL aus unserer Datenbank abzurufen. Wichtig ist, dass die Abfrage für die Datenbank erst ausgeführt wird, wenn versucht wird, auf die Daten in der Abfrage zuzugreifen bzw. zu durchlaufen, oder bis wir die ToList()-Methode dafür aufrufen. Der Code, der unsere FindUpcomingDinners()-Methode aufruft, kann optional dem IQueryable<Dinner-Objekt> zusätzliche "verkettete" Vorgänge/Filter hinzufügen, bevor die Abfrage ausgeführt wird. LINQ to SQL ist dann intelligent genug, um die kombinierte Abfrage für die Datenbank auszuführen, wenn die Daten angefordert werden.

Zum Implementieren der Paginglogik können wir die Index()-Aktionsmethode von DinnersController aktualisieren, sodass zusätzliche Operatoren "Skip" und "Take" auf die zurückgegebene IQueryable<Dinner-Sequenz> angewendet werden, bevor ToList() aufgerufen wird:

//
// GET: /Dinners/

public ActionResult Index() {

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

    return View(paginatedDinners);
}

Der obige Code überspringt die ersten 10 bevorstehenden Abendessen in der Datenbank und gibt dann 20 Abendessen zurück. LINQ to SQL ist intelligent genug, um eine optimierte SQL-Abfrage zu erstellen, die diese Überspringlogik in der SQL-Datenbank und nicht auf dem Webserver ausführt. Dies bedeutet, dass selbst wenn wir Millionen von bevorstehenden Dinners in der Datenbank haben, nur die 10, die wir benötigen, als Teil dieser Anforderung abgerufen werden (was sie effizient und skalierbar macht).

Hinzufügen eines "Page"-Werts zur URL

Anstatt einen bestimmten Seitenbereich hart zu codieren, möchten wir, dass unsere URLs einen "page"-Parameter enthalten, der angibt, welchen Dinner-Bereich ein Benutzer anfordert.

Verwenden eines Querystring-Werts

Der folgende Code veranschaulicht, wie wir unsere Index()-Aktionsmethode aktualisieren können, um einen querystring-Parameter zu unterstützen und URLs wie /Dinners?page=2 zu aktivieren:

//
// 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);
}

Die oben genannte Index()-Aktionsmethode verfügt über einen Parameter mit dem Namen "page". Der Parameter wird als nullable integer deklariert (was int? angibt). Dies bedeutet, dass die URL /Dinners?page=2 bewirkt, dass der Wert "2" als Parameterwert übergeben wird. Die /Dinners-URL (ohne querystring-Wert) bewirkt, dass ein NULL-Wert übergeben wird.

Wir multiplizieren den Seitenwert mit der Seitengröße (in diesem Fall 10 Zeilen), um zu bestimmen, wie viele Abendessen übersprungen werden sollen. Wir verwenden den C#-NULL-Operator (??), der bei Nullable-Typen nützlich ist. Der obige Code weist der Seite den Wert 0 zu, wenn der Seitenparameter NULL ist.

Verwenden von eingebetteten URL-Werten

Eine Alternative zur Verwendung eines Querystring-Werts wäre das Einbetten des Seitenparameters in die tatsächliche URL selbst. Beispiel: /Dinners/Page/2 oder /Dinners/2. ASP.NET MVC enthält eine leistungsstarke URL-Routing-Engine, die die Unterstützung von Szenarien wie diesen erleichtert.

Wir können benutzerdefinierte Routingregeln registrieren, die alle eingehenden URLs oder URL-Formate einer beliebigen Controllerklasse oder Aktionsmethode zuordnen. Wir müssen lediglich die Datei Global.asax in unserem Projekt öffnen:

Screenshot der Navigationsstruktur

Registrieren Sie dann eine neue Zuordnungsregel mithilfe der MapRoute()-Hilfsmethode wie beim ersten Aufruf von Routen. MapRoute() unten:

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);
}

Oben registrieren wir eine neue Routingregel namens "UpcomingDinners". Wir geben an, dass es das URL-Format "Dinners/Page/{page}" aufweist, wobei {page} ein parameterwert ist, der in die URL eingebettet ist. Der dritte Parameter der MapRoute()-Methode gibt an, dass URLs, die diesem Format entsprechen, der Index()-Aktionsmethode für die DinnersController-Klasse zugeordnet werden sollen.

Wir können genau denselben Index()-Code wie zuvor für unser Querystring-Szenario verwenden. Es sei denn, jetzt stammt der Parameter "page" von der URL und nicht von der 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);
}

Wenn wir nun die Anwendung ausführen und /Dinners eingeben, werden die ersten 10 bevorstehenden Abendessen angezeigt:

Screenshot der Liste der bevorstehenden Abendessen von Nerd.

Und wenn wir /Dinners/Page/1 eingeben, sehen wir die nächste Seite der Abendessen:

Screenshot der nächsten Seite der Liste

Hinzufügen der Seitennavigationsoberfläche

Der letzte Schritt zum Abschließen des Pagingszenarios ist die Implementierung der Navigationsbenutzeroberfläche "Weiter" und "Vorherige" in unserer Ansichtsvorlage, damit Benutzer die Dinner-Daten problemlos überspringen können.

Um dies ordnungsgemäß zu implementieren, müssen wir die Gesamtzahl der Dinners in der Datenbank sowie die Anzahl der Datenseiten kennen, in die dies übersetzt wird. Anschließend müssen wir berechnen, ob sich der aktuell angeforderte "Page"-Wert am Anfang oder Ende der Daten befindet, und die Benutzeroberfläche "previous" und "next" entsprechend ein- oder ausblenden. Wir könnten diese Logik in unserer Index()-Aktionsmethode implementieren. Alternativ können wir dem Projekt eine Hilfsklasse hinzufügen, die diese Logik auf eine wiederverwendbare Weise kapselt.

Im Folgenden finden Sie eine einfache Hilfsklasse "PaginatedList", die von der im .NET Framework integrierten List<T-Auflistungsklasse> abgeleitet wird. Es implementiert eine wiederverwendbare Auflistungsklasse, die verwendet werden kann, um jede Sequenz von IQueryable-Daten zu paginieren. In unserer NerdDinner-Anwendung wird es über IQueryable<Dinner-Ergebnisse> funktionieren, aber es könnte genauso einfach für IQueryable<Product> - oder IQueryable<Customer-Ergebnisse> in anderen Anwendungsszenarien verwendet werden:

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);
        }
    }
}

Beachten Sie oben, wie Eigenschaften wie "PageIndex", "PageSize", "TotalCount" und "TotalPages" berechnet und dann verfügbar gemacht werden. Außerdem werden dann zwei Hilfseigenschaften "HasPreviousPage" und "HasNextPage" verfügbar gemacht, die angeben, ob sich die Seite der Daten in der Sammlung am Anfang oder Am Ende der ursprünglichen Sequenz befindet. Der obige Code führt dazu, dass zwei SQL-Abfragen ausgeführt werden– die erste, die die Gesamtzahl der Dinner-Objekte abruft (dies gibt nicht die Objekte zurück, sondern führt eine "SELECT COUNT"-Anweisung aus, die eine ganze Zahl zurückgibt), und die zweite, um nur die Datenzeilen abzurufen, die wir für die aktuelle Datenseite benötigen.

Anschließend können wir unsere DinnersController.Index()-Hilfsmethode aktualisieren, um ein PaginatedList<Dinner> aus unserem DinnerRepository.FindUpcomingDinners()-Ergebnis zu erstellen und an unsere Ansichtsvorlage zu übergeben:

//
// 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);
}

Wir können dann die Ansichtsvorlage \Views\Dinners\Index.aspx aktualisieren, um von ViewPage<NerdDinner.Helpers.PaginatedList<Dinner>> anstelle von ViewPage<IEnumerable<Dinner>> zu erben, und dann den folgenden Code unten in unserer Ansichtsvorlage hinzufügen, um die nächste und vorherige Navigationsbenutzeroberfläche ein- oder auszublenden:

<% if (Model.HasPreviousPage) { %>

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

<% } %>

<% if (Model.HasNextPage) {  %>

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

<% } %>

Beachten Sie oben, wie wir die Html.RouteLink()-Hilfsmethode verwenden, um unsere Links zu generieren. Diese Methode ähnelt der Html.ActionLink()-Hilfsmethode, die wir zuvor verwendet haben. Der Unterschied besteht darin, dass wir die URL mithilfe der Routingregel "UpcomingDinners" generieren, die wir in unserer Datei Global.asax eingerichtet haben. Dadurch wird sichergestellt, dass wir URLs für unsere Index()-Aktionsmethode generieren, die das Format /Dinners/Page/{page} aufweisen. Dabei ist der {page}-Wert eine Variable, die wir oben basierend auf dem aktuellen PageIndex bereitstellen.

Und jetzt, wenn wir unsere Anwendung erneut ausführen, sehen wir 10 Abendessen gleichzeitig in unserem Browser:

Screenshot der Liste

Wir verfügen <<< auch über eine Navigationsoberfläche am >>> unteren Rand der Seite, die es uns ermöglicht, unsere Daten mithilfe von durch Suchmaschinen zugänglichen URLs vorwärts und rückwärts zu überspringen:

Screenshot der Seite

Nebenthema: Grundlegendes zu den Auswirkungen von IQueryable<T>
IQueryable<T> ist ein sehr leistungsstarkes Feature, das eine Vielzahl von interessanten Szenarien für verzögerte Ausführung (z. B. Paging und kompositionsbasierte Abfragen) ermöglicht. Wie bei allen leistungsstarken Features möchten Sie vorsichtig sein, wie Sie es verwenden und sicherstellen, dass es nicht missbraucht wird. Es ist wichtig zu erkennen, dass die Rückgabe eines IQueryable<T-Ergebnisses> aus Ihrem Repository es aufruft, Code an verkettete Operatormethoden an das Repository anzufügen und so an der ultimativen Abfrageausführung teilzunehmen. Wenn Sie diesen Aufrufcode nicht bereitstellen möchten, sollten Sie IList<T> - oder IEnumerable<T-Ergebnisse> zurückgeben, die die Ergebnisse einer abfrage enthalten, die bereits ausgeführt wurde. Bei Paginierungsszenarien müssen Sie die eigentliche Datenpa paginierungslogik in die aufgerufene Repositorymethode pushen. In diesem Szenario können wir unsere FindUpcomingDinners()-Findermethode aktualisieren, um eine Signatur zu erhalten, die entweder eine PaginatedList zurückgegeben hat: PaginatedList< Dinner> FindUpcomingDinners(int pageIndex, int pageSize) { } Or return back an IList<Dinner>, and use a "totalCount" out param to return the total count of Dinners: IList<Dinner> FindUpcomingDinners(int pageIndex, int pageSize, out int totalCount) { }

Nächster Schritt

Sehen wir uns nun an, wie wir unserer Anwendung Authentifizierungs- und Autorisierungsunterstützung hinzufügen können.