22 January 2009 ASP.NET MVC; MVC, HowTo Robert Muehsig

In fast jedem ASP.NET Projekt wird es eine Masterpage geben und diese wird was "dynamisches" Anzeigen - sei es eine Tagcloud oder allein der Seitentitel. Allerdings ist sowas im MVC Sinne nicht ganz trivial. Ich arbeite gerade in einem kleinen Projekt mit dem ASP.NET MVC Framework über das ich schon ab und an mal gebloggt habe.

Zwei Beispiele von solch einem dynamischen Content:

image 

image

ASP.NET mit seinem Control System lässt sowas sehr einfach zu. Jedes Control ist für seine Datenbeschaffung selbst zuständig.
Das MVC Pattern besagt jedoch, dass alle Daten von einem Controller kommen und der View diese nur anzeigt. Daten über die Codebehind auf eine Masterpage schreiben geht auch in ASP.NET MVC - allerdings verfehlt man damit etwas das MVC Konzept.
Stephen Walther hat eine recht "komplexe" Lösung aufgezeigt - die sicherlich dem ähnelt was ich hier beschreibe.

Was wollen wir erreichen?

Wir wollen die Standard ASP.NET MVC Vorlage etwas "dynamischer" machen, die rot umrahmten Teile wollen wir durch dynamischen Content ersetzen:

image image

Die Start-Projektstruktur:

image

Schritt 1: ViewData "Objekthierarchie" anlegen

Wenn man Masterpage nutzt, hat die Masterpage bestimmte Daten und die eigentliche Seite auch ihre Daten zum anzeigen - es kommt zu einer Hierarchie, die wir erstmal relativ simpel abbilden:

image

ViewDataBase ist unser "Root" Objekt...

    public class ViewDataBase
    {
        public SiteMasterViewData SiteMasterViewData { get; set; }
    }

... dieses Enthält auch ein spezielles Objekt für die Site.Master um z.B. den Titel dynamisch zu ändern:

    public class SiteMasterViewData
    {
        public string Title { get; set; }
    }

Schritt 2: Strongly-Typed Viewdata in der Masterpage registrieren

Damit die Site.Master das nun auch mitbekommt, nehmen wir die ViewDataBase als Viewdata Typ:

    public partial class Site : System.Web.Mvc.ViewMasterPage<ViewDataBase>
    {
    }

... und zeigen im Frontend den Titel an:

            <div id="title">
                <h1><%= Html.Encode(ViewData.Model.SiteMasterViewData.Title) %></h1>
            </div>

Schritt 3: ActionFilter fügen Masterpage Daten hinzu

ActionFilter sind ein netter ASP.NET MVC Mechanismus: Über so genannte Filter kann man das Ergebnis eines Requests vor dem Eintreffen auf die eigentliche Action Methode beeinflussen oder hinterher (z.B. siehe hier). Dazu fügen wir einen Ordner "Filters" hinzu:

image

Hier fügen wir nun dem "TempData" ein Eintrag namens "ViewData" mit einem dynamischen Seitentitel ("Master Dynamisch + Datum") vor dem eigentlichen Methodenaufruf hinzu:

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            ViewDataBase data = new ViewDataBase();
            data.SiteMasterViewData = new SiteMasterViewData();
            data.SiteMasterViewData.Title = "Master Dynamisch @ " + DateTime.Now.ToShortDateString();

            // remove existing viewdata
            filterContext.Controller.TempData.Remove("ViewData");

            filterContext.Controller.TempData.Add("ViewData", data);
        }

Schritt 4: "Home" Views vorbereiten

Bevor wir weitermachen, hinterlegen wir für die beiden Home-Views jeweils eine ViewData Typ Klasse, welche von ViewDataBase ableitet:

image

image

Hier am Beispiel der AboutViewData:

    public class AboutViewData : ViewDataBase
    {
        public AboutViewData(SiteMasterViewData siteMaster)
        {
            base.SiteMasterViewData = siteMaster;
        }

        public string Text { get; set; }
    }

In der About.aspx muss es wie folgt aussehen:

    <h2><%= Html.Encode(ViewData.Model.Text) %></h2>
    <p>
        TODO: Put <em>about</em> content here.
    </p>

Schritt 5: HomeController mit Filter dekorieren

Über den HomeController setzen wir nun noch unseren Filter, der unseren tollen Titel mitgibt:

    [HandleError]
    [AddSiteMasterViewData]
    public class HomeController : Controller
    {

Schritt 6: Action Methods anpassen

Nun müssen noch die ActionMethods angepasst werden. Dazu holen wir uns die ViewDataBase Daten aus dem TempData, welches in dem "AddSiteMasterViewData" Filter befüllt wird und befüllen das IndexViewData bzw. das AboutViewData Objekt .

        public ActionResult Index()
        {
            ViewDataBase masterData = (ViewDataBase)this.ControllerContext.Controller.TempData["ViewData"];

            IndexViewData viewData = new IndexViewData(masterData.SiteMasterViewData);
            viewData.Text = "Welcome to ASP.NET MVC!";

            return View(viewData);
        }

        public ActionResult About()
        {
            ViewDataBase masterData = (ViewDataBase)this.ControllerContext.Controller.TempData["ViewData"];

            AboutViewData viewData = new AboutViewData(masterData.SiteMasterViewData);
            viewData.Text = "About Page";

            return View(viewData);
        }

Ergebnis:

image

Der Content ist nun dynamisch und beide Werte können angepasst werden. Man baut sich dadurch eine Hierarchie auf - in unserem Beispiel:

image

Allerdings könnte man auch zwei oder drei Masterpages verschachteln - ActionFilter werden nacheinander abgearbeitet, d.h. man könnte also auch sowas sich zusammenbauen:

image

Notiz am Rande: Eigentlich wollte ich auch zwei Masterpages verschachteln - dann hätte man so eine Hierarchie wie in diesem Bild gesehen. Allerdings ist der Text ohnehin schon lang genug, aber für das Funktionsprinzip sollte es ausreichen :)

Negatives und Positives

Ich muss allerdings zugeben, dass ich nicht restlos mit der Lösung zufrieden bin. Das schreiben in dieses "TempData" (das wohl wahrscheinlich wegen solchen Fällen auch mit erschaffen wurde) gefällt mir nicht. Das Gute daran ist allerdings, dass man in einem nachfolgenden Filter wieder einlesen kann und entsprechend darauf reagieren.
Der "SiteMasterFilter" könnte z.B. prüfen ob der Nutzer gerade angemeldet ist oder nicht - dies kann dann der nachfolgende Filter oder die Action Method auch wieder nutzen.

So ganz glücklich bin ich mit den Bezeichnungen auch nicht. Allerdings wenn man sich vorher Gedanken macht, was man entsprechend braucht, kann man damit robuste Lösungen auf Basis von MVC entwickeln. Die Filter kann man auch über Unit-Tests testen.

Um es Einzusetzen ist allerdings solch eine Hierachie Pflicht. Wenn ich den mitgelieferten Account Controller nur mit dem SiteMaster Filter ausstatte, passiert noch garnix - in meiner jetzigen Version crasht daher auch der Account Controller, weil er eben nicht die SiteMasterViewData mitliefert.

Was haltet ihr von der Lösung? Gut, Schlecht, Mittel oder gar Epic Fail? Wenn jemand (im MVC Sinne) eine andere gute Lösung hat, dann immer her damit :)

[ Download Demosource ]


Written by Robert Muehsig

Software Developer - from Dresden, Germany, now living & working in Switzerland. Microsoft MVP & Web Geek.
Other Projects: KnowYourStack.com | ExpensiveMeeting | EinKofferVollerReisen.de

If you like the content and want to support me you could buy me a beer or a coffee via Litecoin or Bitcoin - thanks for reading!