HowTo: Dynamischen Content auf ASP.NET MVC Masterpages bringen

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 ]

13 Kommentare bisher »

  1. Michael Topf sagt

    am 22. Januar 2009 @ 13:39

    Hallo Robert,

    ich schlage mich gerade mit dem gleichen Thema herum und bin auch noch zu keinem richtig befriedigenden Ergebnis gekommen. Insbesondere sehe ich noch das Problem das das Anzeigen dynamischer Daten ja nur der erste Schritt ist. Es stellt ja aber nichts ungewöhnliches dar auch entsprechende Eingaben von Elementen verarbeiten zu wollen. Ich denke mal an den Login oder die Suche. Dann müsste das ganze ja auch an einen "zentralen Controller" weitergeleitet werden, der das ganze abarbeitet.

    Michael

  2. Sebastian Jancke sagt

    am 22. Januar 2009 @ 18:24

    Hallo,

    ich möchte anmerken, dass der Titel einer View nicht die Verantwortlichkeit eines Controllers ist. Siehe dazu auch Oren Eini:
    http://ayende.com/Blog/archive/2008/12/29/do-not-put-presentation-concerns-in-the-controllers.aspx

    Zu grundsätzlichen Modifikationen/ Prinzipien-treuerer Nuztung des MVC-Frameworks gibt es auch einen guten Blogpost von Oren Eini:
    http://ayende.com/Blog/archive/2008/12/29/my-baseline-asp.net-mvc-modifications.aspx

    -Sebastian Jancke

  3. Robert Mühsig sagt

    am 22. Januar 2009 @ 19:50

    Hallo Sebastian,
    ich bin der Meinung (wie Phil Haack und andere Leutchen die Orens Post kommentiert haben), dass der Titel oftmals zum "Model" der Web-Applikation gehört.
    Wie würdest du denn diesen Fall abbilden?

  4. Robert Mühsig sagt

    am 23. Januar 2009 @ 01:18

    Nachdem ich etwas Zeit gefunden hab und alle Kommentare mir auf Orens Postdurchgelesen hab, stimm ich jedenfalls beim Title mit überein – ja, es geht auch über ContentPlaceHolder und man kann die Daten entsprechend dann an diesen übergeben.
    Warum man die anderen Änderungen machen sollte, ist mir noch nicht ganz klar.

  5. Dirk Rodermund sagt

    am 23. Januar 2009 @ 08:33

    Hallo Robert,
    ich kann mich nur Sebastian (und Ayende) anschließen.
    Des weiteren bin ich schon mit einem streng typisierten ViewModel in der MasterPage an unschöne Grenze gestossen:
    Wenn Du z.B. deinen Controller mit [HandleError] attributierst und und Deine ErrorPage aus dem Master basiert, dann hast Du das Problem, das das Model dann vom Typ HandleErrorInfo ist und dein Master damit dann nicht ohne weiteres umgehen kann.
    Daher verwende ich auch den Ayendes Ansatz mit dem ContentPlaceHolder.
    Mal sehen, wie sich die von Scott angekündigten Verbesserungen auf unsere Arbeit mit dem Framework auswirken.

    Gruß Dirk!

  6. Sebastian Jancke sagt

    am 25. Januar 2009 @ 11:54

    Hallo Robert,
    nachdem ich nun wieder mal vorm Rechner sitze, kann ich deine Fragen beantworten ;-)

    Zunächst mal denke ich, habe ich mit meinem Blogpost eine mögliche Lösung über ASP.NET Content-Placeholder beschrieben. Der Grund ist einfach: Ein Titel (und auch eine Welcome-Message) liegt im MVC-Pattern nicht im Verantwortungsbereich eines Controllers. Dafür gibt es die View.

    Die anderen Änderungen haben für mich zwei Motivationen: Zunächst mal möchte ich weniger Rauschen. Deshalb ist ViewData.Model nicht sehr passend. Viel wichtiger ist aber noch: Ich möchte überall streng-typisierte Modelle haben. Und dem steht der ViewData-Dictionary sehr im Weg. Deshalb ist es eine gute Idee diesen zu killen. In FubuMVC von Jeremy D Miller (und anderen) ist es so auch nur möglich, streng typisierte Modelle aus einem Controller zu liefern. Und wenn man explizit einen Dictionary hat, dann könnte man diesen auch liefern, ist sich dann aber darüber bewusst.

  7. Marco sagt

    am 27. Januar 2009 @ 15:05

    Hallo,

    mich würde mal interessieren wie ihr folgendes Problem einfach aber gut lösenwürdet, ein ContentPlaceHolder kommt jedenfalls nicht in Frage.

    Ich habe folgendes Markup:

    <body>
      <div class="xyz">
        <!– Master content –>
        <asp:ContentPlaceHolder ID="MainContent" runat="server" />
        <!– Master content –>
      </div>
    </body>

    Ich möchte nun die Klasse (xyz) des Div-Containers welches sich in der Masterpage befindet von der View aus beeinflussen. Die Klasse enthält spezielle Formatierungen sowie Hintergrundbilder, die von der View abhängen und nicht vom Controller oder Model, damit auch nichts zu tun haben.

    Danke und lg,
    Marco

  8. Robert Mühsig sagt

    am 27. Januar 2009 @ 15:08

    Ich würde es so machen wie in meinem Beispiel beschrieben. Der Controller würde dann die passende Css Klasse mitliefern und an die MasterPage "weiterreichen".
    "Einfach" ist das nun nicht gerade, aber es wäre eine Möglichkeit.

  9. Marco sagt

    am 27. Januar 2009 @ 15:17

    Hallo Robert,

    der Haken daran ist nur, dass es eine reine Darstellungssache ist, von der der Controller eigentlich nichts wissen soll. Verglichen mit einem Navigationssystem etwa die Darstellung im Tag- oder Nachtmodus. Sprich, der Controller soll der View zwar mitteilen, ob das Model im Tag- oder Nachtmodus dargestellt werden soll, aber nicht wie diese Darstellung definiert ist (in Form von CSS-Klassen o. ä.). Ein weiterer Grund ist, dass man Views zum Großen Teil auch direkt am Server anpassen kann ohne die MVC Applikation neu zu kompilieren, Änderungen am Controller müssen immer kompiliert werden. (Beachte: Ich rede von einer Web Application, nicht von einer Website).

  10. Robert Mühsig sagt

    am 27. Januar 2009 @ 15:25

    Ich denke man sollte es zum Teil auch etwas pragmatischer sehen.
    Der Controller braucht ja nicht als ViewData die Eigenschaft "CssClass" weiterreichen, sondern man könnte das ja entsprechend anderes benennen.
    Ich glaub im View kann man auch mitbekommen von welchen Controller er Daten bekommt, jetzt könntest du quasi die Css Klasse anhand des Controllernamens einfach nehmen.

    Alles war mit News zutun hat bekommt z.B. einen blauen Hintergrund
    NewsController -> Rendert "News" View und dort bekommt man sicherlich raus, welcher Controller die Daten geschickt hat:
    <div class="<%=this.Controller.ToString()%>Class"> …</div>

    Daraus wird dann z.B. "NewsClass". 100% Flexibilität wirst du sicherlich auch nicht so einfach erreichen – aber du wirst sicherlich den View auch nicht plötzlich einem anderen Controller zuordnen, sondern könntest jetzt die CSS Klasse einfach ändern.

  11. Marco sagt

    am 27. Januar 2009 @ 16:10

    Das ist schon ein Ansatz. Was mir aber irgendwie fehlt, ist ein Interaktionsmöglichkeit zwischen View und Master.

    Ich habe jetzt im Master direkt nach dem öffnenden Html-Tag  und vor dem head einen Content-Placeholder Initialization eingefügt, in dem ich dann im View Code ausführen kann, z. B. Werte in ViewData, TempData oder irgendwo anders hinschreiben, wo die Masterpage drauf zugreifen kann.

  12. Robert Mühsig sagt

    am 27. Januar 2009 @ 16:12

    Der Master-View sollte allerdings nicht in die ViewData reinschreiben und auch sonst möglichst wenig "Logik" haben.
    Es gibt nur wenig Interaktionsmöglichkeiten zwischen View und Master. Daher die Idee mit der Vererbungshierarchie und den Filtern, wie ich oben beschrieben hab.

  13. Marco sagt

    am 27. Januar 2009 @ 17:04

    Ich weiß.

    Allerdings handelt es sich bei mir nicht um ein großes Projekt, sondern um viele unabhängige kleine Projekte, die teilweise nur einen Controller und ~ 4 Views haben. Da ich aber für andere Aufgaben schon ein recht umfangreiches Framework aufgebaut habe (Anbindung an unsere Services, Datenbank etc.) möchte ich die Vererbungshierarchie nicht noch mehr aufblähen und komplexer machen.
    Und um gewisse Logik in den Views kommt man leider nicht drumrum, manche Projekte etwa sind mehrsparchig und alle Ressourcen werden bei uns über eine Datenbank verwaltet. Da kommt dann schon wieder Logik rein, die ich bislang aus unterschiedlichen Gründen leider noch nicht auf Dependency Injection umbauen konnte…

Komentar RSS · TrackBack URI

Hinterlasse einen Kommentar

Name: (erforderlich)

eMail: (erforderlich)

Website:

Kommentar: