Beim Arbeiten mit ASPX-Seiten stösst man immer wieder auf ähnlich gelagerte Probleme. Und wie es so oft im Leben eines Programmierers ist, gibt es meist viele Wege ein Problem zu lösen.
So zum Beispiel das Problem, dass sich HTML-Elemente auf den meisten Seiten wiederholen. Mittlerweile gibt es eine ganze Reihe von Lösungsansätzen in Form von Server Side Include, User Controls und Templates, die alle aber immer wieder den einen oder anderen Haken haben.
Was mir persönlich immer gefehlt hat, war die Möglichkeit sich wirklich nur noch um den eigentlichen Inhaltsbereich einer Seite zu kümmern. Als Programmierer bin ich per Definition faul. Ich möchte nicht jedes Mal Quellcode von einer Seite auf eine neue kopieren müssen, um z.B. Controls einzubinden, damit auch ja die Navigation in der Seite enthalten ist.
Ich möchte eine Basisklasse, die das gesamte Rahmenwerk bereits enthält.
Im Folgenden möchte ich eine Lösung für ein Problem aufzeigen, die anscheinend von der gängigen Fachliteratur in dieser Form in der Praxis nicht erwartet wurde.
<html> <head> <title>CMetz Testcenter</title> </head> <body> <table border="0" cellspacing="0" cellpadding="0"> <tr> <td><img src="logo.gif"></td> <td><img src="banner.gif"></td> </tr> <tr> <td> <h3>Navigation</h3> <a href="seite1.aspx">Seite 1</a><br> <a href="seite2.aspx">Seite 2</a><br> <a href="seite3.aspx">Seite 3</a><br> </td> <td> <!-- Content Bereich --> </td> </tr> <tr> <td colspan="2">Footer</td> </tr> </table> </body> </html> |
Wir stellen fest, dass es mehrere Bereiche auf der Internetseite gibt, die auf allen Seiten immer wieder gleich sind. Der versierte ASP.NET-Entwickler wird nun sofort auf die Idee kommen, diese Bereiche mit Hilfe von User Controls auszulagern.
In diesem Fall würden sich beispielsweise der Headerbereich mit Logo und Banner, die komplette Navigation und der Footerbereich anbieten.
Von unserer statischen HTML-Seite würde also durch den Einsatz von User Controls folgende ASPX-Seite übrig bleiben.
<%@ Page Language="vb" Codebehind="Default.aspx.vb" Inherits="StartPage" %> <%@ Register tagPrefix="CM" tagName="HeaderControl" src="controls/PageHeader.ascx" %> <%@ Register tagPrefix="CM" tagName="NavigationControl" src="controls/PageNavigation.ascx" %> <%@ Register tagPrefix="CM" tagName="FooterControl" src="controls/PageFooter.ascx" %> <html> <head> <title>CMetz Testcenter</title> </head> <body> <table border="0" cellspacing="0" cellpadding="0"> <tr> <CM:HeaderControl id="ctlHeader" runat="server" /> </tr> <td> <CM:NavigationControl id="ctlNavigation" runat="server" /> </td> <td> <!-- Content Bereich --> </td> </tr> <CM:FooterControl id="ctlFooter" runat="server" /> </table> </body> </html>
|
Der grosse Vorteil, den man dadurch erhalten soll, dass der Code übersichtlicher und Änderungen am Design sich schneller auf die gesamte Website anwenden lassen sollen.
Diese Aussagen sind - wenn wir diesen Code mit dem Original vergleichen - jedoch nicht wirklich zutreffend.
Das Grunddesign der Tabellenstruktur ist weiterhin in der HTML-Seite vorhanden und Änderungen an dieser Struktur müssten auch weiterhin auf allen Seiten, die mit diesem Schema erstellt wurden nachgezogen werden.
Dem eigentlichen Ziel, sich bei einer Seite einzig und allein auf den Contentbereich zu konzentrieren, ist man leider nur einen kleinen Schritt näher gekommen.
Eine Möglichkeit aus diesem Dilemma heraus wäre, den im Header- bzw. FooterControl sogar noch den HTML-Code für die Tabellendefinition mit aufzunehmen. Dies würde jedoch bedeuten, dass die Struktur der Seite auf mehrere Controls aufgesplittet werden würden. Eine spätere Pflege wird dadurch wesentlich erschwert, da man nun nicht mehr den Überblick hätte.
Die Basisseite
Ein Lösungsansatz, der wesentlich eleganter wäre, ist eine Art Basisseite zu definieren, die das Layout der Seitenstruktur, sowie die Standardcontrols enthält und alle Seiten von dieser ableiten zu lassen.
Public Class PageBase Inherits System.Web.UI.Page ?
End Class
|
Alle Seiten werden nun von dieser Basisklasse mit Inherits PageBase abgeleitet.
Diese Basisklasse hat keine eigene WebForm und hat damit erstmal jedoch kein definiertes Layout.
Die geschützten Methoden Render und CreateChildControls der Klasse System.Web.UI.Page kann jedoch so erweitert werden, dass man eigene HTML-Anweisungen oder User Controls einbinden kann.
In der einfachsten Form sieht dies wie folgt aus:
Protected Overrides Sub Render(writer As HtmlTextWriter) Writer.WriteLine("<html><body>") Writer.RenderChildren(writer) Writer.WriteLine("</body><html>") End Sub |
Um die oben definierten User Controls einzubinden kann die Funktion LoadControl verwendet werden. Alle Elemente einer Seite werden intern in einer Collection namens Controls gespeichert und sind darüber direkt ansprechbar. Die Reihenfolge innerhalb dieser Collection definiert zugleich die Position des Elements auf der fertig gerenderten HTML-Ausgabe. In diese Collection wird jedes User Control aber auch jedes einzelne HTML-Element separat aufgelistet. Soll deshalb ein Header und Footer eingebunden werden sieht die Lösung wie folgt aus:
Protected Overrides Sub CreateChildControls() Dim Header As HeaderControl Dim Footer As FooterControl Header = CType(LoadControl("controlsPageHeader.ascx"), HeaderControl) Footer = CType(LoadControl("controlsPageFooter.ascx"), FooterControl) MyBase.CreateControlCollection() Me.Controls.AddAt(0, Header) Me.Controls.AddAt(Me.Controls.Count, Footer) Me.ChildControlsCreated = True End Sub |
Mit dieser Lösung werden zuerst die User Controls geladen und initialisiert. Anschliessend werden sie in der Liste aller Controls der Seite an erster und an letzter Steller hinzugefügt.
Ein Nachteil dieser Lösung ist jedoch, dass man hier davon ausgeht, dass die Tabellenstruktur der Seite direkt in den Controls eingebunden wurde. Weiterhin ist damit der HTML-Code über mehrere Controls verteilt, was nicht gerade als saubere Lösung definiert werden kann.
Single-Point of Administration
Ziel sollte es also sein ein einziges Element zu haben in der das gesamte Rahmenwerk der Seiten definiert ist.
Also gehen wir doch einfach daran ein solches User Control zu erstellen, das sich nur um dieses Seitenstruktur kümmert.
<%@ Control Language="vb" Codebehind="PageBaseControl.ascx.vb" Inherits=" PageBaseControl" %> <%@ Register tagPrefix="CM" tagName="HeaderControl" src="PageHeader.ascx" %> <%@ Register tagPrefix="CM" tagName="NavigationControl" src="PageNavigation.ascx" %> <%@ Register tagPrefix="CM" tagName="FooterControl" src="PageFooter.ascx" %> <html> <head> <title>Testcenter</title> </head> <body> <table border="0" cellspacing="0" cellpadding="0"> <tr> <CM:HeaderControl id="ctlHeader" runat="server" /> </tr> <td> <CM:NavigationControl id="ctlNavigation" runat="server" /> </td> <td> <asp:PlaceHolder id="Content" runat="server" /> </td> </tr> <CM:FooterControl id="ctlFooter" runat="server" /> </table> </body> </html>
|
Bis auf die erste Zeile sieht dieser Quellcode beinahe wie unsere erste ASPX-Seite aus. Statt einer WebForm handelt es sich jedoch nun um ein User Control. Ausserdem habe ich dort, wo später der Content platziert werden soll ein PlaceHolder Standardcontrol eingefügt. Dieses sollte im Codebehind als Public definiert werden, damit man später auf die Elemente im Contentbereich zugreifen kann.
Das sieht schon ganz gut aus! Wir haben eine Datei die das gesamte Layout definiert und alle allgemeingültigen User Controls enthält. Eine Seite, die hiervon abgeleitet wird, sollte nun das ganze Rahmenwerk der Website mitbringen.
Anschliessend wird wieder die Standardmethode CreateChildControls der PageBase Klasse angepasst, um das User Control - wie vorher in der bisherigen Basisklasse von PageBase - zu laden. Die Schwierigkeit liegt darin, dass die Elemente nun auch tatsächlich im PlaceHolder Control des User Controls eingetragen werden.
Die Klasse PageBase dient ja als Basisklasse für alle anderen ASPX-Seiten. Der Aufruf einer ASPX-Seite bewirkt, dass zuerst die Elemente der Web Form in die Page.Controls Collection geladen werden.
Erst später im Ausführungszyklus einer ASPX-Seite wird die Methode CreateChildControls aufgerufen. Zu diesen Zeitpunkt ist die Collection Controls jedoch bereits mit dem HTML-Code und den enthaltenen dynamischen Elemente der aufgerufenen ASPX-Seite gefüllt.
Wir brauchen diese jedoch nicht an dieser Stelle. Stattdessen sollen alle Elemente unseres PageBaseControls als erstes in dieser Liste stehen.
Die Elemente der aufgerufenen ASPX-Seite sollen stattdessen in das WebControl PlaceHolder unseres Rahmenlayouts unserer ASCX-Datei verschoben werden.
Dies erreicht man durch folgenden Code:
Protected Layout As PageBaseControl
<System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent() Try Layout = CType(LoadControl("/controls/PageBaseControl.ascx"), PageBaseControl) Catch ' For design time End Try End Sub
...
Protected Overrides Sub CreateChildControls() Dim c As Control For Each c In Me.Controls Layout.Content.Controls.Add(c) Next Me.Controls.Add(Layout) Me.ChildControlsCreated = True End Sub
|
Ich verwende - in Anlehnung an die Windowsprogrammierung - die Standardmethode InitializeComponent dazu, alle Objekte und Variablen ordentlich zu initialisieren. Dazu gehört auch das Rahmenwerk für das Layout der Seite dazu.
Das Control wird geladen und in dem Attribut Layout gespeichert.
Beim Aufruf der Methode CreateChildControls in die Collection Page.Controls bereits mit den Elemente der aufgerufenen ASPX-Seite gefüllt. Es muss nun also nichts weiter getan werden, als alle Elemente der Reihe nach in den öffentlichen Platzhalter im Layout zu verschieben.
Anschliessend enthält die Controls Collection von Layout alle Elemente des Rahmenlayouts plus alle Elemente der aufgerufenen Seite. Diese Liste muss nun wieder der Controls-Sammlung der Seite übergeben werden.
Fazit
Mit diesem Trick kann man also die Vorteile eines zentralen Layoutrahmenwerkes, die leichte Verwaltung und Pflege ohne grossen Aufwand kombinieren.