Datenpunkte

Serverseitige Auslagerung mithilfe des Entity Framework und ASP.NET MVC 3

Julie Lerman

Beispielcode herunterladen.

image: Julie LermanIn meiner Februarkolumne in Datenpunkte habe ich das Plug-In jQuery DataTables und dessen Fähigkeit vorgestellt, große Datenmengen auf Clientseite problemlos zu verarbeiten. Dies ist praktisch für Webanwendungen, in denen Sie große Mengen von Daten aufteilen möchten. In diesem Monat möchte ich die Verwendung von Abfragen behandeln, die kleinere Nutzlasten zurückgeben, um eine andere Art von Interaktion mit den Daten zu ermöglichen. Dies ist besonders dann wichtig, wenn Sie es mit Anwendungen für Mobilgeräte zu tun haben.

Ich nutze Funktionen, die in ASP.NET MVC 3 eingeführt wurden, und zeige, wie diese zusammen mit einer effizienten serverseitigen Auslagerung im Rahmen des Entity Framework verwendet werden. Bei dieser Aufgabe gibt es zwei Probleme. Das erste besteht darin, der Entity Framework-Abfrage die korrekten Auslagerungsparameter bereitzustellen. Das zweite besteht darin, eine Funktion der clientseitigen Auslagerung nachzuahmen, indem visuelle Hinweise bereitgestellt werden, die anzeigen, dass weitere Daten für den Abruf verfügbar sind, und indem Links bereitgestellt werden, um den Abruf auszulösen.

ASP.NET MVC 3 verfügt über zahlreiche neue Funktionen, wie das neue Razor-Anzeigemodul, Verbesserungen für die Validierung und eine Fülle weiterer JavaScript-Funktionen. Sie finden die MVC-Startseite unter asp.net/mvc. Hier können Sie ASP.NET MVC 3 herunterladen. Außerdem finden Sie hier Links zu Blogeinträgen und Schulungsvideos, die Ihnen bei den ersten Schritten helfen. Eine der neuen Funktionen, die ich verwende, ist ViewBag. Wenn Sie ASP.NET MVC bereits verwendet haben, stellt ViewBag eine Verbesserung der ViewData-Klasse dar. Sie können nun dynamisch erstellte Eigenschaften verwenden.

Ein weiteres neues Element in ASP.NET MVC 3 ist der spezielle System.Web.Helpers.WebGrid. Obwohl eine der Funktionen des Grids die Auslagerung ist, verwende ich für dieses Beispiel zwar den neuen Grid, nicht jedoch dessen Auslagerungsfunktion, da dies eine clientseitige Auslagerung ist. Mit anderen Worten, diese Funktion lagert mittels eines Datensatzes aus, der ihr bereitgestellt wird, ähnlich wie das DataTables-Plug-In. Ich verwende stattdessen die serverseitige Auslagerung.

Für diese kleine Anwendung benötigen Sie ein Entitätsdatenmodell. Ich verwende ein Modell, das aus der Microsoft AdventureWorksLT-Beispieldatenbank erstellt wurde. Ich benötige jedoch nur Customer und SalesOrderHeaders. Ich habe die Eigenschaften Rowguid, PasswordHash und PasswordSalt von Customer in eine getrennte Entität verschoben, sodass ich diese bei der Bearbeitung nicht berücksichtigen muss. Abgesehen von dieser kleinen Änderung habe ich das Modell nicht verändert.

Ich habe mittels der ASP.NET MVC 3-Projektvorlage ein Projekt erstellt. Dadurch werden einige Controller und Anzeigen vorausgefüllt. Der voreingestellte HomeController stellt die Customers dar.

Ich verwende eine einfache Datenzugriffsklasse, um Interaktionsmöglichkeiten mit dem Modell, dem Kontext und letzten Endes der Datenbank bereitzustellen. In dieser Klasse stellt meine GetPagedCustomers die serverseitige Auslagerung bereit. Wenn das Ziel der ASP.NET MVC-Anwendung darin bestünde, den Benutzern die Interaktion mit allen Kunden zu ermöglichen, würden sehr viele Kunden mit einer einzigen Abfrage zurückgegeben und im Browser verwaltet werden. Stattdessen gestatten wir der Anwendung die Anzeige von 10 Zeilen gleichzeitig. Dieser Filter wird von GetPagedCustomers bereitgestellt. Die Abfrage, die ich ausführen muss, sieht wie folgt aus:

context.Customers.Where(c => 
c.SalesOrderHeaders.Any()).Skip(skip).Take(take).ToList()

Die Anzeige weiß, welche Seite angefordert werden muss, und gibt diese Informationen an den Controller weiter. Der Controller steuert, wie viele Zeilen pro Seite bereitgestellt werden sollen. Der Controller berechnet den Skip-Wert mittels der Nummer der Seite und der Anzahl der Zeilen pro Seite. Wenn der Controller die GetPagedCustomers-Methode aufruft, gibt er den berechneten Skip-Wert und die Anzahl der Zeilen pro Seite weiter. Dies ist der Take-Wert. Wenn wir uns also auf Seite 4 befinden, und es werden 10 Zeilen pro Seite angezeigt, dann ist der Skip-Wert 40, und der Take-Wert ist 10.

Die Auslagerungsabfrage erstellt zunächst einen Filter, der nur diejenigen Kunden anfordert, für die Bestellungen vorhanden sind. Anschließend werden die resultierenden Daten mittels der LINQ Skip- und Take-Methoden zu einer Teilmenge der Kunden. Die Abfrage, einschließlich der Auslagerung, wird vollständig in der Datenbank durchgeführt. Die Datenbank gibt nur die Anzahl der Zeilen zurück, die von der Take-Methode angegeben wird.

Die Abfrage besteht aus nur wenigen Komponenten, um einige Tricks zu ermöglichen, die ich später hinzufügen werde. Dies ist ein erster Blick auf die GetPagedCustomers-Methode, die von HomeController aufgerufen wird:

public static List<Customer> GetPagedCustomers(int skip, int take)
    {
      using (var context = new AdventureWorksLTEntities())
      {
        var query = context.Customers.Include("SalesOrderHeaders")
          .Where(c => c.SalesOrderHeaders.Any())
          .OrderBy(c => c.CompanyName + c.LastName + c.FirstName);

        return query.Skip(skip).Take(take).ToList();
      }
    }

Die Index-Methode des Controllers, die diese Methode aufruft, legt die Anzahl der Zeilen fest, die zurückgegeben werden. Hierfür wird eine Variable verwendet, die ich pageSize nennen werde und die den Wert für Take darstellt. Die Index-Methode gibt außerdem die Stelle an, an der begonnen werden sollen. Dies erfolgt anhand einer Seitennummer, die als Parameter übergeben wird, wie hier gezeigt:

public ActionResult Index(int? page)
    {
      const int pageSize = 10;
      var customers=DataAccess.GetPagedCustomers((page ?? 0)*pageSize, pageSize);
      return View(customers);
    }

Damit sind wir schon ein gutes Stück voran gekommen. Die serverseitige Auslagerung ist nun vollständig implementiert. Mit einem WebGrid im Markup für die Index-Anzeige können wir die Kunden anzeigen, die von der GetPagedCustomers-Methode zurückgegeben werden. Im Markup müssen Sie den Grid deklarieren und instantiieren und dabei das Modell übergeben, das die Kundenliste darstellt, die bei der Erstellung der Anzeige durch den Controller bereitgestellt wurde. Mittels der WebGrid GetHtml-Methode können Sie den Grid anschließend formatieren und dabei angeben, welche Spalten angezeigt werden sollen. Ich zeige hier nur drei der Customer-Eigenschaften: CompanyName, FirstName und LastName. IntelliSense wird vollständig unterstützt, wenn Sie dieses Markup eingeben. Dies ist unabhängig davon, ob Sie eine Syntax verwenden, die mit ASPX-Ansichten verknüpft ist, oder eine Syntax, die mit der Syntax des neuen MVC 3 Razor-Anzeigemoduls (wie im folgenden Beispiel) verknüpft ist. In der ersten Spalte stelle ich einen Link für die Bearbeitung von Aktionen bereit, sodass die Benutzer die angezeigten Kunden bearbeiten können:

    @{
      var grid = new WebGrid(Model); 
    }
    <div id="customergrid">
      @grid.GetHtml(columns: grid.Columns(
        grid.Column(format: (item) => Html.ActionLink
          ("Edit", "Edit", new { customerId = item.CustomerID })),
      grid.Column("CompanyName", "Company"), 
      grid.Column("FirstName", "First Name"),
      grid.Column("LastName", "Last Name")
       ))
    </div>

Das Ergebnis ist in Abbildung 1 dargestellt.

image: Providing Edit ActionLinks in the WebGrid

Abbildung 1 Bereitstellung von Links für die Bearbeitung von Aktionen im WebGrid

So weit, so gut. Dies ermöglicht Benutzern jedoch nicht die Navigation zu einer anderen Seite mit Daten. Hierfür gibt es eine Reihe von Möglichkeiten: Eine Möglichkeit besteht darin, die Seitennummer in der URI anzugeben, zum Beispiel http://adventureworksmvc.com/Page/3. Sie möchten sicher nicht Ihre Endbenutzer bitten, dies zu tun. Eine bessere Methode stellt die Implementierung von Auslagerungssteuerelementen dar, wie Links zu Seitennummern: "1 2 3 4 5". Oder Sie können Links verwenden, mit denen Benutzer vorwärts oder zurück navigieren können: "<<      >>".

Das aktuelle Hindernis für die Aktivierung der Auslagerungslinks besteht darin, dass die Index-Anzeigenseite nicht weiß, dass weitere Kunden für den Abruf verfügbar sind. Sie kennt nur die 10 Kunden, die von ihr angezeigt werden. Indem Sie der Datenzugriffsebene zusätzliche Logik hinzufügen und diese über den Controller an die Anzeige weitergeben, können Sie dieses Problem lösen. Beginnen wir mit der Datenzugriffslogik.

Um zu erfahren, ob es mehr als den aktuellen Satz von Kunden gibt, müssen Sie alle Kunden zählen, die von der Abfrage zurückgegeben werden könnten, wenn sie diese nicht in Gruppen von 10 Kunden auslagern würde. Hier zahlt sich die Verfassung der Abfrage in GetPagedCustomers aus. Beachten Sie, dass die erste Abfrage an _customerQuery zurückgegeben wird, eine Variable, die auf Klassenebene deklariert wird, wie hier gezeigt:

_customerQuery = context.Customers.Where(c => c.SalesOrderHeaders.Any());

Sie können die Count-Methode an das Ende dieser Abfrage anfügen, um die zahl aller Kunden zu ermitteln, die der Abfrage entsprechen, bevor die Auslagerung angewendet wird. Die Count-Methode erzwingt die sofortige Ausführung einer vergleichsweise einfachen Abfrage. Dies ist die in SQL Server ausgeführte Abfrage, aus der die Antwort einen einzelnen Wert zurückgibt:

    SELECT 
    [GroupBy1].[A1] AS [C1]
    FROM ( SELECT 
           COUNT(1) AS [A1]
           FROM [SalesLT].[Customer] AS [Extent1]
           WHERE  EXISTS (SELECT 
                  1 AS [C1]
                  FROM [SalesLT].[SalesOrderHeader] AS [Extent2]
                  WHERE [Extent1].[CustomerID] = [Extent2].[CustomerID]
           )
    )  AS [GroupBy1]

Wenn Sie die Zahl ermittelt haben, können Sie festlegen, ob die aktuelle Seite mit Kunden die erste Seite, die letzte Seite oder eine Seite dazwischen ist. Anschließend können Sie diese Logik verwenden, um festzulegen, welche Links angezeigt werden sollen. Wenn Sie sich zum Beispiel jenseits der ersten Seite mit Kunden befinden, ist es logisch, einen Link anzuzeigen, mit dem Benutzer auf vorherige Seiten mit Kundendaten zugreifen können. Dieser Link könnte so aussehen: "<<".

Wir können Werte berechnen, um diese Logik in der Datenzugriffsklasse darzustellen und sie anschließend in einer Wrapperklasse zusammen mit den Kunden zu veröffentlichen. Dies ist die neue Klasse, die ich verwenden werde:

public class PagedList<T>
  {
    public bool HasNext { get; set; }
    public bool HasPrevious { get; set; }
    public List<T> Entities { get; set; }
  }

Die GetPagedCustomers-Methode gibt nun eine PagedList-Klasse anstelle einer Liste zurück. Abbildung 2 zeigt die neue Version von GetPagedCustomers.

Abbildung 2Die neue Version von GetPagedCustomers

public static PagedList<Customer> GetPagedCustomers(int skip, int take)
    {
      using (var context = new AdventureWorksLTEntities())
      {
        var query = context.Customers.Include("SalesOrderHeaders")
          .Where(c => c.SalesOrderHeaders.Any())
          .OrderBy(c => c.CompanyName + c.LastName + c.FirstName);

        var customerCount = query.Count();

        var customers = query.Skip(skip).Take(take).ToList();
      
        return new PagedList<Customer>
        {
          Entities = customers,
          HasNext = (skip + 10 < customerCount),
          HasPrevious = (skip > 0)
        };
      }
    }

Die neuen Variablen sind nun ausgefüllt. Betrachten wir nun, wie die Index-Methode im HomeController diese zurück zur Anzeige verschiebt. An dieser Stelle können Sie die neue ViewBag-Klasse verwenden. Die Ergebnisse der Kundenabfrage werden weiterhin in einer Anzeige zurückgegeben. Sie können jedoch die Werte zusätzlich ausfüllen, um das Markup für den nächsten und den vorigen Link in der ViewBag-Klasse festzulegen. Diese stehen der Anzeige zur Laufzeit zur Verfügung:

public ActionResult Index(int? page)
    {
      const int pageSize = 10;
      var customers=DataAccess.GetPagedCustomers((page ?? 0)*pageSize, pageSize);
      ViewBag.HasPrevious = DataAccess.HasPreviousCustomers;
      ViewBag.HasMore = DataAccess.HasMoreCustomers;
      ViewBag.CurrentPage = (page ?? 0);
      return View(customers);
    }

Hierbei ist wichtig zu verstehen, dass die ViewBag-Klasse dynamisch und nicht stark typisiert ist. ViewBag verfügt nicht wirklich über HasPrevious und HasMore. Ich habe sie bei der Eingabe des Codes erfunden. Lassen Sie sich daher nicht verunsichern, wenn IntelliSense Ihnen diese nicht vorschlägt. Sie können jede gewünschte dynamische Eigenschaft erstellen.

Wenn Sie das ViewPage.ViewData-Dictionary verwenden und sich für die diesbezüglichen Unterschiede interessieren: ViewBag erfüllt diese Aufgabe. Dies macht Ihren Code nicht nur etwas eleganter, sondern es typisiert auch die Eigenschaften. Beispielsweise ist HasNext ein dynamischer {bool}, und CurrentPage ist ein dynamischer {int}. Sie müssen die Werte nicht umwandeln, wenn Sie sie später abrufen.

Im Markup ist die Kundenliste weiterhin in der Modellvariablen enthalten. Es ist jedoch auch eine ViewBag-Variable verfügbar. Es liegt an Ihnen, welche dynamischen Variablen Sie in das Markup eingeben. Eine Schnellinfo erinnert Sie daran, dass die Eigenschaften dynamisch sind, wie in Abbildung 3 gezeigt.

image: ViewBag Properties Aren’t Available Through IntelliSense Because They’re Dynamic

Abbildung 3 ViewBag-Eigenschaften sind nicht über IntelliSense verfügbar, weil sie dynamisch sind

Dies ist das Markup, das die ViewBag-Variablen verwendet, um festzulegen, ob die Navigationslinks angezeigt werden oder nicht:

@{ if (ViewBag.HasPrevious)
  {
    @Html.ActionLink("<<", "Index", new { page = (ViewBag.CurrentPage - 1) })
  }
}

@{ if (ViewBag.HasMore)
   { @Html.ActionLink(">>", "Index", new { page = (ViewBag.CurrentPage + 1) }) 
  }
}

Diese Logik stellt eine Variante des Markups dar, das im Lernprogramm für die NerdDinner-Anwendung verwendet wird. Dieses finden Sie unter nerddinnerbook.s3.amazonaws.com/Intro.htm.

Wenn ich nun die Anwendung ausführe, kann ich von einer Seite mit Kunden zur nächsten navigieren.

Wenn ich mich auf der ersten Seite befinde, steht mir ein Link für die Navigation zur nächsten Seite, jedoch kein Link für die Navigation zu einer früheren Seite bereit, da keine früheren Seiten vorhanden sind (siehe Abbildung 4).

image: The First Page of Customer Has Only a Link to Navigate to the Next Page

Abbildung 4 Die erste Seite enthält nur einen Link für die Navigation zur nächsten Seite

Wenn ich auf den Link klicke und zur nächsten Seite navigiere, werden Links angezeigt, um zur vorherigen oder zur nächsten Seite zu navigieren (siehe Abbildung 5).

image: A Single Page of Customers with Navigation Links to Go to Previous or Next Page of Customers

Abbildung 5 Eine einzelne Seite mit Kunden und Links für die Navigation zur vorherigen oder zur nächsten Seite mit Kunden

 Der nächste Schritt besteht natürlich darin, zusammen mit einem Designer an einem attraktiveren Design für die Auslagerung zu arbeiten.

Eine wesentliche Komponente Ihrer Toolbox

Zusammenfassend lässt sich sagen, dass Ihre Anwendung nicht unbedingt stets davon profitiert, große Mengen an Daten bereitzustellen, obwohl es eine Reihe von Tools für die Optimierung der clientseitigen Auslagerung gibt, wie die jQuery DataTables-Erweiterung und den neuen ASP.NET MVC 3-Webgrid. Die effiziente Durchführung serverseitiger Auslagerungen stellt eine wesentliche Komponente Ihrer Toolbox dar. Das Entity Framework und ASP.NET MVC stellen gemeinsam eine hervorragende Benutzerumgebung bereit und vereinfachen gleichzeitig die Entwicklung dieser Benutzerumgebung.

Julie Lerman ist Microsoft MVP, .NET-Mentor und Unternehmensberaterin und lebt in den Bergen von Vermont. Sie hält bei User Groups und Konferenzen in der ganzen Welt Vorträge zum Thema Datenzugriff und anderen Microsoft .NET-Themen. Julie Lerman führt einen Blog unter thedatafarm.com/blog und ist Autorin des hoch gelobten Buchs "Programming Entity Framework" (Programmieren mit Entity Framework) (O’Reilly Media 2009). Sie können ihr auf Twitter unter Twitter.com/julielerman folgen.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Vishal Joshi