ASP.NET MVC 5

Eine Einführung zu Single-Page Applications für .NET-Entwickler

Long Le

Codebeispiel herunterladen.

Die meisten Microsoft .NET Framework-Entwickler haben den Großteil ihres Berufslebens mit Servern verbracht und Webanwendungen mithilfe von C#- oder Visual Basic .NET-Codierung erstellt. Natürlich haben sie JavaScript für einfache Sachverhalte wie modale Fenster, Validierung, AJAX-Aufrufe usw. verwendet. Allerdings wurde JavaScript (in der Regel als clientseitiger Code) als Sprache für Dienstprogramme genutzt und Anwendungen wurden hauptsächlich serverseitig betrieben.

In letzter Zeit hat es sich durchgesetzt, Webanwendungscode von der Serverseite zu Clients (Browser) zu migrieren, um eine fließende und reaktionsfreudige Benutzererfahrung sicherzustellen, wie sie von Anwendern erwartet wird. Vor diesem Hintergrund hegen viele .NET-Entwickler (besonders in Unternehmen) große Befürchtungen in Bezug auf bewährte JavaScript-Methoden, Architektur, Komponententests, Wartbarkeit und die rapide Zunahme verschiedener Arten von JavaScript-Bibliotheken. Ein Grund für den Trend, zu clientseitigen Anwendungen überzugehen, ist die zunehmende Verwendung von SPAs (Single-Page Applications). Zu sagen, dass die Zukunft in der Entwicklung von SPAs liegt, wäre eine grobe Untertreibung. Mithilfe von SPAs bieten einige der besten Anwendungen im Web eine nahtlose Benutzererfahrung und gute Reaktionsfähigkeit, während gleichzeitig Nutzlasten (Datenverkehr) und Serverroundtrips reduziert werden.

In diesem Artikel gehe ich auf mögliche Bedenken zum Übergang von der Serverseite zu SPAs ein. Am besten können derartige Sorgen aus dem Weg geräumt werden, indem JavaScript als eigenständige Sprache akzeptiert wird, genau wie C#, Visual Basic .NET, Python und andere .NET-Sprachen.

Nachfolgend führe ich einige grundlegende Prinzipien der .NET-Entwicklung auf, die beim Entwickeln von Apps in JavaScript mitunter ignoriert oder vergessen werden:

  • Ihre Codebasis kann in .NET verwaltet werden, da Sie gezielt mit Klassengrenzen und dem Platz von Klassen in Projekten umgehen.
  • Sie trennen die verschiedenen Anliegen und haben deshalb keine Klassen, die für Hunderte verschiedener Bereiche verantwortlich sind und deren Verantwortlichkeiten überlappen.
  • Sie setzen wiederverwendbare Repositorys, Abfragen, Entitäten (Modelle) und Datenquellen ein.
  • Sie haben sich Gedanken über die Benennung der Klassen und Dateien gemacht und ihnen aussagekräftige Namen gegeben.
  • Sie sorgen für eine optimale Nutzung von Entwurfsmustern, Codierungskonventionen und Organisationsansätzen.

Da sich dieser Artikel an .NET-Entwickler richtet, die sich erstmals mit der Welt der SPAs beschäftigen, versuche ich mit möglichst wenigen Frameworks eine überschaubare SPA mit intakter Architektur zu erstellen.

Erstellen einer SPA in sieben wesentlichen Schritten

Nachstehend sind die sieben grundlegenden Schritte aufgeführt, mit denen Sie eine neue mit einer Visual Studio 2013 ASP.NET MVC-Standardvorlage erstellte ASP.NET-Webanwendung in eine SPA konvertieren. Dabei wird auf die entsprechenden Projektdateien verwiesen, die Sie im zugehörigen Codedownload finden.

  1. Herunterladen und Installieren der NuGet-Pakete RequireJS, Text-Plug-In für RequireJS und Kendo UI Web
  2. Hinzufügen eines Konfigurationsmoduls (Northwind.Web/Scripts/app/main.js)
  3. Hinzufügen eines App-Moduls (Northwind.Web/Scripts/app/app.js)
  4. Hinzufügen eines Routermoduls (Northwind.Web/Scripts/app/router.js)
  5. Hinzufügen einer Aktion und Anzeigen der beiden benannten SPAs (Northwind.Web/Controllers/HomeController.cs und Northwind.Web/Views/Home/Spa.cshtml)
  6. Bearbeiten der Datei „_ViewStart.cshtml“, damit MVC beim Laden von Ansichten nicht standardmäßig die Datei „_Layout.cshtml“ verwendet (Northwind.Web/Views/_ViewStart.cshtml)
  7. Aktualisieren der Links für die Layoutnavigation (Menü) in Abstimmung auf die neuen SPA-freundlichen URLs (Northwind.Web/Views/Shared/_Layout.cshtml)

Wenn Sie diese sieben Schritte ausgeführt haben, sollte die Projektstruktur für Ihre Webanwendung ähnlich wie in Abbildung 1 aussehen.

ASP.NET MVC Project Structure
Abbildung 1: ASP.NET MVC-Projektstruktur

Ich zeige Ihnen, wie Sie mit den folgenden JavaScript-Bibliotheken, die über NuGet erhältlich sind, eine ausgezeichnete SPA in ASP.NET MVC erstellen:

  • RequireJS (requirejs.org): Dies ist ein Ladeprogramm für JavaScript-Dateien und Module. RequireJS stellt #include-/import-/require-APIs bereit sowie die Möglichkeit, mithilfe der Abhängigkeitsinjektion (Dependency Injection, DI) verschachtelte Abhängigkeiten zu laden. Der RequireJS-Entwurfsansatz verwendet die Asynchronous Module Definition(AMD)-API für JavaScript-Module, wodurch Teile des Codes in nützliche Einheiten eingekapselt werden können. Außerdem stellt RequireJS eine intuitive Methode für den Verweis auf andere Codeeinheiten (Module) dar. RequireJS-Module folgen dem Modulmuster (bit.ly/18byc2Q). Eine vereinfachte Umsetzung dieses Musters verwendet JavaScript-Funktionen für die Kapselung. Sie sehen dieses Muster später in der Praxis, wenn alle JavaScript-Module in eine define- oder require-Funktion eingeschlossen werden.
  • Wer sich mit DI und IoC (Inversion of Control) auskennt, kann sich dieses Prinzip als clientseitiges DI-Framework vorstellen. Keine Bange, wenn Sie momentan nur Bahnhof verstehen. Gleich wird anhand einiger Codedarstellungen alles klarer.
  • Text-Plug-In für RequireJS (bit.ly/1cd8lTZ): Damit werden HTML-Segmente (Ansichten) remote in die SPA geladen.
  • Entity Framework (bit.ly/1bKiZ9I): Dies bedarf wohl keiner weiteren Erklärung, und da sich dieser Artikel um SPAs dreht, werde ich nicht weiter auf Entity Framework eingehen. Es gibt umfangreiches Dokumentationsmaterial für jeden, für den Entity Framework Neuland ist.
  • Kendo UI Web (bit.ly/t4VkVp): Dabei handelt es sich um ein umfassendes JavaScript/­HTML5-Framework inklusive Webbenutzeroberflächen-Widgets, Datenquellen, Vorlagen, Model-View-ViewModel(MVVM)-Muster, SPAs, Stilen usw., mit dem Sie eine reaktionsfähige und adaptive Anwendung erstellen können, die auch fantastisch aussieht.

Einrichten der SPA-Infrastruktur

Um das Einrichten einer SPA-Infrastruktur zu demonstrieren, erkläre ich zunächst, wie Sie das RequireJS (config)-Modul erstellen (Northwind.Web/Scripts/app/main.js). Dieses Modul fungiert als Einstiegspunkt beim Starten der App. Wenn Sie eine Konsolen-App kreiert haben, können Sie sich das als Main-Einstiegspunkt in „Program.cs“ vorstellen. Das Modul enthält im Prinzip die erste Klasse und die Methode, die beim Starten der SPA aufgerufen wird. Die Datei „main.js“ dient als Manifest für die SPA. Hier definieren Sie, wo sich die einzelnen Elemente in der SPA und (sofern vorhanden) ihre Abhängigkeiten befinden. Der Code für die RequireJS-Konfiguration ist in Abbildung 2 dargestellt.

Abbildung 2: RequireJS-Konfiguration

require.config({
  paths: {
    // Packages
    'jquery': '/scripts/jquery-2.0.3.min',
    'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
    'text': '/scripts/text',
    'router': '/scripts/app/router'
  },
  shim : {
    'kendo' : ['jquery']
  },
  priority: ['text', 'router', 'app'],
  jquery: '2.0.3',
  waitSeconds: 30
});
require([
  'app'
], function (app) {
  app.initialize();
});

Die paths-Eigenschaft in Abbildung 2 enthält eine Liste mit den Speicherorten und Namen der einzelnen Module. Shim ist der Name eines zuvor definierten Moduls. Die shim-Eigenschaft enthält alle Abhängigkeiten dieses Moduls. In diesem Fall laden Sie das Modul mit dem Namen kendo, das eine Abhängigkeit zum Modul jQuery besitzt. Wenn also ein Modul das Modul kendo benötigt, laden Sie zunächst jQuery, da jQuery als Abhängigkeit für das Modul kendo definiert wurde.

In Abbildung 2 wird der Code „require([], function(){})“ im nächsten Modul geladen, das ich „app“ genannt habe. Wie Sie sehen, habe ich Modulen aussagekräftige Namen gegeben.

Wie ruft also Ihre SPA dieses Modul zunächst auf? Diesen Vorgang konfigurieren Sie auf der ersten Landing Page in der SPA durch das Attribut „data-main“ im Skriptverweis-Tag für RequireJS. Ich habe angegeben, dass das Modul im main-Modul ausgeführt wird („main.js“). RequireJS übernimmt das Laden dieses Moduls. Sie müssen nur angeben, welches Modul zuerst geladen werden soll.

Es gibt zwei Optionen für SPA-Ansichten, die in die SPA geladen werden: Standard-HTML-Seiten (*.html) und ASP.NET MVC-Razor-Seiten (*.cshtml). Da dieser Artikel für .NET-Entwickler gedacht ist und viele Unternehmen serverseitige Bibliotheken und Frameworks besitzen, die weiterhin in den Ansichten verwendet werden sollen, erkläre ich hier die Erstellung von Razor-Ansichten.

Wie oben beschrieben, füge ich als Erstes eine Ansicht hinzu und nenne sie „Spa.cshtml“. Diese Ansicht lädt im Grunde die Shell oder alle HTML-Elemente für das Layout der SPA. Ausgehend von dieser Ansicht lade ich die anderen Ansichten (z. B. „About.cshtml“, „Contact.cshtml“ und „Index.cshtml“), während der Benutzer durch die SPA navigiert. Dazu tausche ich die Ansichten aus, die alle HTML-Elemente im Div-Element „content“ ersetzen.

Erstellen der SPA-Landing Page (Layout) (Northwind.Web/Views/Spa.cshtml) Da die Ansicht „Spa.cshtml“ als Landing Page für die SPA fungiert, in der alle anderen Ansichten geladen werden, enthält sie außer den Verweisen auf die erforderlichen Stylesheets und RequireJS kaum Markup. Beachten Sie das Attribut „data-main“ im nachstehenden Code, das RequireJS anweist, welches Modul zuerst geladen wird:

@{
  ViewBag.Title = "Spa";
  Layout = "~/Views/Shared/_Layout.cshtml";
}
<link href=
  "~/Content/kendo/2013.3.1119/kendo.common.min.css" 
  rel="stylesheet" />
<link href=
  "~/Content/kendo/2013.3.1119/kendo.bootstrap.min.css" 
  rel="stylesheet" />
<script src=
  "@Url.Content("~/scripts/require.js")"
  data-main="/scripts/app/main"></script>
<div id="app"></div>

Hinzufügen einer Aktion für das SPA-Layout (Northwind.Web/­Controllers/HomeController.cs) Um die Ansicht „Spa.cshtml“ zu erstellen und zu laden, fügen Sie eine Aktion und eine Ansicht hinzu:

public ActionResult Spa()
{
  return View();
}

Erstellen des Anwendungsmoduls (Northwind.Web/Scripts/app/app.js) Hier ist das Anwendungsmodul, das für das Initialisieren und Starten des Kendo UI-Routers verantwortlich ist:

define([
    'router'
  ], function (router) {
    var initialize = function() {
      router.start();
    };
    return {
      initialize: initialize
    };
  });

Erstellen des Routermoduls (Northwind.Web/Scripts/app/router.js) Es wird durch „app.js“ aufgerufen. Wenn Sie sich bereits mit ASP.NET MVC-Routen auskennen, wissen Sie, wie dies abläuft. Dies sind die SPA-Routen für Ihre Ansichten. Ich definiere sämtliche Routen für alle SPA-Ansichten, damit der Kendo UI-Router weiß, welche Ansichten in die SPA geladen werden sollen, wenn der Benutzer durch die SPA navigiert. Weitere Informationen finden Sie unter Codebeispiel 1 im zugehörigen Download.

Die Kendo UI-Routerklasse verfolgt den Anwendungsstatus nach und navigiert von einem Anwendungsstatus zum anderen. Der Router lässt sich unter Verwendung des Fragmentteils der URL (#page) in den Browserverlauf integrieren, wodurch für jeden Anwendungsstatus Lesezeichen und Links eingerichtet werden können. Wenn ein Benutzer auf eine routbare URL klickt, greift der Router ein und weist die Anwendung an, sich wieder in den Zustand zurückzusetzen, der in der Route codiert war. Die Routendefinition ist eine Zeichenfolge, die einen Pfad darstellt, über den der vom Benutzer gewünschte Anwendungsstatus identifiziert wird. Wenn eine Routendefinition einem URL-Hash-Fragment des Browsers zugeordnet wird, führt dies zu einem Aufruf des Routen-Handlers (siehe Abbildung 3).

Abbildung 3: Registrierte Routendefinitionen und zugehörige URLs

Registrierte Route (Definition) Vollständige URL (lesezeichenfähig)
/ localhost:25061/home/spa/home/index
/home/index localhost:25061/home/spa/#/home/index/home/about
/home/about localhost:25061/home/spa/#/home/about/home/contact
/home/contact localhost:25061/home/spa/#/home/contact/customer/index
/customer/index localhost:25061/home/spa/#/customer/index

Was das Kendo UI-Layout-Widget angeht, spricht der Name für sich. Sie sind wahrscheinlich mit dem Layout einer ASP.NET Web Forms-MasterPage oder mit dem MVC-Layout vertraut, das ein Projekt beim Erstellen einer neuen ASP.NET MVC-Webanwendung hat. In diesem SPA-Projekt befindet es sich unter „Northwind.Web/Views/Shared/_Layout.cshtml“. Es gibt kaum einen Unterschied zwischen dem Kendo UI-Layout und dem MVC-Layout, außer dass das Kendo UI-Layout clientseitig ausgeführt wird. Das Kendo UI-Layout funktioniert genau wie das serverseitige Layout, wobei die MVC-Laufzeit den Inhalt des Layouts gegen andere Ansichten austauscht. Sie verwenden die showIn-Methode, um die Ansicht (Inhalt) des Kendo UI-Layouts auszutauschen. Der Inhalt der Ansicht (HTML) wird in das div-Element mit der ID „content“ platziert, das beim Initialisieren an das Kendo UI-Layout übergeben wurde. Nach der Layoutinitialisierung rendern Sie das Layout im div-Element mit der ID „app“ – dies ist ein div-Element auf der Landing Page (Northwind.Web/Views/Home/Spa.cshtml). Ich gehe darauf gleich noch ein.

Die Hilfsmethode „loadView“ nimmt ein Ansichtsmodell, eine Ansicht und ggf. einen Rückruf an, um diese im Anschluss an die Bindung von Ansicht und Ansichtsmodell aufzurufen. Innerhalb der loadView-Methode nutzen Sie die Kendo UI FX-Bibliothek, um die Benutzererfahrung ästhetisch aufzuwerten, indem Sie einfache Animationselemente für den Ansichtsaustauschprozess hinzufügen. Dazu schieben Sie die aktuell geladene Ansicht nach links, laden die neue Ansicht remote und schieben die neu geladene Ansicht dann zurück in die Mitte. Sie können stattdessen natürlich eine Vielzahl anderer Animationen aus der Kendo UI FX-Bibliothek verwenden. Einer der wichtigsten Vorteile der Verwendung des Kendo UI-Layouts wird deutlich, wenn Sie die showIn-Methode zum Austauschen von Ansichten aufrufen. Das Layout stellt sicher, dass die Ansicht entladen, ordnungsgemäß gelöscht und aus dem DOM des Browsers entfernt wird. So wird eine problemlose Skalierung und gute Leistung der SPA sichergestellt.

Bearbeiten der Ansicht „_ViewStart.cshtml“ (Northwind.Web/Views/­_ViewStart.cshtml) So legen Sie für alle Ansichten fest, dass das ASP.NET MVC-Layout nicht standardmäßig verwendet wird:

@{
  Layout = null;
}

An dieser Stelle sollte die SPA funktionieren. Wenn Sie auf einen der Navigationslinks im Menü klicken, sehen Sie, dass der aktuelle Inhalt per AJAX ausgetauscht wird. Dies haben wir dem Kendo UI-Router und RequireJS zu verdanken.

Diese sieben Schritte zum Konvertieren einer neuen ASP.NET-Webanwendung in eine SPA sind doch gar nicht so schlimm, oder?

Nun, da die SPA funktionsfähig ist, widme ich mich dem, was die meisten Entwickler wohl mit einer SPA machen, nämlich CRUD-Funktionen (Create, Read, Update & Delete) hinzufügen.

Hinzufügen von CRUD-Funktionen zu einer SPA

Mit den folgenden grundlegenden Schritten fügen Sie der SPA (und den zugehörigen Projektcodedateien) eine Customer-Rasteransicht hinzu:

  • Hinzufügen des MVC-Controllers „CustomerController“ (Northwind.Web/Controllers/CustomerController.cs)
  • Hinzufügen des OData-basierten REST-Web-API-Controllers „Customer“ (Northwind.Web/Api/CustomerController.cs)
  • Hinzufügen einer Customer-Rasteransicht (Northwind.Web/Views/­Customer/Index.cshtml)
  • Hinzufügen des Moduls „CustomerModel“ (Northwind.Web/Scripts/app/models/CustomerModel)
  • Hinzufügen des Moduls „customerDatasource“ für das Customer-Raster (Northwind.Web/Scripts/app/datasources/customer­Datasource.js)
  • Hinzufügen des Moduls „indexViewModel“ für die Customer-Rasteransicht (Northwind.Web/Scripts/app/viewModels/­indexViewModel.js)

Einrichten der Lösungsstruktur mit Entity Framework In der Lösungsstruktur in Abbildung 4 sind drei Projekte markiert: „Northwind.Data“ (1), „Northwind.Entity“ (2) und „Northwind.Web“ (3). Im Folgenden erläutere ich alle drei Projekte und die Entity Framework Power Tools.

  • Northwind.Data: Dazu gehört alles, was mit dem Object-Relational Mapping(ORM)-Tool von Entity Framework in Bezug steht und für die Persistenz verwendet wird.
  • Northwind.Entity: Dazu gehören Domänenentitäten, die aus Plain Old CLR Object(POCO)-Klassen bestehen. Das sind alle Domänenobjekte mit Persistenzignoranz.
  • Northwind.Web: Dazu gehören die ASP.NET MVC 5-Webanwendung, die Darstellungsschicht, in der Sie die SPA mithilfe von zwei zuvor
  • erwähnten Bibliotheken (Kendo UI und RequireJS) ausbauen, sowie der Rest des serverseitigen Stapels: Entity Framework, Web-API und OData.
  • Entity Framework Power Tools: Zum (datenorientierten) Erstellen aller POCO-Entitäten und -Zuordnungen habe ich die Entity Framework Power Tools des Entity Framework-Teams verwendet (bit.ly/1cdobhk). Nach der Codegenerierung habe ich hier lediglich die Entitäten in ein separates Projekt (Northwind.Entity) kopiert, um die Bereiche voneinander abzugrenzen.

A Best-Practice Solution Structure
Abbildung 4: Eine bewährte Lösungsstruktur

Anmerkung: Sowohl das Northwind-SQL-Installationsskript als auch eine Sicherungskopie der Datenbank sind im herunterladbaren Quellcode im Ordner „Northwind.Web/App_Data“ enthalten (bit.ly/1cph5qc).

Die Lösung hat nun Zugriff auf die Datenbank. Jetzt erstelle ich die MVC-Klasse „CustomerController.cs“, um den Index einzurichten und Ansichten zu bearbeiten. Da die einzige Aufgabe des Controllers im Einrichten einer HTML-Ansicht für die SPA besteht, ist der verwendete Code minimal.

Erstellen des MVC-Customer-Controllers (Northwind.Web/­Controllers/CustomerController.cs) Um den Customer-Controller mit den Aktionen für den Index zu erstellen und Ansichten zu bearbeiten, gehen Sie folgendermaßen vor:

public class CustomerController : Controller
{
  public ActionResult Index()
  {
    return View();
  }
  public ActionResult Edit()
  {
    return View();
  }
}

Erstellen der Ansicht mit dem Customer-Raster (Northwind.Web/­Views/Customers/Index.cshtml) In Abbildung 5 wird gezeigt, wie Sie die Ansicht unter Verwendung des Customer-Rasters erstellen.

Keine Panik, wenn Ihnen das Markup in Abbildung 5 nicht bekannt vorkommt. Es ist MVVM-Markup (HTML) von Kendo UI. Damit wird einfach ein HTML-Element konfiguriert – in diesem Fall das div-Element mit der ID „grid“. Wenn Sie diese Ansicht später mithilfe des Kendo UI-MVVM-Frameworks an ein Ansichtsmodell binden, wird das Markup in Kendo UI-Widgets konvertiert. Weitere Informationen dazu finden Sie unter bit.ly/1d2Bgfj.

Abbildung 5: Markup für die Customer-Rasteransicht mit einem MVVM-Widget und Ereignisbindungen

<div class="demo-section">
  <div class="k-content" style="width: 100%">
    <div id="grid"
      data-role="grid"
      data-sortable="true"
      data-pageable="true"
      data-filterable="true"
      data-editable="inline"
      data-selectable="true"
      data-toolbar='[ { template: kendo.template($("#toolbar").html()) } ]'
      data-columns='[
        { field: "CustomerID", title: "ID", width: "75px" },
        { field: "CompanyName", title: "Company"},
        { field: "ContactName", title: "Contact" },
        { field: "ContactTitle", title: "Title" },
        { field: "Address" },
        { field: "City" },
        { field: "PostalCode" },
        { field: "Country" },
        { field: "Phone" },
        { field: "Fax" } ]'
      data-bind="source: dataSource, events:
        { change: onChange, dataBound: onDataBound }">
    </div>
    <style scoped>
    #grid .k-toolbar {
      padding: 15px;
    }
    .toolbar {
      float: right;
    }
    </style>
  </div>
</div>
<script type="text/x-kendo-template" id="toolbar">
  <div>
    <div class="toolbar">
      <span data-role="button" data-bind="click: edit">
        <span class="k-icon k-i-tick"></span>Edit</span>
      <span data-role="button" data-bind="click: destroy">
        <span class="k-icon k-i-tick"></span>Delete</span>
      <span data-role="button" data-bind="click: details">
        <span class="k-icon k-i-tick"></span>Edit Details</span>
    </div>
    <div class="toolbar" style="display:none">
      <span data-role="button" data-bind="click: save">
        <span class="k-icon k-i-tick"></span>Save</span>
      <span data-role="button" data-bind="click: cancel">
        <span class="k-icon k-i-tick"></span>Cancel</span>
    </div>
  </div>
</script>

Erstellen des OData-basierten MVC-Web-API-Controllers „Customer“ (Northwind.Web/Api/CustomerController.cs) Nun zeige ich Ihnen, wie Sie den OData-basierten MVC-Web-API-Controller „Customer“ erstellen. OData ist ein Datenzugriffsprotokoll für das Web, das das Abfragen und Bearbeiten von Datensätzen über CRUD-Operationen vereinheitlicht. Mithilfe der ASP.NET-Web-API lässt sich ein OData-Endpunkt ganz leicht erstellen. Sie können steuern, welche OData-Operationen zur Verfügung gestellt werden. Sie können mehrere OData-Endpunkte zusammen mit Endpunkten, die nicht auf OData basieren, hosten. Sie können Ihr Datenmodell, die Back-End-Geschäftslogik und die Datenschicht vollständig steuern. Abbildung 6 zeigt den Code für den Web-API-OData-Controller „Customer“.

Mit dem Code in Abbildung 6 wird einfach ein OData-basierter Web-API-Controller erstellt, der Customer-Daten aus der Northwind-Datenbank verfügbar macht. Wenn Sie den Controller erstellt haben, können Sie das Projekt ausführen und mit einem Tool wie Fiddler (kostenloser Web-Debugger unter fiddler2.com) oder LINQPad Customer-Daten abfragen.

Abbildung 6: OData-basierter Web-API-Controller „Customer“

public class CustomerController : EntitySetController<Customer, string>
{
  private readonly NorthwindContext _northwindContext;
  public CustomerController()
  {
    _northwindContext = new NorthwindContext();
  }
  public override IQueryable<Customer> Get()
  {
    return _northwindContext.Customers;
  }
  protected override Customer GetEntityByKey(string key)
  {
    return _northwindContext.Customers.Find(key);
  }
  protected override Customer UpdateEntity(string key, Customer update)
  {
    _northwindContext.Customers.AddOrUpdate(update);
    _northwindContext.SaveChanges();
    return update;
  }
  public override void Delete(string key)
  {
    var customer = _northwindContext.Customers.Find(key);
    _northwindContext.Customers.Remove(customer);
    _northwindContext.SaveChanges();
  }
}

Konfigurieren und Bereitstellen von OData aus der Customer-Tabelle für das Raster (Northwind.Web/App_Start/WebApiConfig.cs) In Abbildung 7 wird OData aus der Customer-Tabelle für das Raster konfiguriert und bereitgestellt.

Abfragen der OData-Web-API mit LINQPad Wenn Sie LINQPad (linqpad.net) bislang noch nicht verwendet haben, wird es Zeit, dieses Tool in Ihr Entwicklertoolkit aufzunehmen. Es ist ein echtes Muss und als kostenlose Version erhältlich. Abbildung 8 zeigt LINQPad mit einer Verbindung zu Web-API-OData (localhost:2501/odata). Hier werden die Ergebnisse der LINQ-Abfrage „Customer.Take (100)“ dargestellt.

Abbildung 7: Konfigurieren von ASP.NET MVC-Web-API-Routen für OData

public static void Register(HttpConfiguration config)
{
  // Web API configuration and services
  ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
  var customerEntitySetConfiguration =
    modelBuilder.EntitySet<Customer>("Customer");
  customerEntitySetConfiguration.EntityType.Ignore(t => t.Orders);
  customerEntitySetConfiguration.EntityType.Ignore(t =>
     t.CustomerDemographics);
  var model = modelBuilder.GetEdmModel();
  config.Routes.MapODataRoute("ODataRoute", "odata", model);
  config.EnableQuerySupport();
  // Web API routes
  config.MapHttpAttributeRoutes();
  config.Routes.MapHttpRoute(
    "DefaultApi", "api/{controller}/{id}",
    new {id = RouteParameter.Optional});
}

Querying the Customer Controller Web API OData Via a LINQPad Query
Abbildung 8: Abfragen von Web-API-OData für den Customer-Controller über eine LINQPad-Abfrage

Erstellen des (überwachbaren) Customer-Modells (Northwind.Web/­Scripts/app/models/customerModel.js) Als Nächstes erstellen wir das (von Kendo UI überwachbare) Customer-Modell. Sie können sich das als clientseitiges Customer-Entitätsdomänenmodell vorstellen. Ich habe das Customer-Modell erstellt, damit es leicht in der Customer-Rasteransicht und in der Bearbeitungsansicht wiederverwendet werden kann. Dieser Code ist in Abbildung 9 dargestellt.

Abbildung 9: Erstellen des (von Kendo UI überwachbaren) Customer-Modells

define(['kendo'],
  function (kendo) {
    var customerModel = kendo.data.Model.define({
      id: "CustomerID",
      fields: {
        CustomerID: { type: "string", editable: false, nullable: false },
        CompanyName: { title: "Company", type: "string" },
        ContactName: { title: "Contact", type: "string" },
        ContactTitle: { title: "Title", type: "string" },
        Address: { type: "string" },
        City: { type: "string" },
        PostalCode: { type: "string" },
        Country: { type: "string" },
        Phone: { type: "string" },
        Fax: { type: "string" },
        State: { type: "string" }
      }
    });
    return customerModel;
  });

Erstellen einer DataSource-Komponente für das Customers-Raster (Northwind.Web/Scripts/app/datasources/customersDatasource.js) Wenn Sie sich mit Datenquellen von ASP.NET-Web Forms auskennen, ist Ihnen dieses Konzept nicht neu. Hier erstellen wir eine Datenquelle für das Customers-Raster (Northwind.Web/Scripts/app/datasources/customersDatasource.js). Die DataSource-Komponente von Kendo UI (bit.ly/1d0Ycvd) ist eine abstrakte Komponente für die Verwendung von lokalen Daten (JavaScript-Objektarrays) oder Remotedaten (XML, JSON oder JSONP). Neben der vollständigen Unterstützung für CRUD-Datenvorgänge bietet diese Komponente lokale sowie serverseitige Unterstützung für Sortier-, Paginier-, Filter- und Gruppiervorgänge wie auch Aggregate.

Erstellen des Ansichtsmodells für die Customers-Rasteransicht Wenn Sie MVVM von Windows Presentation Foundation (WPF) oder Silverlight kennen, dann wird Ihnen dieser Prozess vertraut vorkommen, er findet lediglich auf der Clientseite statt (in diesem Projekt unter „Northwind.Web/Scripts/ViewModels/­Customer/indexViewModel.cs“). MVVM ist ein architektonisches Trennungsmuster, mit dem die Ansicht von den Daten und der Geschäftslogik getrennt wird. Weiter unten werden Sie sehen, dass sich alle Daten und die Geschäftslogik usw. im Ansichtsmodell befinden und dass es sich bei der Ansicht um reines HTML handelt (Präsentation). Abbildung 10 zeigt den Code für die Customer-Rasteransicht.

Abbildung 10: Modell der Customer-Rasteransicht

define(['kendo', 'customerDatasource'],
  function (kendo, customerDatasource) {
    var lastSelectedDataItem = null;
    var onClick = function (event, delegate) {
      event.preventDefault();
      var grid = $("#grid").data("kendoGrid");
      var selectedRow = grid.select();
      var dataItem = grid.dataItem(selectedRow);
      if (selectedRow.length > 0)
        delegate(grid, selectedRow, dataItem);
      else
        alert("Please select a row.");
      };
      var indexViewModel = new kendo.data.ObservableObject({
        save: function (event) {
          onClick(event, function (grid) {
            grid.saveRow();
            $(".toolbar").toggle();
          });
        },
        cancel: function (event) {
          onClick(event, function (grid) {
            grid.cancelRow();
            $(".toolbar").toggle();
          });
        },
        details: function (event) {
          onClick(event, function (grid, row, dataItem) {
            router.navigate('/customer/edit/' + dataItem.CustomerID);
          });
        },
        edit: function (event) {
          onClick(event, function (grid, row) {
            grid.editRow(row);
            $(".toolbar").toggle();
          });
        },
        destroy: function (event) {
          onClick(event, function (grid, row, dataItem) {
            grid.dataSource.remove(dataItem);
            grid.dataSource.sync();
          });
        },
        onChange: function (arg) {
          var grid = arg.sender;
          lastSelectedDataItem = grid.dataItem(grid.select());
        },
        dataSource: customerDatasource,
        onDataBound: function (arg) {
          // Check if a row was selected
          if (lastSelectedDataItem == null) return;
          // Get all the rows     
          var view = this.dataSource.view();
          // Iterate through rows
          for (var i = 0; i < view.length; i++) {
          // Find row with the lastSelectedProduct
            if (view[i].CustomerID == lastSelectedDataItem.CustomerID) {
              // Get the grid
              var grid = arg.sender;
              // Set the selected row
              grid.select(grid.table.find("tr[data-uid='" + view[i].uid + "']"));
              break;
            }
          }
        },
      });
      return indexViewModel;
  });

Ich erläutere kurz die verschiedenen Codekomponenten aus Abbildung 10:

  • onClick (Hilfsmethode): Hierbei handelt es sich um eine Hilfsfunktion, die eine Instanz des Customer-Rasters, die ausgewählte Zeile und ein JSON-Modell des dargestellten Customer-Elements für die ausgewählte Zeile abruft.
  • save: Speichert Änderungen bei der Inlinebearbeitung eines Customer-Elements.
  • cancel: Bricht den Inlinebearbeitungsmodus ab.
  • details: Leitet die SPA zur Customer-Bearbeitungsansicht weiter und fügt die Customer-ID an die URL an.
  • edit: Aktiviert die Inlinebearbeitung für das ausgewählte Customer-Element.
  • destroy: Löscht das ausgewählte Customer-Element.
  • onChange (Ereignis): Wird jedes Mal aktiviert, wenn ein Customer-Element ausgewählt wird. Sie speichern das zuletzt ausgewählte Customer-Element, damit der Status beibehalten wird. Wenn Sie Updates ausgeführt haben oder vom Customer-Raster weg navigiert wurde, wählen Sie das zuletzt ausgewählte Customer-Element erneut aus, wenn Sie wieder zum Raster navigieren.

Nun fügen Sie die Module „customerModel“, „indexViewModel“ und „customersDatasource“ zur RequireJS-Konfiguration hinzu (Northwind.Web/Scripts/app/main.js). Dieser Code ist in Abbildung 11 dargestellt.

Abbildung 11: Zusätzliche Elemente der RequireJS-Konfiguration

paths: {
  // Packages
  'jquery': '/scripts/jquery-2.0.3.min',
  'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
  'text': '/scripts/text',
  'router': '/scripts/app/router',
  // Models
  'customerModel': '/scripts/app/models/customerModel',
  // View models
  'customer-indexViewModel': '/scripts/app/viewmodels/customer/indexViewModel',
  'customer-editViewModel': '/scripts/app/viewmodels/customer/editViewModel',
  // Data sources
  'customerDatasource': '/scripts/app/datasources/customerDatasource',
  // Utils
  'util': '/scripts/util'
}

Hinzufügen einer Route für die neue Customers-Rasteransicht Beachten Sie, dass Sie die Symbolleiste des Rasters im loadView-Rückruf (in „Northwind.Web/Scripts/app/router.js“) binden, nachdem sie initialisiert wurde und die MVVM-Bindung stattgefunden hat. Der Grund dafür ist, dass die Symbolleiste bei der ersten Bindung des Rasters noch nicht initialisiert wurde, da sie im Raster vorhanden ist. Wenn das Raster zum ersten Mal per MVVM initialisiert wird, wird es von der Kendo UI-Vorlage in die Symbolleiste geladen. Nach dem Laden im Raster binden Sie nur die Symbolleiste an Ihr Ansichtsmodell, sodass die Schaltflächen auf der Symbolleiste an die Speicher- und Abbruchmethoden in Ihrem Ansichtsmodell gebunden sind. Hier ist der Code, mit dem Sie die Routendefinition für die Customer-Bearbeitungsansicht registrieren:

router.route("/customer/index", function () {
  require(['customer-indexViewModel', 'text!/customer/index'],
    function (viewModel, view) {
      loadView(viewModel, view, function () {
        kendo.bind($("#grid").find(".k-grid-toolbar"), viewModel);
      });
    });
});

Nun ist die Customers-Rasteransicht voll funktionsfähig. Laden Sie „localhost:25061/Home/Spa#/customer/index“ (für Ihren Computer gilt wahrscheinlich eine andere Portnummer) in einem Browser, und Sie erhalten ein Ergebnis wie in Abbildung 12.

The Customer Grid View with MVVM Using the Index View Model
Abbildung 12: Customer-Rasteransicht mit MVVM unter Verwendung des Indexansichtsmodells

Verknüpfen der Customer-Bearbeitungsansicht Mit den folgenden grundlegenden Schritten fügen Sie der SPA eine Customer-Bearbeitungsansicht hinzu:

  • Erstellen einer Customer-Bearbeitungsansicht, die per MVVM an Ihr Customer-Modell gebunden ist (Northwind.Web/Views/Customer/Edit.cshtml)
  • Hinzufügen eines Moduls für das Bearbeitungsansichtsmodell für die Customer-Bearbeitungsansicht (Northwind.Web/Scripts/app/viewModels/­editViewModel.js)
  • Hinzufügen eines Dienstprogramm-Hilfsmoduls zum Abrufen von IDs aus der URL (Northwind.Web/Scripts/app/util.js)

Da Sie das Kendo UI-Framework verwenden, können Sie zum Formatieren der Bearbeitungsansicht Kendo UI-Formatvorlagen verwenden. Weitere Informationen dazu erhalten Sie unter bit.ly/1f3zWuC. Abbildung 13 zeigt das Markup der Bearbeitungsansicht mit einem MVVM-Widget und einer Ereignisbindung.

Abbildung 13: Markup der Bearbeitungsansicht mit einem MVVM-Widget und einer Ereignisbindung

<div class="demo-section">
  <div class="k-block" style="padding: 20px">
    <div class="k-block k-info-colored">
      <strong>Note: </strong>Please fill out all of the fields in this form.
    </div>
    <div>
      <dl>
        <dt>
          <label for="companyName">Company Name:</label>
        </dt>
        <dd>
          <input id="companyName" type="text"
            data-bind="value: Customer.CompanyName" class="k-textbox" />
        </dd>
        <dt>
          <label for="contactName">Contact:</label>
        </dt>
        <dd>
          <input id="contactName" type="text"
            data-bind="value: Customer.ContactName" class="k-textbox" />
        </dd>
        <dt>
          <label for="title">Title:</label>
        </dt>
        <dd>
          <input id="title" type="text"
            data-bind="value: Customer.ContactTitle" class="k-textbox" />
        </dd>
        <dt>
          <label for="address">Address:</label>
        </dt>
        <dd>
          <input id="address" type="text"
            data-bind="value: Customer.Address" class="k-textbox" />
        </dd>
        <dt>
          <label for="city">City:</label>
        </dt>
        <dd>
          <input id="city" type="text"
            data-bind="value: Customer.City" class="k-textbox" />
        </dd>
        <dt>
          <label for="zip">Zip:</label>
        </dt>
        <dd>
          <input id="zip" type="text"
            data-bind="value: Customer.PostalCode" class="k-textbox" />
        </dd>
        <dt>
          <label for="country">Country:</label>
        </dt>
        <dd>
          <input id="country" type="text"
          data-bind="value: Customer.Country" class="k-textbox" />
        </dd>
        <dt>
          <label for="phone">Phone:</label>
        </dt>
        <dd>
          <input id="phone" type="text"
            data-bind="value: Customer.Phone" class="k-textbox" />
        </dd>
        <dt>
          <label for="fax">Fax:</label>
        </dt>
        <dd>
          <input id="fax" type="text"
            data-bind="value: Customer.Fax" class="k-textbox" />
        </dd>
      </dl>
      <button data-role="button"
        data-bind="click: saveCustomer"
        data-sprite-css-class="k-icon k-i-tick">Save</button>
      <button data-role="button" data-bind="click: cancel">Cancel</button>
      <style scoped>
        dd
        {
          margin: 0px 0px 20px 0px;
          width: 100%;
        }
        label
        {
          font-size: small;
          font-weight: normal;
        }
        .k-textbox
        {
          width: 100%;
        }
        .k-info-colored
        {
          padding: 10px;
          margin: 10px;
        }
      </style>
    </div>
  </div>
</div>

Erstellen eines Dienstprogramms zum Abrufen der Customer-ID aus der URL Sie legen präzise Module mit klaren Grenzen an und erzielen so eine schöne Trennung der verschiedenen Bereiche. Ich zeige Ihnen nun, wie Sie ein Util-Modul erstellen, in dem alle Hilfsprogramme für Ihre Dienstprogramme gespeichert werden. Wir fangen mit einer Dienstprogrammmethode an, die wie in Abbildung 14 gezeigt die Customer-ID in der URL für die Customer-Datenquelle abrufen kann (Northwind.Web/Scripts/app/datasources/customerDatasource.js).

Abbildung 14: Dienstprogrammmodul

define([],
  function () {
    var util;
    util = {
      getId:
      function () {
        var array = window.location.href.split('/');
        var id = array[array.length - 1];
        return id;
      }
    };
    return util;
  });

Hinzufügen des Moduls für das Bearbeitungsansichtsmodell und des Util-Moduls zur RequireJS-Konfiguration (Northwind.Web/Scripts/app/main.js) Der Code in Abbildung 15 stellt die zusätzlichen Elemente der RequireJS-Konfiguration für die Customer-Bearbeitungsmodule dar.

Abbildung 15: Zusätzliche Elemente der RequireJS-Konfiguration für die Customer-Bearbeitungsmodule

require.config({
  paths: {
    // Packages
    'jquery': '/scripts/jquery-2.0.3.min',
    'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
    'text': '/scripts/text',
    'router': '/scripts/app/router',
    // Models
    'customerModel': '/scripts/app/models/customerModel',
    // View models
    'customer-indexViewModel': '/scripts/app/viewmodels/customer/indexViewModel',
    'customer-editViewModel': '/scripts/app/viewmodels/customer/editViewModel',
    // Data sources
    'customerDatasource': '/scripts/app/datasources/customerDatasource',
    // Utils
    'util': '/scripts/util'
    },
  shim : {
    'kendo' : ['jquery']
  },
  priority: ['text', 'router', 'app'],
  jquery: '2.0.3',
  waitSeconds: 30
  });
require([
  'app'
], function (app) {
  app.initialize();
});

Hinzufügen des Modells für die Customer-Bearbeitungsansicht (Northwind.Web/Scripts/app/viewModels/editViewModel.js) Der Code in Abbildung 16 illustriert, wie Sie ein Modell für die Customer-Bearbeitungsansicht hinzufügen.

Abbildung 16: Modul des Customer-Bearbeitungsansichtsmodells für die Customer-Ansicht

define(['customerDatasource', 'customerModel', 'util'],
  function (customerDatasource, customerModel, util) {
    var editViewModel = new kendo.data.ObservableObject({
      loadData: function () {
        var viewModel = new kendo.data.ObservableObject({
          saveCustomer: function (s) {
            customerDatasource.sync();
            customerDatasource.filter({});
            router.navigate('/customer/index');
          },
          cancel: function (s) {
            customerDatasource.filter({});
            router.navigate('/customer/index');
          }
        });
        customerDatasource.filter({
          field: "CustomerID",
          operator: "equals",
          value: util.getId()
        });
        customerDatasource.fetch(function () {
          console.log('editViewModel fetching');
          if (customerDatasource.view().length > 0) {
            viewModel.set("Customer", customerDatasource.at(0));
          } else
            viewModel.set("Customer", new customerModel());
        });
        return viewModel;
      },
    });
    return editViewModel;
  });

Ich erläutere kurz die verschiedenen Codekomponenten aus Abbildung 16:

  • saveCustomer: Diese Methode ist für das Speichern von Änderungen am Customer-Element verantwortlich. Außerdem setzt sie den Filter für das DataSource-Element zurück, damit das Raster mit allen Customer-Elementen hydratisiert wird.
  • cancel: Diese Methode leitet die SPA zurück zur Customer-Rasteransicht. Außerdem setzt sie den Filter für das DataSource-Element zurück, damit das Raster mit allen Customer-Elementen hydratisiert wird.
  • filter: Ruft die Filtermethode für das DataSource-Element auf und startet basierend auf der ID in der URL eine Abfrage für ein bestimmtes Customer-Element.
  • fetch: Richtet den Filter ein und ruft dann die fetch-Methode für das DataSource-Element auf. Im Rückruf der fetch-Methode legen Sie die Customer-Eigenschaft Ihres Ansichtsmodells auf das Customer-Element fest, das vom DataSource-Abrufvorgang zurückgegeben wurde. Dieses Element wird für die Bindung an die Customer-Bearbeitungsansicht verwendet.

Wenn RequireJS ein Modul lädt, wird Code innerhalb des define-Methodentexts nur einmal aufgerufen, und zwar beim Laden des Moduls. Aus diesem Grund stellen Sie eine Methode („loadData“) in Ihrem Bearbeitungsansichtsmodell zur Verfügung, damit ein Mechanismus zum Laden von Daten vorhanden ist, nachdem das Bearbeitungsansichtsmodell geladen wurde (siehe Northwind.Web/­Scripts/app/router.js).

Hinzufügen einer Route für die neue Customer-Bearbeitungsansicht (Northwind.Web/Scripts/app/router.js) Der relevante Code zum Hinzufügen des Routers lautet folgendermaßen:

router.route("/customer/edit/:id",
        function () {
    require(['customer-editViewModel',
          'text!/customer/edit'],
      function (viewModel, view) {
      loadView(viewModel.loadData(), view);
    });
  });

Wenn das Modell der Customer-Bearbeitungsansicht von RequireJS angefordert wird, können Sie das Customer-Element abrufen, indem Sie die loadData-Methode vom Ansichtsmodell aufrufen. So können Sie jedes Mal, wenn die Customer-Bearbeitungsansicht geladen wird, die korrekten Customer-Daten anhand der ID in der URL laden. Eine Route muss nicht immer nur eine hartcodierte Zeichenfolge sein. Sie kann auch Parameter enthalten, zum Beispiel einen Back-End-Server-Router (Ruby on Rails, ASP.NET MVC, Django oder ähnliche). Dazu setzen Sie ein Routensegment mit einem Doppelpunkt vor den gewünschten Variablennamen.

Nun können Sie die Customer-Bearbeitungsansicht im Browser laden (localhost:25061/Home/Spa#/customer/edit/ANATR), und es wird das in Abbildung 17 dargestellte Fenster angezeigt.

The Customer Edit View
Abbildung 17: Customer-Bearbeitungsansicht

Anmerkung: Obwohl die Löschfunktion („destroy“) in der Customer-Rasteransicht verknüpft wurde, kommt es zu einer Ausnahme (siehe Abbildung 19), wenn Sie auf der Symbolleiste auf die Schaltfläche zum Löschen klicken (siehe Abbildung 18).

The Customer Grid View
Abbildung 18: Customer-Rasteransicht

Expected Exception When Deleting a Customer Due to CustomerID Foreign Key Referential Integrity
Abbildung 19: Erwartete Ausnahme beim Löschen eines Customer-Elements aufgrund der referenziellen Integrität des CustomerID-Fremdschlüssels

Diese Ausnahme wurde absichtlich eingerichtet, da die meisten Customer-IDs Fremdschlüssel in anderen Tabellen sind, zum Beispiel Bestellungen, Rechnungen usw. Sie müssten einen überlappenden Löschvorgang verknüpfen, der alle Datensätze aus allen Tabellen löscht, in denen die Customer-ID als Fremdschlüssel vorhanden ist. Zwar können Sie in der Praxis nichts löschen, aber ich wollte Ihnen trotzdem die Schritte und den Code für die Löschfunktion zeigen.

Damit sind wir am Ende angelangt. Ich habe veranschaulicht, wie schnell und einfach Sie eine standardmäßige ASP.NET-Webanwendung mithilfe von RequireJS und Kendo UI in eine SPA konvertieren können. Außerdem habe ich gezeigt, wie leicht das Hinzufügen von CRUD-Funktionen zur SPA ist.

Eine Livedemo des Projekts finden Sie unter bit.ly/1bkMAlK, und besuchen Sie die CodePlex-Projektwebsite (inklusive herunterladbarem Code) unter easyspa.codeplex.com.

Viel Spaß beim Programmieren!

Long Le ist der leitende .NET-App/Dev-Architekt bei CBRE Inc. und Telerik/Kendo UI-MVP. Er verbringt die meiste Zeit mit der Entwicklung von Frameworks und Anwendungsblöcken, dem Erstellen von Best Practices und Anleitungen für Muster sowie der Standardisierung von Unternehmenstechnologie. Long Le arbeitet seit mehr als zehn Jahren mit Microsoft-Technologien. In seiner Freizeit ist er ein aktiver Blogger (blog.longle.net) und spielt gerne Call of Duty. Sie erreichen ihn auf Twitter unter twitter.com/LeLong37.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Derick Bailey (Telerik) und Mike Wasson (Microsoft)