MSDN Magazin > Home > Ausgaben > 2008 > March >  ASP.NET MVC: Erstellen von Webanwendungen ...
ASP.NET MVC
Erstellen von Webanwendungen ohne Webformulare
Chris Tavares

Dieser Artikel basiert auf der Vorabversion von ASP.NET MVC Framework. Änderungen der Details in diesem Artikel sind vorbehalten.
Themen in diesem Artikel:
  • Model View Controller-Muster
  • Erstellen von Controllern und Ansichten
  • Erstellen von Formularen und Zurücksenden
  • Controller Factorys und andere Erweiterungspunkte
In diesem Artikel werden folgende Technologien verwendet:
ASP.NET
Codedownload verfügbar unter: MVCFramework2008_03.exe (189 KB)
Browse the Code Online
Ich bin jetzt seit etwa 15 Jahren professioneller Entwickler und war zuvor mindestens 10 Jahre als Hobbyentwickler tätig. Wie die meisten meiner Generation begann ich auf 8-Bit-Rechnern und wechselte dann zur PC-Plattform über. Auf immer komplexeren Rechnern schrieb ich alle möglichen Anwendungen, von kleinen Spielen bis hin zu persönlichem Datenmanagement und der Steuerung externer Hardware.
In der ersten Hälfte meines Berufslebens hatte jedoch sämtliche von mir geschriebene Software eines gemeinsam: Es handelte sich immer um lokale Anwendungen, die auf dem Desktop eines Benutzers liefen. Anfang der 90er Jahre hörte ich immer öfter von einer Neuheit, die sich das World Wide Web nannte. Ich sah darin eine Möglichkeit, eine Webanwendung zur Eingabe von Arbeitszeitnachweisen zu erstellen, die direkt vom Arbeitsort erfolgen konnte, ohne mich physisch zurück ins Büro zu bewegen.
Die Erfahrung war mir damals ein völliges Rätsel. Mit dem zustandslosen Web zurechtzukommen, passte einfach nicht zu meiner Desktop-orientierten Denkweise. Dazu kamen miserables Debuggen, ein UNIX-Server, auf den ich keinen Stammzugriff hatte, und diese komischen spitzen Klammern. Jedenfalls kehrte ich für einige weitere Jahre beschämt zur Desktopentwicklung zurück.
Obwohl mir die Webentwicklung wichtig erschien, hielt ich mich davon fern, da ich das Programmiermodell einfach nicht richtig verstand. Dann aber wurden Microsoft® .NET Framework und ASP.NET veröffentlicht. Endlich gab es ein Framework, mit dem Webanwendungen fast genauso wie Desktopanwendungen programmiert werden konnten. Ich konnte Fenster (Seiten) erstellen, Steuerelemente mit Ereignissen verbinden, und dank des Designers musste ich mich nicht mit diesen merkwürdigen spitzen Klammern beschäftigen. Am Besten war, dass mir ASP.NET das zustandslose Internet automatisch im Ansichtzustand darstellte! Ich war als Programmierer wieder glücklich ... zumindest für eine Weile.
Zusammen mit meiner Erfahrung wuchs auch meine Designauswahl. Ich hatte mehrere bewährte Methoden gelernt, die ich bei der Arbeit an meinen Desktopanwendungen verwendete. Dazu gehörten die beiden folgenden:
  • Trennung von Bereichen: Die Benutzeroberflächenlogik sollte nie mit dem zugrunde liegenden Verhalten vermischt werden.
  • Automatisierte Komponententests: Schreiben automatisierter Tests zum Überprüfen, ob der Code auch wirklich die gewünschten Aufgaben ausführt.
Die diesen Methoden jeweils zugrunde liegenden Prinzipien gelten unabhängig von der Technologie. Die Trennung von Bereichen ist ein grundlegendes Prinzip, das Ihnen beim Umgang mit Komplexität behilflich ist. Durch Vermischen unterschiedlicher Verantwortlichkeiten innerhalb desselben Objekts – wie die Berechnung verbleibender Arbeitsstunden, die Formatierung von Daten und das Zeichnen eines Diagramms – werden Wartungsprobleme geradezu herausgefordert. Automatisiertes Testen wiederum ist entscheidend für die Erstellung von Code mit Produktionsqualität, ohne dass Sie den Verstand verlieren, insbesondere wenn Sie ein bestehendes Projekt aktualisieren.
Die Webformulare von ASP.NET erleichterten den Anfang sehr, doch andererseits war es schwierig, meine Designprinzipien auf Webanwendungen zu übertragen. Webformulare sind unerbittlich auf die Benutzeroberfläche fokussiert. Der grundlegende Bestandteil ist nun einmal die Seite. Zunächst werden die Benutzeroberfläche entworfen und die Steuerelemente darauf platziert. Es ist sehr verführerisch, am Anfang einfach die jeweilige Anwendungslogik in die Ereignishandler der Seite zu setzen (ähnlich wie bei Visual Basic®-Anwendungen, die für Windows® aktiviert sind).
Darüber hinaus sind Komponententests von Seiten oft schwierig. Sie können kein Seitenobjekt über den gesamten Lebenszyklus hinweg ausführen, ohne ASP.NET als Ganzes einzubeziehen. Es ist zwar möglich, Webanwendungen durch Senden von HTTP-Anforderungen an einen Server oder Automatisieren eines Browsers zu testen, aber diese Art des Testens ist anfällig (der Test bricht zusammen, wenn Sie lediglich eine Steuerelement-ID ändern), schwer einzurichten (Sie müssen den Server auf den Rechnern aller Entwickler auf genau die gleiche Weise einrichten) und langsam in der Ausführung.
Als ich mich an kompliziertere Webanwendungen wagte, waren die durch Webformulare bereitgestellten Abstraktionen, beispielsweise Steuerelemente, Ansichtszustand und der Lebenszyklus der Seite, eher hinderlich als hilfreich. Ich habe immer mehr Zeit für die Konfiguration der Datenbindung aufgewendet (und massenweise Ereignishandler geschrieben, um sie korrekt zu konfigurieren). Folglich musste ich herausfinden, wie die Größe des Ansichtszustands verringert werden konnte, um die Seiten schneller zu laden. Webformulare erfordern eine physische Datei für jede URL, wodurch dynamische Seiten (wie zum Beispiel ein Wiki) schwierig werden. Das erfolgreiche Schreiben eines benutzerdefinierten Websteuerelements ist ein bemerkenswert komplexer Prozess, der ein umfangreiches Verständnis sowohl des Lebenszyklus' einer Seite als auch von Visual Studio® Designer erfordert.
Während meiner Zeit bei Microsoft hatte ich die Gelegenheit, meine Erkenntnisse über verschiedene Schmerzpunkte von .NET weiterzugeben und hoffentlich die Qual etwas zu lindern. Kürzlich ergab sich eine solche Möglichkeit bei meiner Teilnahme als Entwickler am Patterns & Practices Web Client Software Factory-Projekt (codeplex. Com/websf). Die Einbindung von Komponententests ist bei den Ergebnissen des Patterns & Practices-Team ein wichtiger Punkt. Bei der Webclient Software Factory wurde die Verwendung des Model View Presenter (MVP)-Musters für das Erstellen testbarer Webformulare vorgeschlagen.
Kurz gesagt erstellen Sie mit MVP Ihre Seiten so, dass die Seite (Ansicht) einfach Aufrufe an ein separates Objekt, den Presenter, ausführt. Sie integrieren also nicht die Logik in die Seite. Das Presenter-Objekt führt dann die erforderliche Logik aus, um auf die Aktivität der Ansicht zu regieren, normalerweise unter Verwendung anderer Objekte (des Modells), um auf Datenbanken zuzugreifen, Geschäftslogik auszuführen und so weiter. Nach Beenden dieser Schritte wird die Ansicht durch den Presenter aktualisiert. Dieser Ansatz bietet Testfähigkeit, da der Presenter von der ASP.NET-Pipeline isoliert ist. Er kommuniziert mit der Ansicht über eine Schnittstelle und kann isoliert von der Seite getestet werden.
MVP funktioniert, aber die Implementierung kann etwas eigenartig sein. Sie benötigen eine separate Ansichtschnittstelle und müssen eine Menge Ereignisweiterleitungsfunktionen in die Codebehind-Dateien schreiben. Aber wenn Sie eine testfähige Benutzeroberfläche für Ihre Webformularanwendungen wünschen, ist dies zurzeit die beste Möglichkeit. Für mögliche Verbesserungen wäre eine Änderung der zugrunde liegenden Plattform erforderlich.

Model View Controller-Muster
Glücklicherweise hat das ASP.NET Team Entwicklern wie mir Gehör geschenkt und die Entwicklung eines neuen Webanwendungsframeworks in Angriff genommen, das direkt neben dem allgemein bekannten und geschätzten Webformular arbeitet, jedoch völlig andere Entwurfsziele verfolgt:
  • Integration von HTTP und HTML, anstatt diese auszublenden.
  • Testfähigkeit ist von Grund auf integriert.
  • Erweiterbar an fast jedem Punkt.
  • Vollkommene Kontrolle über die Ausgabe.
Dieses neue Framework basiert auf dem Model View Controller (MVC)-Muster, daher der Name ASP.NET MVC. Das MVC-Muster wurde ursprünglich in den 70er Jahren als Teil von Smalltalk erfunden. Wie in diesem Artikel aufgezeigt wird, passt es hervorragend zur Funktionsweise des Internets. MVC teilt die Benutzeroberfläche in drei unterschiedliche Objekte ein: den Controller, der Eingaben empfängt und behandelt, das Modell, das die Domänenlogik enthält, und die Ansicht, die die Ausgabe generiert. Im Kontext des Internets ist die Eingabe eine HTTP-Anforderung, und der Anforderungsfluss sieht wie Abbildung 1 aus.
Abbildung 1 MVC-Musteranforderungsfluss (Klicken Sie zum Vergrößern auf das Bild)
Dies unterscheidet sich erheblich vom Prozess in Webformularen. Im Webformularmodell geht die Eingabe zur Seite (Ansicht), und die Ansicht ist sowohl für die Verarbeitung der Eingabe als auch das Generieren der Ausgabe verantwortlich. Bei MVC sind die Verantwortlichkeiten getrennt.
Wahrscheinlich gehen Ihnen jetzt zwei Dinge durch den Kopf. Entweder: „Das ist großartig. Wie genau geht das?“ oder: „Warum sollte ich drei Objekte schreiben, wenn ich zuvor nur eins schreiben musste?“ Beides sind gute Fragen und können am besten anhand eines Beispiels erklärt werden. Um die Vorzüge diese Methode zu veranschaulichen, zeige ich Ihnen hier das Schreiben einer kleinen Webanwendung mithilfe von MVC Framework auf.

Erstellen eines Controllers
Um folgen zu können, müssen Sie Visual Studio 2008 installieren und eine Kopie von MVC Framework abrufen. Zum Zeitpunkt dieses Artikels ist es als Teil des Community Technology Preview (CTP) der ASP.NET-Erweiterungen vom Dezember 2007 verfügbar (asp.net/downloads/3.5-extensions). Laden Sie die CTP-Erweiterungen und das MVC-Toolkit herunter, das einige nützliche Hilfsobjekte enthält. Nachdem Sie CTP heruntergeladen und installiert haben, wird im Dialogfeld für neue Projekte ein neues ASP.NET MVC Webanwendungsprojekt angezeigt.
Durch Auswählen dieses MVC-Webanwendungsprojekts erhalten Sie eine Lösung, die etwas anders aussieht als eine normale Website oder Anwendung. Die Lösungsvorlage erstellt eine Webanwendung mit einigen neuen Verzeichnissen (siehe Abbildung 2). Vor allem enthält das Controllerverzeichnis die Controllerklassen, und das Ansichtenverzeichnis (und alle seine Unterverzeichnisse) enthält die Ansichten.
Abbildung 2 Die MVC-Projektstruktur 
Sie schreiben jetzt einen sehr einfachen Controller, der einen Namen zurückgibt, der auf der URL übergeben wird. Klicken Sie mit der rechten Maustaste auf den Ordner „Controller“. Durch Auswahl von „Element hinzufügen“ wird das bekannte Dialogfeld „Neues Element hinzufügen“ mit einigen neuen Elementen angezeigt, einschließlich einer MVC-Controllerklasse und verschiedenen MVC-Ansichtskomponenten. In diesem Fall fügen Sie eine Klasse namens „HelloController“ hinzu:
using System;
using System.Web;
using System.Web.Mvc;

namespace HelloFromMVC.Controllers
{
    public class HelloController : Controller
    {
        [ControllerAction]
        public void Index()
        {
            ...
        }
    }
}
Eine Controllerklasse ist viel leichter als eine Seite. Eigentlich sind nur das Ableiten von System.Web.Mvc.Controller und das Weitergeben des Attributs [ControllerAction] an die Aktionsmethoden wirklich erforderlich. Eine Aktion ist eine Methode, die als Reaktion auf eine Anforderung an eine bestimmte URL aufgerufen wird. Aktionen sind für die erforderliche Verarbeitung und das anschließende Rendern einer Ansicht verantwortlich. Zunächst schreiben Sie eine einfache Aktion, die den Namen an die Ansicht übergibt, wie Sie hier sehen:
[ControllerAction]
 public void HiThere(string id)
 {
     ViewData["Name"] = id;
     RenderView("HiThere");
 }
Die Aktionsmethode erhält den Namen von der URL über den id-Parameter (später genaueres über das Wie), speichert ihn in der ViewData-Sammlung und rendert dann eine Ansicht namens HiThere.
Bevor das Aufrufen dieser Methode oder das Aussehen der Ansicht erläutert werden, soll näher auf Testfähigkeit eingegangen werden. Erinnern Sie sich an die vorherigen Anmerkungen darüber, wie schwer Webformularseitenklassen zu testen sind? Nun, Controller lassen sich viel leichter testen. In der Tat ist es ohne zusätzliche Infrastruktur möglich, einen Controller direkt zu instanziieren und Aktionsmethoden aufzurufen. Es sind kein HTTP-Kontext und kein Server erforderlich, lediglich eine Testumgebung. Als Beispiel habe ich für diese Klasse in Abbildung 3 einen Visual Studio Team System (VSTS)-Komponententest eingefügt.
namespace HelloFromMVC.Tests
{
    [TestClass]
    public class HelloControllerFixture
    {
        [TestMethod]
        public void HiThereShouldRenderCorrectView()
        {
            TestableHelloController controller = new 
              TestableHelloController();
            controller.HiThere("Chris");

            Assert.AreEqual("Chris", controller.Name);
            Assert.AreEqual("HiThere", controller.ViewName);
        }

    }

    class TestableHelloController : HelloController
    {
        public string Name;
        public string ViewName;

        protected override void RenderView(
            string viewName, string master, object data)
        {
            this.ViewName = viewName;
            this.Name = (string)ViewData["Name"];
        }
    }

}

Hier gehen verschiedene Dinge vor. Der eigentliche Test ist sehr einfach: Instanziieren Sie den Controller, rufen Sie die Methode mit den erwarteten Daten auf, und überprüfen Sie dann, ob die richtige Ansicht gerendert wurde. Führen Sie die Überprüfung durch Erstellen einer testspezifischen Unterklasse aus, die die RenderView-Methode überschreibt. Dadurch kann die eigentliche Erstellung von HTML umgangen werden. Achten Sie nur darauf, dass die richtigen Daten an die Ansicht gesendet wurden und dass die richtige Ansicht gerendert wurde. Für diesen Test werden die zugrunde liegenden Details der Ansicht außer Acht gelassen.

Erstellen einer Ansicht
Selbstverständlich ist es letzten Endes erforderlich, etwas HTML zu erstellen, also erstellen Sie die HiThere-Ansicht. Dafür erstellen Sie zunächst im Ordner „Ansichten“ der Lösung einen neuen Ordner namens „Hello“. Standardmäßig wird eine Ansicht vom Controller im Ordner „Ansichten\<Controllerpräfix>“ gesucht (das Controllerpräfix ist der Name der Controllerklasse ohne das Wort „Controller“). Nach Ansichten, die von HelloController gerendert wurden, wird also in „Ansichten\Hello“ gesucht. Die Lösung sieht aus wie in Abbildung 4.
Abbildung 4 Hinzufügen einer Ansicht zum Projekt (Klicken Sie zum Vergrößern auf das Bild)
Das HTML für die Ansicht sieht folgendermaßen aus:
<html  >
<head runat="server">
    <title>Hi There!</title>
</head>
<body>
    <div>
        <h1>Hello, <%= ViewData["Name"] %></h1>
    </div>
</body>
</html>
Mehrere Dinge sollten Ihnen sofort auffallen. Es gibt keine runat ="Server"-Tags. Es gibt kein Formulartag. Es gibt keine Steuerelementdeklarationen. Eigentlich sieht das Ganze eher nach klassischem ASP als nach ASP.NET aus. Beachten Sie, dass MVC-Ansichten nur für das Generieren von Ausgaben verantwortlich sind. Es sind also keine Ereignisverarbeitung oder komplexen Steuerelemente erforderlich, wie es bei Webformularen der Fall ist.
Das MVC Framework verwendet das .aspx-Dateiformat als hilfreiche Textvorlagensprache. Sie können sogar Codebehind verwenden, doch standardmäßig sieht die Codebehind-Datei folgendermaßen aus:
using System;
using System.Web;
using System.Web.Mvc;

namespace HelloFromMVC.Views.Hello
{
    public partial class HiThere : ViewPage
    {
    }
}
Es gibt keine Seiten-Init- oder Lademethoden, keine Ereignishandler, nichts außer der Deklaration der Basisklasse, die nicht Page sondern ViewPage ist. Für eine MVC-Ansicht reicht dies völlig aus. Führen Sie die Anwendung aus, navigieren Sie zu http://localhost:<Port>/Hello/HiThere/Chris, und Sie erhalten eine Anzeige wie in Abbildung 5.
Abbildung 5 Erfolgreiche MVC-Ansicht (Klicken Sie zum Vergrößern auf das Bild)
Wenn Sie statt Abbildung 5 eine Ausnahme sehen, geraten Sie nicht in Panik. Wenn die Datei „HiThere.aspx“ in Visual Studio als aktives Dokument festgelegt ist, versucht Visual Studio nach Drücken auf F5 direkt auf die .aspx-Datei zuzugreifen. Da MVC-Ansichten erfordern, dass der Controller vor der Anzeige ausgeführt wird, können Sie nicht direkt zur Seite navigieren. Bearbeiten Sie einfach die URL, damit sie aussieht wie in Abbildung 5, und es müsste funktionieren.
Woher wusste das MVC Framework, dass die Aktionsmethode aufgerufen werden soll? Es gab nicht einmal eine Dateierweiterung für diese URL. Die Antwort ist URL-Routing. Wenn Sie sich die Datei „global.asax.cs“ anschauen, sehen Sie den Code, der in Abbildung 6 dargestellt ist. Die globale RouteTable speichert eine Sammlung von Route-Objekten. Jede Route beschreibt ein URL-Formular und wozu es dient. Standardmäßig werden der Tabelle zwei Routen hinzugefügt. Die erste ist der eigentliche Trick. Sie bestimmt, dass für jede URL, die aus drei Teilen nach dem Servernamen besteht, der erste ein Controllername sein sollte, der zweite ein Aktionsname und der dritte der ID-Parameter:
public class Global : System.Web.HttpApplication
{
    protected void Application_Start(object sender, EventArgs e)
    {
        // Change Url= to Url="[controller].mvc/[action]/[id]" 
        // to enable automatic support on IIS6 

        RouteTable.Routes.Add(new Route
        {
            Url = "[controller]/[action]/[id]",
            Defaults = new { action = "Index", id = (string)null },
            RouteHandler = typeof(MvcRouteHandler)
        });

        RouteTable.Routes.Add(new Route
        {
            Url = "Default.aspx",
            Defaults = new { 
                controller = "Home", 
                action = "Index", 
                id = (string)null },
            RouteHandler = typeof(MvcRouteHandler)
        });
    }
}

Url = "[controller]/[action]/[id]"
Durch diese Standardroute wurde die HiThere-Methode aufgerufen. Erinnern Sie sich an diese URL: http://localhost/Hello/HiThere/Chris? Diese Route hat „Hello“ dem Controller zugeordnet, „HiThere“ der Aktion und „Chris“ der ID. Das MVC Framework hat dann eine HelloController-Instanz erstellt, die HiThere-Methode aufgerufen und Chris als Wert des ID-Parameters übergeben.
Diese Standardroute bietet viel, doch Sie können auch Ihre eigenen Routen hinzufügen. Sie möchten zum Beispiel eine wirklich freundliche Website erstellen, auf der die Benutzer einfach ihren Namen eingeben, um persönlich begrüßt zu werden. Wenn Sie diese Route am Anfang der Routingtabelle einfügen,
  RouteTable.Routes.Add(new Route
  {
    Url = "[id]",
    Defaults = new { 
        controller = "Hello", 
        action = "HiThere" },
    RouteHandler = typeof(MvcRouteHandler)
  });
dann können Sie einfach zu „http://localhost/Chris“ navigieren, und die Aktion ist nach wie vor aufgerufen. Eine freundliche Begrüßung wird angezeigt.
Woher wusste das System, welcher Controller und welche Aktion aufgerufen werden sollten? Die Antwort liegt im Standardparameter. Dieser verwendet die neue anonyme Typsyntax C# 3.0, um ein Pseudowörterbuch zu erstellen. Das Standardobjekt auf der Route kann willkürliche zusätzliche Informationen enthalten doch für MVC kann es auch einige bekannte Einträge umfassen: Controller und Aktion. Wenn in der URL kein Controller oder keine Aktion angegeben werden, dann wird der Name in den Standardeinstellungen verwendet. Daher müssen sie nicht in der URL vorhanden sein, damit die Anforderung dem richtigen Controller und der richtigen Aktion zugeordnet wird.
Noch eine Anmerkung: Erinnern Sie sich daran, dass ich gesagt habe, „am Anfang der Tabelle einfügen“? Wenn dies am Ende der Tabelle eingefügt wird, erhalten Sie einen Fehler. Routing funktioniert nach dem Prinzip, wer zuerst kommt, mahlt zuerst. Bei der Verarbeitung von URLs arbeitet das Routingsystem die Tabelle von oben nach unten durch, und die erste passende Route gewinnt. In diesem Fall passt die Standardroute „[controller]/[action]/[id]“, weil es Standardwerte für die Aktion und die ID gibt. Folglich wird nach ChrisController gesucht, und da kein Controller vorhanden ist, wird ein Fehler angezeigt.

Ein umfangreicheres Beispiel
Nach dieser Ausführung zu den Grundlagen von MVC Framework soll im Folgenden ein umfangreicheres Beispiel erörtert werden, das nicht nur lediglich eine Zeichenfolge anzeigt. Ein Wiki ist eine Website, die im Browser bearbeitet werden kann. Seiten können problemlos hinzugefügt oder bearbeitet werden. Ich habe mithilfe des MVC Framework ein kleines Beispiel-Wiki geschrieben. Der Bildschirm „Diese Seite bearbeiten“ wird in Abbildung 7 gezeigt.
Abbildung 7 Bearbeiten der Homepage (Klicken Sie zum Vergrößern auf das Bild)
Sehen Sie sich den Codedownload für diesen Artikel an, um die Implementierung der zugrunde liegenden Wiki-Logik nachzuvollziehen. Hier soll erläutert werden, wie MVC Framework das Platzieren des Wiki im Internet unterstützt hat. Zuerst habe ich die URL-Struktur entworfen. Ich hatte folgendes Ziel:
  • /[pagename] zeigt die Seite mit diesem Namen an.
  • /[pagename]?Version=n zeigt die angeforderte Version der Seite an, wobei 0 = die aktuelle Version, 1 = die vorherige ist und so weiter.
  • /Edit/[pagename] öffnet den Bearbeitungsbildschirm für diese Seite.
  • /CreateNewVersion/[pagename] ist die URL, an die gesendet wird, um eine Bearbeitung einzureichen.
Sehen Sie sich zunächst die grundlegende Anzeige einer Wiki-Seite an. Dafür wurde eine neue Klasse namens „WikiPageController“ erstellt. Anschließend wurde eine Aktion namens „ShowPage“ hinzugefügt. Zunächst sah die WikiPageController-Klasse wie in Abbildung 8 aus. Die ShowPage-Methode ist ziemlich einfach. Die WikiSpace- und WikiPage-Klassen repräsentieren jeweils einen Satz Wiki-Seiten und eine spezielle Seite (sowie deren Überarbeitungen). Diese Aktion lädt das Modell hoch und ruft RenderView auf. Aber wozu dient die Zeile „new WikiPageViewData“?
public class WikiPageController : Controller 
{
  ISpaceRepository repository;

  public ISpaceRepository Repository 
  {
    get {
      if (repository == null) 
      {
        repository = new FileBasedSpaceRepository(
            Request.MapPath("~/WikiPages"));
      }
      return repository;
    }

    set { repository = value; }
  }

  [ControllerAction]
  public void ShowPage(string pageName, int? version) 
  {
    WikiSpace space = new WikiSpace(Repository);
    WikiPage page = space.GetPage(pageName);

    RenderView("showpage", 
      new WikiPageViewData 
      { 
        Name = pageName,
        Page = page,
        Version = version ?? 0 
      });
  }
}

Im vorherigen Beispiel wurde eine Möglichkeit gezeigt, Daten vom Controller zur Ansicht zu übergeben: das ViewData-Wörterbuch. Wörterbücher sind praktisch, aber auch gefährlich. Sie können beliebige Einträge enthalten, es wird kein IntelliSense® für die Inhalte bereitgestellt, und weil das ViewData-Wörterbuch vom Typ „Wörterbuch<zeichenfolge, Objekt>“ ist, muss alles konvertiert werden, um die Inhalte nutzen zu können.
Wenn Sie wissen, welche Daten in der Ansicht benötigt werden, können Sie stattdessen ein stark typisiertes ViewData-Objekt verwenden. In diesem Fall wurde ein einfaches Objekt erstellt, WikiPageViewData, wie in Abbildung 9 zu sehen ist. Dieses Objekt übergibt die Wiki-Seiteninformationen zusammen mit einigen Dienstprogrammmethoden an die Ansicht, um beispielsweise die HTML-Version des Wiki-Markups abzurufen.
public class WikiPageViewData {

    public string Name { get; set; }
    public WikiPage Page { get; set; }
    public int Version { get; set; }

    public WikiPageViewData() {
        Version = 0;
    }

    public string NewVersionUrl {
        get {
            return string.Format("/CreateNewVersion/{0}", Name);
        }
    }

    public string Body {
        get { return Page.Versions[Version].Body; }
    }

    public string HtmlBody {
        get { return Page.Versions[Version].BodyAsHtml(); }
    }

    public string Creator {
        get { return Page.Versions[Version].Creator; }
    }

    public string Tags {
        get { return string.Join(",", Page.Versions[Version].Tags); }
    }
}

Die Ansichtsdaten sind nun definiert. Wie lassen sie sich verwenden? In „ShowPage.aspx.cs“ sehen Sie Folgendes:
namespace MiniWiki.Views.WikiPage {
    public partial class ShowPage : ViewPage<WikiPageViewData>
    {
    }
}
Beachten Sie, dass die Basisklasse nach dem Typ „ViewPage<WikiPageViewData>“ definiert wurde. Die ViewData-Eigenschaft der Seite ist also vom Typ „WikiPageViewData“ und kein Wörterbuch wie im vorangegangenen Beispiel.
Das eigentliche Markup in der .aspx-Datei ist relativ einfach:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
  AutoEventWireup="true" CodeBehind="ShowPage.aspx.cs" 
  Inherits="MiniWiki.Views.WikiPage.ShowPage" %>
<asp:Content 
  ID="Content1"  
  ContentPlaceHolderID="MainContentPlaceHolder" 
  runat="server">
  <h1><%= ViewData.Name %></h1>
  <div id="content" class="wikiContent">
    <%= ViewData.HtmlBody %>
  </div>
</asp:Content>
Beachten Sie, dass der Indizierungsoperator [] nicht verwendet wird, um auf ViewData zu verweisen. Da nun ein stark typisiertes ViewData-Objekt vorliegt, kann stattdessen einfach direkt auf die Eigenschaft zugegriffen werden. Es sind keine Datentypkonvertierungen erforderlich, und durch Visual Studio wird IntelliSense bereitgestellt.
Aufmerksame Beobachter werden das Tag <asp:Content> in dieser Datei bemerkt haben. Masterseiten funktionieren tatsächlich mit MVC-Ansichten. Dazu können Masterseiten auch selbst Ansichten sein. Sehen Sie sich den Masterseiten-Codebehind an:
namespace MiniWiki.Views.Layouts
{
    public partial class Site :  
        System.Web.Mvc.ViewMasterPage<WikiPageViewData>
    {
    }
}
Das dazugehörige Markup ist in Abbildung 10 aufgeführt. Im Moment erhält die Masterseite genau das gleiche ViewData-Objekt wie die Ansicht. Die Basisklasse der Masterseite wurde als „ViewMasterPage<WikiPageViewData>“ deklariert, um die richtige Art ViewData zu erhalten. Von dort aus richten Sie die verschiedenen DIV-Tags ein, um die Seite anzulegen, füllen die Versionsliste aus und beenden mit dem üblichen Inhaltsplatzhalter.
<%@ Master Language="C#" 
  AutoEventWireup="true" 
  CodeBehind="Site.master.cs" 
  Inherits="MiniWiki.Views.Layouts.Site" %>
<%@ Import Namespace="MiniWiki.Controllers" %>
<%@ Import Namespace="MiniWiki.DomainModel" %>
<%@ Import Namespace="System.Web.Mvc" %>
<html >
<head runat="server">
  <title><%= ViewData.Name %></title>
  <link href="http://../../Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <div id="inner">
    <div id="top">
      <div id="header">
        <h1><%= ViewData.Name %></h1>
      </div>
      <div id="menu">
        <ul>
          <li><a href="http://Home">Home</a></li>
          <li>
            <%= Html.ActionLink("Edit this page", 
                  new { controller = "WikiPage", 
                        action = "EditPage", 
                        pageName = ViewData.Name })%>
        </ul>
      </div>
    </div>
    <div id="main">
      <div id="revisions">
        Revision history:
        <ul>
          <% 
            int i = 0;
            foreach (WikiPageVersion version in ViewData.Page.Versions)
            { %>
              <li>
                <a href="http://<%= ViewData.Name %>?version=<%= i %>">
                  <%= version.CreatedOn %>
                  by
                  <%= version.Creator %>
                </a>
              </li>
          <%  ++i;
          } %>
        </ul>
      </div>
      <div id="maincontent">
        <asp:ContentPlaceHolder 
          ID="MainContentPlaceHolder" 
          runat="server">
        </asp:ContentPlaceHolder>
      </div>
    </div>
  </div>
</body>
</html>

Auch zu beachten ist der Aufruf an Html.ActionLink. Dies ist ein Beispiel für ein Renderinghilfsprogramm. Die verschiedenen Ansichtsklassen haben zwei Eigenschaften, Html und Url. Jede verfügt über nützliche Methoden, Teile des HTML auszugeben. In diesem Fall nimmt Html.ActionLink ein Objekt (hier eines anonymen Typs) und führt es zurück durch das Routingsystem. Dies erzeugt eine URL, die zum angegebenen Controller und zur Aktion weiterleitet. Auf diese Weise führt der Link „Diese Seite bearbeiten“ immer zum richtigen Ort, egal wie die Routen geändert werden.
Sie haben wahrscheinlich auch bemerkt, dass ich auf das manuelle Erstellen eines Links zurückgreifen musste (die Links zu vorherigen Seitenversionen). Leider funktioniert das aktuelle Routingsystem nicht so gut zum Erzeugen von URLs, wenn Abfragezeichenfolgen verwendet werden. Dies dürfte in späteren Versionen des Frameworks behoben werden.

Erstellen von Formularen und Zurücksenden
Sehen Sie sich jetzt die EditPage-Aktion auf dem Controller an:
[ControllerAction]
public void EditPage(string pageName)
{
  WikiSpace space = new WikiSpace(Repository);
  WikiPage page = space.GetPage(pageName);

  RenderView("editpage", 
    new WikiPageViewData { 
      Name = pageName, 
      Page = page });
}
Wieder macht die Aktion nicht viel – sie rendert lediglich die Ansicht mit der speziellen Seite. In der Ansicht, die in Abbildung 11 zu sehen ist, wird es interessanter. Diese Datei erstellt ein HTML-Formular, aber es gibt kein Runat ="Server". Das Url.Action-Hilfsprogramm wird zum Erzeugen der URL verwendet, an die das Formular zurücksendet. Es gibt unterschiedliche Anwendungen verschiedener HTML-Hilfsprogramme, beispielsweise TextBox, TextArea und SubmitButton. Sie dienen erwartungsgemäß dazu, ein HTML für verschiedene Eingabefelder zu generieren.
<%@ Page Language="C#" 
  MasterPageFile="~/Views/Shared/Site.Master" 
  AutoEventWireup="true" 
  CodeBehind="EditPage.aspx.cs" 
  Inherits="MiniWiki.Views.WikiPage.EditPage" %>
<%@ Import Namespace="System.Web.Mvc" %>
<%@ Import Namespace="MiniWiki.Controllers" %>
<asp:Content ID="Content1" 
  ContentPlaceHolderID="MainContentPlaceHolder" 
  runat="server">
  <form action="<%= Url.Action(
    new { controller = "WikiPage", 
    action = "NewVersion", 
    pageName = ViewData.Name })%>" method=post>
    <%
      if (ViewContext.TempData.ContainsKey("errors"))
      {
    %>
    <div id="errorlist">
      <ul>
      <%
        foreach (string error in 
          (string[])ViewContext.TempData["errors"])
        {
      %>
        <li><%= error%></li>
      <% } %>
      </ul>
    </div>
    <% } %>
    Your name: <%= Html.TextBox("Creator",
                   ViewContext.TempData.ContainsKey("creator") ? 
                   (string)ViewContext.TempData["creator"] : 
                   ViewData.Creator)%>
    <br />
    Please enter your updates here:<br />
    <%= Html.TextArea("Body", ViewContext.TempData.ContainsKey("body") ? 
        (string)ViewContext.TempData["body"] : 
        ViewData.Body, 30, 65)%>
    <br />
    Tags: <%= Html.TextBox(
              "Tags", ViewContext.TempData.ContainsKey("tags") ? 
              (string)ViewContext.TempData["tags"] : 
              ViewData.Tags)%>
    <br />
    <%= Html.SubmitButton("SubmitAction", "OK")%>
    <%= Html.SubmitButton("SubmitAction", "Cancel")%>
  </form>
</asp:Content>

Lästiger bei der Webprogrammierung sind Fehler in einem Formular. Sie möchten Fehlermeldungen anzeigen, jedoch die zuvor eingegebenen Daten beibehalten. Es ist uns allen schon passiert, dass wir in einem Formular mit 35 Feldern einen Fehler gemacht haben, woraufhin eine Reihe von Fehlermeldungen und ein neues, leeres Formular angezeigt werden. MVC Framework bietet TempData als einen Ort zum Speichern der zuvor eingegebenen Informationen an, um das Formular erneut auszufüllen. Dadurch wurde ViewState in Webformularen sehr einfach, da das Speichern der Inhalte von Steuerelementen mehr oder weniger automatisch erfolgte.
Das Gleiche soll in MVC erreicht werden, und dabei ist TempData von Nutzen. TempData ist ein Wörterbuch, ähnlich wie das nicht typisierte ViewData. Die Inhalte von TempData werden jedoch nur für eine einzige Anforderung gespeichert und anschließend gelöscht. In Abbildung 12, in der NewVersion-Aktion sehen Sie, wie dies funktioniert.
[ControllerAction]
public void NewVersion(string pageName) {
  NewVersionPostData postData = new NewVersionPostData();
  postData.UpdateFrom(Request.Form);

  if (postData.SubmitAction == "OK") {
    if (postData.Errors.Length == 0) {
      WikiSpace space = new WikiSpace(Repository);
      WikiPage page = space.GetPage(pageName);
      WikiPageVersion newVersion = new WikiPageVersion(
        postData.Body, postData.Creator, postData.TagList);
      page.Add(newVersion);
    } else {
      TempData["creator"] = postData.Creator;
      TempData["body"] = postData.Body;
      TempData["tags"] = postData.Tags;
      TempData["errors"] = postData.Errors;

      RedirectToAction(new { 
        controller = "WikiPage", 
        action = "EditPage", 
        pageName = pageName });
      return;
    }
  }

  RedirectToAction(new { 
    controller = "WikiPage",
    action = "ShowPage", 
    pageName = pageName });
}

Zunächst wird ein NewVersionPostData-Objekt erstellt. Dies ist ein weiteres Hilfsobjekt mit Eigenschaften und Methoden, die die Inhalte des Beitrags speichern, sowie einer Überprüfung. Um das postData-Objekt zu laden, wird eine Hilfsfunktion aus dem MVC Toolkit verwendet. UpdateFrom ist eigentlich eine Erweiterungsmethode, die vom Toolkit bereitgestellt wird, und es wird Reflektion verwendet, um die Namen der Formularfelder an die Namen der Eigenschaften auf dem Objekt anzupassen. Das Ergebnis ist, dass alle Feldwerte in das postData-Objekt geladen werden. Die Verwendung von UpdateFrom hat jedoch den Nachteil, dass Formulardaten direkt von HttpRequest abgerufen werden, wodurch Komponententests erschwert werden.
NewVersion überprüft zunächst SubmitAction. Dies wird als „OK“ angezeigt, wenn der Benutzer auf die Schaltfläche „OK“ geklickt hat und die bearbeitete Seite senden will. Gibt es hier einen anderen Wert, leitet die Aktion zu ShowPage zurück, wodurch die ursprüngliche Seite erneut angezeigt wird.
Wenn der Benutzer auf „OK“ geklickt hat, prüfen Sie die postData.Errors-Eigenschaft. Diese führt einige einfache Prüfungen der Beitragsinhalte aus. Sind keine Fehler vorhanden, wird die neue Version der Seite verarbeitet und in das Wiki geschrieben. Sind jedoch Fehler vorhanden, wird es interessant.
In diesem Fall legen Sie die verschiedenen Felder des TempData-Wörterbuchs so fest, das sie die Inhalte von PostData enthalten. Dann leiten Sie zur Bearbeitungsseite zurück. Da TempData jetzt festgelegt ist, zeigt die Seite erneut das Formular an, diesmal initialisiert mit den vom Benutzer zuvor gesendeten Werten.
Die Verarbeitung von Beiträgen, die Prüfung und TempData sind relativ arbeitsintensiv und erfordern mehr manuelle Arbeit als wirklich nötig ist. Zukünftige Versionen sollten Hilfsmethoden enthalten, die zumindest einen Teil der Überprüfung von TempData automatisieren. Ein letzter Hinweis zu TempData: Die Inhalte von TempData werden in der serverseitigen Benutzersitzung gespeichert. Wenn Sie die Sitzung ausschalten, funktioniert TempData nicht.

Controllererstellung
Die Grundlagen des Wiki funktionieren jetzt, doch es gibt einige Unklarheiten in der Implementierung, die noch verdeutlicht werden sollen. Zum Beispiel wird die Repository-Eigenschaft verwendet, um die Logik des Wiki vom physischen Speicher zu entkoppeln. Sie können Repositorys bereitstellen, die Inhalte im Dateisystem speichern (so wie in diesem Beispiel), in einer Datenbank oder wo immer Sie möchten. Dabei sind leider zwei Probleme zu lösen:
Zunächst ist die Controllerklasse eng mit der konkreten FileBasedSpaceRepository-Klasse gekoppelt. Es ist ein Standard erforderlich, der verwendet werden kann, wenn die Eigenschaft nicht festgelegt ist. Was noch schlimmer ist, der Pfad zu den Dateien auf dem Datenträger ist hier ebenfalls hartcodiert. Dies sollte zumindest aus der Konfiguration hervorgehen.
Zweitens ist das Repository tatsächlich eine erforderliche Abhängigkeit, ohne die das Objekt nicht ausgeführt wird. Ein guter Entwurf verdeutlicht, dass das Repository ein Konstruktorparameter sein sollte und keine Eigenschaft. Doch es kann dem Konstruktor nicht hinzugefügt werden, weil das MVC Framework einen argumentlosen Konstruktor auf Controllern erfordert.
Glücklicherweise gibt es eine Erweiterungsmöglichkeit, die Ihnen aus der Klemme hilft: die Controller Factory. Eine Controller Factory dient zum Erstellen von Controllerinstanzen. Sie müssen lediglich eine Klasse erstellen, die die IControllerFactory-Schnittstelle implementiert und beim MVC System registrieren. Sie können Controller Factorys für alle Controller oder nur für spezielle Typen registrieren. Abbildung 13 zeigt eine Controller Factory für WikiPageController, die das Repository jetzt als Konstruktorparameter übergibt.
public class WikiPageControllerFactory : IControllerFactory {

  public IController CreateController(RequestContext context, 
    Type controllerType)
  {
    return new WikiPageController(
      GetConfiguredRepository(context.HttpContext.Request));
  }

  private ISpaceRepository GetConfiguredRepository(IHttpRequest request)
  {
    return new FileBasedSpaceRepository(request.MapPath("~/WikiPages"));
  }
}

In diesem Fall ist die Implementierung ziemlich einfach, dadurch können jedoch die Erstellungscontroller aktiviert werden, die erheblich leistungsfähigere Tools verwenden (in bestimmten Dependency Injection-Containern). Jetzt haben Sie alle Informationen, um die Abhängigkeiten für den Controller in ein Objekt zu trennen, das leichter verwaltet und gewartet werden kann.
Als letzter Schritt muss die Factory beim Framework registriert werden. Sie erledigen dies über die Klasse ControllerBuilder, indem Sie in der Application_Start-Methode der Global.asax.cs-Klasse folgende Zeile hinzufügen (entweder vor oder nach den Routen):
ControllerBuilder.Current.SetControllerFactory(
  typeof(WikiPageController), typeof(WiliPageControllerFactory));
Dadurch wird eine Factory für WikiPageController registriert. Wenn es in diesem Projekt andere Controller gibt, wird diese Factory nicht verwendet, da sie nur für den WikiPageController-Typ registriert ist. Sie können auch SetDefaultControllerFactory aufrufen, wenn Sie eine Factory festlegen wollen, die für jeden Controller verwendet werden soll.

Andere Erweiterungspunkte
Die Controller Factory ist nur der Anfang der Frameworkerweiterbarkeit. In diesem Artikel kann nicht ins Detail gegangen werden, daher wird nur auf die Höhepunkte verwiesen. Wenn Ihre Ausgabe nicht HTML sein soll oder wenn Sie ein anderes Vorlagenmodul als Webformulare verwenden möchten, können Sie das ViewFactory-Objekt des Controllers anders einrichten. Sie können die IViewFactory-Schnittstelle implementieren und erhalten dann vollständige Kontrolle über die Generierung der Ausgabe. Dies ist zum Generieren von RSS, XML oder sogar Grafik nützlich.
Wie Sie bereits gesehen haben, ist das Routingsystem ziemlich flexibel. Doch im Routingsystem ist nichts für MVC spezifisch. Jede Route verfügt über eine RouteHandler-Eigenschaft. Bisher habe ich diese immer auf MvcRouteHandler festgelegt. Aber es ist möglich, die IRouteHandler-Schnittstelle zu implementieren und das Routingsystem mit anderen Webtechnologien zu koppeln. Eine zukünftige Version des Frameworks soll einen WebFormsRouteHandler enthalten. Andere Technologien werden in Zukunft die Vorteile des generischen Routingsystems nutzen.
Controller müssen nicht von System.Web.Mvc.Controller ableiten. Ein Controller muss lediglich die Schnittstelle IController implementieren, die nur über eine einzige Methode namens „Execute“ verfügt. Von da aus können Sie beliebig vorgehen. Wenn Sie andererseits nur einige Verhaltensweisen der grundlegenden Controller-Klasse anpassen möchten, bietet der Controller viele virtuelle Funktionen, die überschrieben werden können:
  • Mit OnPreAction, OnPostAction und OnError können Sie generische Vor- und Nachbearbeitung mit jeder Aktion koppeln, die ausgeführt wird. OnError bietet einen Controller-übergreifenden Fehlerbehandlungsmechanismus.
  • HandleUnknownAction wird aufgerufen, wenn eine URL an den Controller weitergeleitet wird, dieser Controller jedoch die in der Route angeforderte Aktion nicht ausführt. Standardmäßig führt diese Methode zu einer Ausnahme, aber Sie können sie außer Kraft setzen und somit nach Belieben anpassen.
  • InvokeAction ist die Methode, die herausfindet, welche Aktionsmethode aufgerufen werden soll, und sie aufruft. Wenn Sie den Prozess anpassen möchten (zum Beispiel um die Anforderung für die Attribute [ControllerAction] zu beseitigen), können Sie dies hier tun.
Es gibt weitere virtuelle Methoden auf dem Controller, doch dies sind vorrangig Testmöglichkeiten und keine Erweiterungspunkte. Zum Beispiel ist RedirectToAction virtuell, damit Sie eine abgeleitete Klasse erstellen können, die nicht wirklich umleitet. So können Sie Aktionen testen, die umleiten, ohne dass ein kompletter Webserver ausgeführt werden muss.

Abschied von Webformularen?
Jetzt fragen Sie sich vielleicht: „Was passiert mit den Webformularen? Werden sie durch MVC ersetzt?“ Die Antwort lautet „Nein“! Webformulare sind eine geläufige Technologie, und Microsoft wird sie weiter unterstützen und verbessern. Es gibt viele Anwendungen, bei denen Webformulare sehr gut funktionieren. Die Berichterstattungsanwendung für Intranetdatenbanken kann zum Beispiel mithilfe von Webformularen in einem Bruchteil der Zeit erstellt werden, die für das Schreiben in MVC erforderlich wäre. Zudem unterstützen Webformulare zahlreiche Steuerelemente, von denen viele hoch entwickelt sind und eine Menge Arbeit ersparen.
Wann sollten Sie also MVC den Webformularen vorziehen? Häufig ist das abhängig von Ihren Anforderungen und Vorlieben. Haben Sie Schwierigkeiten, Ihre URLs nach Ihren Vorlieben anzupassen? Möchten Sie Komponententests auf Ihrer Benutzeroberfläche durchführen? Beide dieser Szenarios legen die Verwendung von MVC nahe. Arbeiten Sie vielleicht viel mit der Darstellung von Daten, mit bearbeitungsfähigen Rastern und aufwändigen Strukturansichtsteuerelementen? Dann sind wahrscheinlich im Moment Webformulare die bessere Wahl.
Mit der Zeit wird MVC Framework wahrscheinlich im Bereich der Benutzeroberflächensteuerung aufholen, doch vermutlich wird der Einstieg nie so einfach sein wie mit Webformularen, bei denen zahlreiche Funktionen per Drag  & Drop zugänglich sind. Doch bis dahin bietet ASP.NET MVC Framework Webentwicklern eine neue Möglichkeit, Webanwendungen in Microsoft .NET Framework zu erstellen. Das Framework wurde für Testfähigkeit entworfen, nutzt HTTP anstatt es zu abstrahieren und kann an fast jedem Punkt erweitert werden. Es ist eine notwendige Ergänzung zu Webformularen für die Entwickler, die vollständige Kontrolle über ihre Webanwendungen erhalten möchten.

Chris Tavares ist Entwickler im Patterns & Practices-Teams von Microsoft. Seine Aufgabe ist die Unterstützung der Entwicklungscommunity bei Best Practices zum Erstellen von Systemen auf Microsoft-Plattformen. Außerdem ist er virtuelles Mitglied des ASP.NET MVC-Teams, wo er sich an der Entwicklung neuer Frameworks beteiligt. Sie können Chris Tavares unter cct@tavaresstudios.com erreichen.

Page view tracker