ASP.NET mit C# Kochbuch – Update
Wie kann ich ...
Autor: Patrick A. Lorenz, www.p-l.de Copyright © 2002-2003 Carl Hanser Verlag München Wien
2 ______________________________________________________________________
X ASP.NET mit C# Kochbuch – Update Dieses Dokument ergänzt das Buch „ASP.NET mit C# Kochbuch“ um über 80 neue Rezepte und Lösungen. Es handelt sich um eine Vorabversion, die Lesern des Buches kostenlos und ohne Rechtsanspruch zur Verfügung gestellt wird. Diese Vorabversion ist nicht lektoriert und kann vermehrt Rechtschreibfehler enthalten. Auch die weiteren üblichen Maßnahmen zur Qualitätssicherung wurden nicht durchlaufen, so dass etwas das Layout nicht optimiert wurde. Sie erhalten dieses Vorabdruck „wie er ist“ ohne jegliche Haftung oder Garantie. ASP.NET mit C# Kochbuch Umfang: Erschienen: Verlag: Preis: ISBN:
816 Seiten und CD-ROM September 2002 Carl Hanser Verlag München Wien 49,90 € 3-446-22235-9
Weitere Infos:
www.asp-buch.de
Hinweis: Dieses Dokument und alle darin enthaltenen Informationen und Quelltexte dürfen ausschließlich in Verbindung mit dem Buch „ASP.NET mit C# Kochbuch“ verwendet und genutzt werden. Eine Vervielfältigung gleich welcher Art ist strengstens verboten und wird zivil- und strafrechtlich verfolgt werden. Sofern Sie dieses Dokument ohne das Buch erhalten haben, wenden Sie sich bitte an
[email protected]. Darüber hinaus gelten die Hinweise aus dem Impressum des Buches auf Seite 4.
X ASP.NET mit C# Kochbuch – Update ______________________________________ 3
X.1 ... Bilder und binäre Daten in einer Datenbank abspeichern? In manchen Fällen ist es wünschenswert, Bilder oder auch andere binäre Daten in einer zentralen Datenbank vorzuhalten. Natürlich lässt sich ein Bild nicht so ganz einfach in ein Datenbankfeld einfügen, wie etwa eine Zeichenkette. Zudem darf es ein wenig Vorbereitung.
Anlage des Datenbankfeldes Um binäre Informationen in einer Datenbank ablegen zu können, muss diese ein entsprechendes Feld beinhalten. Ich habe der Beispieldatenbank misc.mdb eine neue Tabelle BinaryTest angefügt. Diese enthält zwei Felder ID und BinaryData. Als Typ für das letztere Feld ist „OLE-Objekt“ angegeben. Dies erscheint auf den ersten Blick ein wenig verwunderlich, denn mit OLE hat die Operation eigentlich nichts zu tun. Imgrunde handelt es sich jedoch um ein Long Binary-Feld zur Aufnahme von binären Daten.
Abbildung X.1 Das neue Feld soll die binären Daten aufnehmen.
4 ___________________ X.1 ... Bilder und binäre Daten in einer Datenbank abspeichern?
Abspeichern eines Bildes Ist die Datenbank vorbereitet, können binäre Daten in der neuen Tabelle abgelegt werden. Im nachfolgenden Beispiel wird eine Grafikdatei von der Festplatte eingelesen und in die Datenbank geschrieben. Hierzu wird die FileStream-Klasse verwendet. Deren Methode Read ermöglicht das Kopieren des Dateiinhalts in ein byteArray. Dieses kann in Form eines Parameters an eine INSERT-Query übergeben werden. Listing X.1 BinaryData1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { OleDbConnection conn = new OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;" + @"Data Source=c:\inetpub\wwwroot\aspnet\misc.mdb"); conn.Open(); FileStream stream = File.OpenRead(Server.MapPath("paramount.gif")); byte[] bytes = new byte[stream.Length]; stream.Read(bytes, 0, bytes.Length); stream.Close(); string SQL = "INSERT INTO BinaryTest (BinaryData) VALUES (@BinaryData);"; OleDbCommand cmd = new OleDbCommand(SQL, conn); cmd.Parameters.Add("@BinaryData", bytes); cmd.ExecuteNonQuery(); conn.Close(); }
Im Anschluss an diesen Aufruf enthält die Tabelle BinaryTest einen neuen Datensatz. Natürlich kann Access mit diesen Daten wenig anfangen und so verwundert es nicht, dass statt des Bildes nur ein einfacher Hinweis angezeigt wird.
X ASP.NET mit C# Kochbuch – Update ______________________________________ 5
Abbildung X.2 Access kann die enthaltenen Daten nicht interpretieren.
Auslesen eines Bildes Soll das Bild aus der Datenbank gelesen werden, reicht eine einfache SELECTAbfrage aus. Im folgenden Listing wird die Methode ExecuteScalar verwendet, deren Rückgabewert in ein byte-Array konvertiert wird. Dieses Array wird der Methode Response.BinaryWrite übergeben, so dass nach Angabe des korrekten Content-Types das in der Datenbank abgelegte Bild im Browser angezeigt wird. Listing X.2 BinaryData2.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { OleDbConnection conn = new OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;" + @"Data Source=c:\inetpub\wwwroot\aspnet\misc.mdb"); conn.Open(); string SQL = ORDER BY ID OleDbCommand byte[] bytes
"SELECT TOP 1 BinaryData FROM BinaryTest DESC;"; cmd = new OleDbCommand(SQL, conn); = (byte[]) cmd.ExecuteScalar();
Response.ContentType = "image/gif"; Response.BinaryWrite(bytes); conn.Close(); }
6 ___________________ X.1 ... Bilder und binäre Daten in einer Datenbank abspeichern?
Abbildung X.3 Das Bild wurde aus der Datenbank gelesen und an den Client gesendet.
Natürlich können Sie auch über einen DataReader auf die binären Daten zugreifen. Die notwendigen Zeilen sehen so aus: OleDbDataReader reader = cmd.ExecuteReader(); if(reader.Read()) { byte[] bytes = (byte[]) reader["BinaryData"]; Response.ContentType = "image/gif"; Response.BinaryWrite(bytes); }
Möchten Sie das Bild vor dem Absenden an den Client noch bearbeiten, so können Sie das byte-Array über den Umweg eines MemoryStream in eine Instanz der Klasse Bitmap verwandeln. Anschließend stehen Ihnen die üblichen Möglichkeiten von GDI+ uneingeschränkt zur Verfügung. Informationen hierzu finden Sie im Kapitel „Grafik“. Die notwendigen Zeilen sehen so aus: byte[] bytes = (byte[]) cmd.ExecuteScalar(); conn.Close(); Response.ContentType = "image/gif"; Bitmap b = new Bitmap(new MemoryStream(bytes)); b.Save(Response.OutputStream, ImageFormat.Gif);
Selbstverständlich können Sie neben Bildern auch beliebige andere Daten in einem binären Datenbankfeld ablegen. Die Größe des Feldes ist
X ASP.NET mit C# Kochbuch – Update ______________________________________ 7
mit 2 GB praktisch unbegrenzt. Dennoch sollten Sie tunlichst darauf achten, wann und vor allem wie viel Sie in einer Datenbank ablegen. Größere Daten sollten in aller Regel in Form einer Datei auf der Festplatte abgespeichert werden.
X.2 ... einen Standard-Button defnieren? Spätestens von Windows-Applikationen sind wir es gewohnt, ein Formular mit einem einfachen Druck auf die Enter-Taste zu bestätigen und die wie auch immer geartete Funktionalität auszulösen. Auch bei Web-Formularen hat sich diese Technik mittlerweile durchgesetzt. Klicken Sie noch bei jeder Google-Suche auf den entsprechenden Button? Vermutlich nicht; wie andere benutzen Sie einfach die Tastatur. Im Folgenden habe ich ein einfaches Suchformular nachgebildet. Dieses enthält eine Eingabefeld sowie einen Button. Klickt man auf den Button, wird ein PostBack ausgeführt und der eingegebene Text in einem zusätzlichen Label angezeigt. Listing X.3 defaultbutton1.aspx <script runat="server"> void bt_click(object sender, EventArgs e) { lb_msg.Text = string.Format("Sie haben eingegeben: '{0}'", tb_search.Text); }
Das Formular ist ziemlich simpel, nicht? Natürlich funktioniert es, wie erwartet: ein Klick auf den Button zeigt den Suchtext im Label an oder könnte eine beliebige andere Funktionalität auslösen. Was allerdings, wenn Sie die Eingabe lediglich mit
8 _____________________________________ X.2 ... einen Standard-Button defnieren?
der Enter-Taste bestätigen? Richtig, es wird ein PostBack ausgelöst, aber ansonsten passiert nichts – ASP.NET kennt keine Standard-Buttons. Um die Ursache zu verstehen, muss man sich die Abfolge der Ereignisweiterleitung anschauen. Der Klick auf einen Button wird bei HTML über ein POST-Feld übergeben. ASP.NET ergänzt dieses System um ein verstecktes Formularfeld __EVENTTARGET, in dem zum Beispiel bei einem LinkButton-Control mittels JavaScript der Name des auslösenden Objekts abgelegt wird. Auf diese Weise kann das Control auf dem Server ermittelt und das entsprechende Ereignis ausgelöst werden.
Lösung für das Button-Control Die Lösung für das Problem ist recht einfach. Statt darauf zu warten, dass ein Button das versteckte Formularfeld anlegt und mit dem Namen des auslösenden Objekts bestückt, können Sie dieses Feld bereits vorher explizit anlegen und darin den Namen des als Standard zu verwendenden Buttons ablegen. Das abgeänderte Beispiel zeigt, wie es geht. Listing X.4 defaultbutton2.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { this.RegisterHiddenField("__EVENTTARGET", "bt_search"); } void bt_click(object sender, EventArgs e) { lb_msg.Text = string.Format("Sie haben eingegeben: '{0}'", tb_search.Text); }
X ASP.NET mit C# Kochbuch – Update ______________________________________ 9
Das Listing unterscheidet sich in zwei Punkten von dem vorheringen. Zum einen wurde dem Button-Control explizit ein Name zugewiesen. Dies ist erforderlich, da dieser Name in der neu eingefügten Ereignisbehandlung Page_Load dem versteckten Formularfeld zugewiesen wird. Zur Anlage des Feldes wird die Methode RegisterHiddenField der Klasse Page verwendet. Soll die Funktionalität aus einem (User) Control heraus aufgerufen werden, verwenden Sie alternativ folgenden Aufruf: Page.RegisterHiddenField("__EVENTTARGET", "bt_search");
Die Abbildung lässt vermuten, dass nun ein einfacher Druck auf die Enter-Taste ausreicht, um die serverseitige Ereignisbehandlung des Buttons auszulösen.
Abbildung X.4 Die Suche kann nun auch mit der Enter-Taste ausgelöst werden.
Lösung für das LinkButton-Control Bei regulären Button-Controls lässt sich dieses System problemlos einsetzen. Anders sieht es bei LinkButton-Controls aus. Diese verwenden statt einfachem HTML JavaScript. Um einen solchen Button über die Enter-Taste auslösen zu können, benötigen Sie daher ebenfalls JavaScript. Innerhalb des Eingabefeldes müssen Sie auf das clientseitige keydown-Ereignisses reagieren. Wurde die Enter-Taste gedrückt (Keycode 13), lösen Sie einen PostBack aus. Der hierzu notwendige Quellcode wird Ihnen von Methode GetPostBackEventReference der Klasse Page zur Verfügung gestellt.
10 ____________________________________ X.2 ... einen Standard-Button defnieren?
Listing X.5 defaultbutton3.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { if(!IsPostBack) { string script = string.Format("if(window.event.keyCode == 13) {0}", this.GetPostBackEventReference(bt_search)); tb_search.Attributes.Add("onkeydown", script); } } void bt_click(object sender, EventArgs e) { lb_msg.Text = string.Format("Sie haben eingegeben: '{0}'", tb_search.Text); }
Abbildung X.5 Das serverseitige Eregnis wurde über die Enter-Taste ausgelöst.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 11
Lösung für mehrere Eingabefelder Bei mehreren Eingabefeldern kommt es jedoch mitunter vor, das für jedem dieser Felder ein separater Button zugeordnet ist, der eine entsprechende Funktionalität auslöst. Bei regulären Button-Controls ist eine Differenzierung schwierig, da Eingriff per JavaScript serverseitig ignoriert wird. In diesem Fall wird leider einfach die Ereignisbehandlung des ersten Buttons ausgelöst. Verwenden Sie jedoch LinkButton-Controls, können Sie wie im vorherigen Beispiel gezeigt vorgehen. Angenommen, Sie integrieren zwei Eingabefelder mit jeweils einem zugeordneten LinkButton-Control, so würde die Zuordnung mit folgendem JavaScript-Quellcode vorgenommen: void Page_Load(object sender, EventArgs e) { if(!IsPostBack) { string script = string.Format("if(window.event.keyCode == 13) {0}", this.GetPostBackEventReference(bt_search1)); tb_search1.Attributes.Add("onkeydown", script); script = string.Format("if(window.event.keyCode == 13) {0}", this.GetPostBackEventReference(bt_search2)); tb_search2.Attributes.Add("onkeydown", script); } }
Abbildung X.6 Mehrere Eingabefelder verfügen über mehrere Default-Buttons
12 _______________________________ X.3 ... einen eindeutigen Dateinamen erstellen?
X.3 ... einen eindeutigen Dateinamen erstellen? Sofern Sie eine Datei temporär auf der Festplatte ablegen wollen, können Sie einen eindeutigen Dateinamen über die Methode GetTempFileName der Klasse Path ermitteln. Dies funktioniert allerdings ausschließlich für das im System hinterlegte Temp-Verzeichnis. Soll die Datei an anderer Stelle abgelegt werden, muss eine alternative Lösung gefunden werden. Eine einfache Möglichkeit, einen eindeutigen Namen für eine Datei zu erzeugen, bietet die Struktur Guid aus dem Root-Namespace System. Diese repräsentiert einen so genannten globally unique identifier. Dabei handelt es sich um einen weltweit eindeutigen 128-Bit-Wert, der unter anderem auf Basis der aktuellen Zeit sowie der ID der Netzwerkkarte erzeugt wird. Durch dieses System lassen sich Dublikate nahezu ausschließen – und das wohlgemerkt weltweit. Das folgende Listing zeigt, wie Sie mit Hilfe der Struktur einen eindeutigen Dateinamen erzeugen können. Dazu wird die statische Methode NewGuid verwendet.
Listing X.6 UniqueFilename1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { Guid guid = Guid.NewGuid(); string directory = Server.MapPath("/"); string fileName = Path.Combine(directory, guid.ToString() + ".txt"); Response.Write(fileName); }
Abbildung X.7 Der Dateiname ist mit sehr hoher Wahrscheinlichkeit eindeutig.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 13
X.4 ... feststellen, ob auf dem Client das .NET Framework installiert ist? Mitunter ist es nützlich festzustellen, ob auf dem Client das .NET Framework installiert ist und ja, welche Version. Die offizielle Website www.asp.net prüft beispielsweise beim Download von bestimmten Zusatzprodukten ab, ob die entsprechenden Systemvoraussetzungen erfüllt sind. Sofern der Benutzer Ihre Seiten mit einem Internet Explorer ab Version 5.0 aufruft, wird die von ihm benutzte CLR-Version (Common Language Runtime) mit der HTTP-Kopfzeile UserAgent übertragen. Sie können diese mit der folgende Zeile ausgeben: Response.Write(Request.UserAgent);
Im Falle eines installierten .NET Frameworks sieht die Ausgabe beispielsweise so aus: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705)
Statt die Zeichenkette nun manuell auf das Vorkommen der CLR-Version zu parsen, können Sie dies den Browser-Capabilities überlassen. Die Klasse liefert über die Eigenschaft ClrVersion Zugriff auf eine Version-Instanz. Ist die davon gelieferte Hauptversion ungleich 0, ist das Framework installiert. Eine entsprechende Abfrage sieht wie folgt aus: Listing X.7 FrameworkInstalled1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { Version clrVersion = Request.Browser.ClrVersion; if(clrVersion.Major != 0) lb_msg.Text = string.Format("Das .NET Framework ist installiert (Version: {0})", clrVersion.ToString()); else lb_msg.Text = "Das .NET Framework ist nicht installiert."; }
14 ________________________________ X.5 ... einen sicheren Zurück-Button erstellen?
Abbildung X.8 Sie können ermitteln, ob das Framework auf dem Client installiert ist.
Auch wenn ich es gerade schon im Nebensatz erwähnt habe, diese Funktionalität steht ausschließlich mit dem Internet Explorer ab Version 5.0 zur Verfügung. Verwendet der Besucher Mozilla oder einen anderen Browser, haben Sie das Einsehen.
X.5 ... einen sicheren Zurück-Button erstellen? Viele Websites verwenden zur Navigation einen Zurück-Button, mit Hilfe dessen die zuvor aufgerufene Seite erneut angezeigt werden kann. Oftmals wird diese Funktionalität über JavaScript realisiert. Dies ist prinzipiell auch bei ASP.NET möglich. Durch die neue PostBack-Technik bereitet der Rücksprung einigen Browsern wie Mozilla jedoch Probleme. Statt auf die ursprüngliche Seite aufzurufen, wird der Zustand der aktuellen vor dem PostBack angezeigt. Die Lösung für das Problem ist gleichwohl eine angenehme Alternative auch für andere Browser, kommt sie doch völlig ohne clientseitige Scripts aus. Statt dessen wird die Referrer-Kopfzeile ausgewertet, die vom Browser an den Server gesendet wird. Hier teilt der Browser mit, von welcher Seite die aktuelle aufgerufen wurde. Zu dieser Adresse können Sie einen Redirect durchführen. Höre ich da ein „aber“? Natürlich wird die die Referrer-Kopfzeile (sie steht in ASP.NET über die Eigenschaft Request.UrlReferrer zur Verfügung) im Zuge des PostBacks nicht mehr korrekt übertragen. Der Aufrufer ist in diesem Fall ja die eigene Seite. Es ist daher notwendig, den Referrer beim ersten Aufruf der Seite abzufragen und im ViewState abzulegen. Das folgende Listing zeigt eine entsprechende Implementierung: Listing X.8 SecondPage1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { if(!IsPostBack)
X ASP.NET mit C# Kochbuch – Update _____________________________________ 15
{ if(Request.UrlReferrer != null) this.Referrer = Request.UrlReferrer.ToString(); } } void bt_back_click(object sender, EventArgs e) { this.RedirectToReferrer(); } protected string Referrer { get { return(ViewState["referrer"] as string); } set { ViewState["referrer"] = value; } } protected void RedirectToReferrer() { if(this.Referrer != null) Response.Redirect(this.Referrer); else Response.Redirect("default.aspx"); }
Innerhalb des Page_Load-Ereignisses wird der Referrer abgefragt und über eine Eigenschaft im ViewState abgelegt. Über den ersten der beiden Buttons können Sie nun beliebige PostBacks initiieren. Wann immer Sie auf den zweiten Button klicken, führt dieser Sie über die Methode RedirectToReferrer zurück auf die ursprüngliche Seite. Wurde die Adresse hingegen direkt aufgerufen oder kein Referrer übertragen, wird statt dessen die default.aspx im aktuellen Verzeichnis angezeigt.
16 __________ X.6 ... Statusinformationen während des Uploads von Dateien ausgeben?
Ich habe die Funktionalität bewusst sehr allgemein angelegt. Es bietet sich an, die Aufrufe in einer zentralen Code Behind-Klasse abzulegen, so dass diese in allen Seiten automatisch zur Verfügung steht.
Abbildung X.9 Per Button-Klick landen Sie auf der vorherigen Seite.
X.6 ... Statusinformationen während des Uploads von Dateien ausgeben? ASP.NET bietet mit dem HttpInputFile-Control eine einfache wie effektive Möglichkeit, Dateien vom Client an den Server zu übertragen. Gerade bei größeren Dateien wäre eine Statusausgabe sehr wünschenswert. Auf diese Weise könnte Benutzer einfach feststellen, wie viele der Daten bereits übertragen wurden. In den Foren und Newsgroups ist mehrfach ein entsprechender Wunsch laut geworden. Leider mussten die Fragesteller immer enttäuscht werden, denn es gibt keine derartige Lösung. Die Ursache hierfür liegt in der Architektur des Protokolls HTTP. Dieses überträgt die Daten in einem Schwung und erhält erst anschließend die Ergebnisseite. In der Zwischenzeit ist weder die eine noch die andere Seite aktiv. Zudem stellen die Browser keine Informationen zur Verfügung. Leider P.g. (Pech gehabt).
X.7 ... eine Zeichenkette als Aufzählungsliste darstellen? Ab und an möchte man eine gegebene Zeichenkette in Form einer Aufzählungsliste darstellen. Konkret gemeint sind die HTML-Tags
und - . Um diese Tags auf jede anzuwenden, können Sie natürlich die Zeichenkette durchlaufen und an den entsprechenden Stellen (Umbruch) ergänzen. Doch so richtig schön ist diese Lösung nicht.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 17
Eine Alternative steht Ihnen mit dem Repeater-Control zur Verfügung. Diesem können Sie die Zeichenkette als Datenquelle zuweisen. Zuvor müssen Sie lediglich die Methode Split benutzen, um die Zeichenkette in ein Array der einzelnen Zeilen umzuwandeln. Das Listing zeigt, wie’s geht.
Abbildung X.10 Die Zeichenkette wird in Form einer Aufzählung dargestellt.
Listing X.9 BulletString1.aspx <script runat="server"> void bt_Click(object sender, EventArgs e) { string text = tb.Text.Trim(); rp.DataSource = text.Split('\r'); rp.DataBind(); rp.Visible = (rp.Items.Count > 0); }
18 __________________ X.8 ... die Fußzeile eines DataGrid-Controls individuell nutzen?
Das Listing enthält ein mehrzeiliges Eingabefeld, einen Button und einen Repeater. Ein Klick auf den Button befördert die eingegebene Zeichenkette in die Aufzählung.
X.8 ... die Fußzeile eines DataGrid-Controls individuell nutzen? Wußten Sie dass das DataGrid-Control neben einer Kopf- auch über ein Fußzeile verfügt? Ja, dann wissen Sie vermutlich auch, dass diese zusätzliche Zeile standardmäßig nicht dargestellt wird beziehungsweise die entsprechende Option ShowFooter deaktiviert wird. Ich finde, dies geschieht aus gutem Grund, denn die Bordmöglichkeiten zur Benutzung der Fußzeile sind nicht sehr ausgereift. Von der TemplateColumn-Spalte einmal abgesehen lässt sich dort lediglich ein statischer Text analog zu der Kopfzeile unterbringen. Mit ein paar Tricks können Sie mehr aus der Zeile heausholen. Wie so oft kommt das heiß und innig von mir geliebte Ereignis ItemCreated zum Einsatz. Hier können Sie auf die Erzeugung der Fußzeile warten und die automatischen Spalten Inhalte durch eine individuell angelegte Zeile ersetzen. Das Listing enthält eine einfache Umsetzung dieses Vorgehens. Listing X.10 Footer1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { // Datenbindung } void dg_ItemCreated(object sender, DataGridItemEventArgs e) { if(e.Item.ItemType == ListItemType.Footer) {
X ASP.NET mit C# Kochbuch – Update _____________________________________ 19
int columns = e.Item.Cells.Count; e.Item.Cells.Clear(); TableCell cell = new TableCell(); cell.ColumnSpan = columns; cell.Text = "Diese Fußzeile enthält
einen individuellen Text :-)"; e.Item.Cells.Add(cell); } }
Im Listing werden die bisherigen Zellen der Fußzeile gelöscht. Anschließend wird eine Zelle angelegt, die über die gesamte Breite des DataGrid-Controls verläuft. Über die Eigenschaft Text wird eine individuelle Ausgabe erzeugt. Die Abbildung zeigt das Ergebnis.
Abbildung X.11 Die Fußzeile wurde individuell ersetzt.
Fußzeile im Layout-Bereich hinterlegen Auf die gezeigte Weise können Sie theoretisch beliebige komplexe Fußzeilen erzeugen. In der Praxis ist es jedoch nicht wirklkich angenehm, die Objekte aus-
20 __________________ X.8 ... die Fußzeile eines DataGrid-Controls individuell nutzen?
schließlich programmatisch anzulegen. Viel komfortabler und auch im Sinne der Trennung von Quellcode und Design ist die Ablage innerhalb des HTML-Bereichs der Seite. Da dies von Haus nicht angeboten wird, können Sie stattdessen ein PlaceHolder-Control platzieren und dessen Inhalte dynamisch übernehmen. Sofern Sie mit der Entwicklungsumgebung Visual Studio .NET arbeiten, tut es selbstverständlich auch das dort besser unterstützte Panel-Control. Das zweite Listing enthält eine derartige Implementierung. Listing X.11 Footer2.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { // Datenbindung } void dg_ItemCreated(object sender, DataGridItemEventArgs e) { if(e.Item.ItemType == ListItemType.Footer) { int columns = e.Item.Cells.Count; e.Item.Cells.Clear(); TableCell cell = new TableCell(); cell.ColumnSpan = columns; int count = footer.Controls.Count; for(int i = 0; i < count; i++) cell.Controls.Add(footer.Controls[0]); e.Item.Cells.Add(cell); } } Diese Fußzeile enthält einen :-)
X ASP.NET mit C# Kochbuch – Update _____________________________________ 21
Nach dem Aufruf der Beispielseite wird der Inhalt des definierten PlaceHolderControls innerhalb der Fußzeile des DataGrid-Controls angezeigt. Die Abbildung beweist, dass dieses System problemlos funktioniert.
Abbildung X.12 Der Inhalt des PlaceHolder-Controls wird in der Fußzeile angezeigt.
Fußzeile um dynamisch errechnete Daten ergänzen Mit dem vorgestellten System lässt sich die Fußzeile eines DataGrid-Controls sehr flexibel nutzen. Wenngleich die Möglichkeiten und individuellen Anforderung sehr unterschiedlich sind, so ist eine davon sehr häufig gewünscht. Oftmals soll ähnlich wie bei einem Rechnungsformular unterhalb der Tabelle eine Zusammenfassung der angezeigten Daten ausgegeben werden. Auch dies möglich. Das dritte Beispiel dieses Rezepts demonstriert die Ausgabe eines dynamisch aus der Datenquelle errechneten Wertes. Die Fußzeile enthält ein Literal-Control, dass die Anzahl der angezeigten Autoren aufnehmen soll. Der Wert wird über die Methode DataTable.Compute errechnet. Natürlich könnte ich auch einfach die CountEigenschaft der DataRowCollection benutzen, dieser Ansatz zeigt jedoch, dass analog auch andere (und komplexere) Berechnungen möglich sind. Es steht Ihnen der volle Umfang der ADO.NET-Expressions zur Verfügung. Listing X.12 Footer3.aspx <script runat="server">
22 __________________ X.8 ... die Fußzeile eines DataGrid-Controls individuell nutzen?
void Page_Load(object sender, EventArgs e) { // Datenbindung } void dg_ItemCreated(object sender, DataGridItemEventArgs e) { if(e.Item.ItemType == ListItemType.Footer) { int columns = e.Item.Cells.Count; e.Item.Cells.Clear(); TableCell cell = new TableCell(); cell.ColumnSpan = columns; int count = footer.Controls.Count; for(int i = 0; i < count; i++) cell.Controls.Add(footer.Controls[0]); Literal literal = (Literal) cell.Controls[1]; DataTable table = ((DataSet) dg.DataSource).Tables[0]; literal.Text = table.Compute("COUNT(ID)", string.Empty).ToString(); e.Item.Cells.Add(cell); } } In der Tabelle werden Autoren angezeigt!
X ASP.NET mit C# Kochbuch – Update _____________________________________ 23
Abbildung X.13 Die Anzahl der enthaltenen Autoren wird in der Fußzeile angezeigt.
X.9 ... in Visual Basic .NET auf alte VB-Funktionen zugreifen? Gerade wer von dem alten „clasic“ ASP auf ASP.NET umsteigt, der wird einige Dinge vermissen. Hierzu zählen insbesondere die vielen kleinen und nützlichen Visual Basic beziehungsweise VBScript-Funktionen, die das Leben eines Entwicklers immens erleichtern können. Viele dieser Funktionen sind weggefallen und existieren nun in Form von Eigenschaften und Methoden in den Klassen des .NET Frameworks. Viele Entwickler möchten zumindest in der Übergangsphase bei den bekannten Funktionen bleiben. Wenngleich ich persönlich dies nicht unbedingt präferiere, kann ich es doch zumindest verstehen. Problematisch ist dies nicht, denn Microsoft hat zumindest in Teilen für eine Kompatibilität gesorgt. Schauen Sie sich beispielsweise folgendes Listing an: Listing X.13 OldVBFunctions1.vb Imports System Public Class Test Public Function GetNextWeek() As DateTime GetNextWeek = DateAdd("ww", 1, Now) End Function End Class
24 ___________________ X.9 ... in Visual Basic .NET auf alte VB-Funktionen zugreifen?
Natürlich ist sofort klar, was hier passiert. Die Methode liefert das aktuelle addiert um eine Kalenderwoche zurück. Doch lässt sich diese Quellcode-Datei kompilieren? Wie Abbildung zeigt, werden sowohl DateAdd als auch Now vom Kompiler nicht erkannt.
Abbildung X.14 Die alten Funktionen und Schlüsselwörter werden nicht gefunden.
Des Rätsels Lösung ist sehr einfach, Sie müssen lediglich den entsprechenden Namespace importieren. Hierzu fügen Sie dem Listing folgende Zeile hinzu – anschließend lässt sich die Datei problemlos kompilieren: Imports Microsoft.VisualBasic
Natürlich können Sie auf diese Weise problemlos die alten Funktionen und Schlüsselwörter benutzen. Wie bereits erwähnt, ist dies nicht die von mir präferierte Vorgehensweise. Ich empfehle die Verwendung der neuen Eigenschaften und Methoden des Frameworks. Das vorangegangene Beispiel würde demnach im folgenden Listing aussehen. Listing X.14 NewFrameworkMethods1.vb Imports System Public Class Test Public Function GetNextWeek() As DateTime GetNextWeek = DateTime.Now.AddDays(7) End Function End Class
X ASP.NET mit C# Kochbuch – Update _____________________________________ 25
X.10 … eigene Rollen/Gruppen mit Forms Authentication verwenden? Das Sicherheitssystem von ASP.NET basiert auf Rollen, auch Gruppen genannt. Anstatt jedem Benutzer individuell Rechte zuzuweisen, fügen Sie diesen einfach einer oder mehreren logischen Gruppen hinzu und vergeben das benötigte Rechte für die gesamte Gruppe. Dies vereinfacht nicht nur die Anlage von neuen Benutzern, sondern auch deren Administration und verringert den Arbeitsaufwand bei notwendigen Änderungen. Das System entspricht konzeptionell in etwa dem der Windows-Benutzerverwaltung. Sofern Sie mit der Windows Authentication arbeitet, werden tatsächlich die Gruppen zu Grunde gelegt, der der Benutzer auf dem lokalen System oder der Domäne angehört. Anders sieht es bei der Forms Authentication aus. Angenommen, Sie haben die Benutzer samt Gruppen in einer Datenbank hinterlegt: wie soll ASP.NET nach der Authentifizierung an die benötigten Gruppeninformationen gelangen und müssen diese tatsächlich bei jeder Anforderung einer neuen Seite ausgelesen werden? Dieses Rezept enthält die Antworten.
Anlage der Datenbankstruktur Um eine Authentifizierung mit Gruppen realisieren zu können, benötigen Sie zunächst die entsprechende Datenbasis. Ich habe dazu die Beispieldatenbank users.mdb um zwei Tabellen erweitert. Groups enthält die zur Verfügung stehenden Gruppendefinitionen. Die Tabelle Users_Groups realisiert die relationale n:mVerknüpfung zwischen der Benutzer- und der Gruppentabelle.
Abbildung X.15 Die Datenbank enthält die Benutzer und die zugeordneten Rollen.
26 _____________ X.10 … eigene Rollen/Gruppen mit Forms Authentication verwenden?
Die Beispieldatenbank enthält zwei Benutzer. Beide sind der Gruppe Users zugeordnet, einer davon zusätzlich der Gruppe Admins. Die zweite Abbildung zeigt die Benutzerdaten.
Abbildung X.16 Die zwei Benutzer sind Mitglieder der angegebenen Gruppen.
Steuerung der Zugriffsberechtigungen Die Zugriffsberechtigungen werden wie gehabt in der Konfigurationsdatei web.config hinterlegt. Statt einzelne Benutzer zu anzugeben, erfolgt die Vergabe der Rechte auf Basis der Gruppennamen. Listing X.15 Web.config <system.web> <deny users="?"/> <system.web> <deny users="*"/>
X ASP.NET mit C# Kochbuch – Update _____________________________________ 27
Im konkreten Fall erhalten alle angemeldeten Benutzer unabhängig von ihren Gruppenmitgliedschaften Zugriff auf die Web-Applikation. Über das location-Tag wird jedoch der Zugriff auf die Seite admin.aspx nur den Mitgliedern der AdminsGruppe gewährt. Selbstverständlich ist neben der Angabe einer einzelnen Datei wie in diesem Fall alternativ auch die Nennung eines ganzen Verzeichnisses möglich.
Anlage der Seitenstruktur Um das Beispiel mit Leben zu füllen, bedarf es genau dreier Seiten: 1. Eine default.aspx, auf die lediglich angemeldete Benutzer zugreifen dürfen. 2. Eine admin.aspx, auf die lediglich Mitglieder der Admins-Gruppe zugreifen dürfen 3. Eine Anmeldeseite login.aspx, die für Eingabe und Überprüfung der Benutzerdaten gegen die Datenbank verantwortlich ist. Die beiden erstgenannten Seiten sind wenig spektakulär, so dass ich auf deren Abdruck verzichte. Interessant ist hingegen die Anmeldeseite. Diese bekommt nun zwei Funktionen, denn außer zur Anmeldung selbst wird sie auch dann aufgerufen, wenn ein Benutzer nicht über die erforderlichen Rechte verfügt, eine angeforderte Seite zu betrachten. Eine Unterscheidung kann auf Basis der Eigenschaft User.Identity.IsAuthenticated vorgenommen werden. Ist der Benutzer bereits angemeldet, so verfügt er vermutlich nicht über die notwendigen Rechte. Entsprechend diesem Wert werden zwei in der Seite enthaltene Panel-Controls gesteuert.
Listing X.16 login.aspx <script language="C#" runat=server> void Page_Load(object sender, EventArgs e) { Panel1.Visible = (!this.User.Identity.IsAuthenticated); Panel2.Visible = this.User.Identity.IsAuthenticated; } void Submit_Click(object sender, EventArgs e) { if(FormsAuthentication.Authenticate(tb_username.Text, tb_password.Text)) {
28 _____________ X.10 … eigene Rollen/Gruppen mit Forms Authentication verwenden?
FormsAuthentication.RedirectFromLoginPage(tb_username.Text, cb_persist.Checked); } else { lb_info.Text = "Die Anmeldung konnte nicht bestätigt werden. Bitte versuchen Sie es noch einmal."; } } Login Bite geben Sie Ihre Benutzerdaten an:
Benutzername:
Benutzername:
Zugriff verweigert Sie haben nicht die notwendigen Rechte, um die gewünschte Seite aufzurufen.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 29
Von der beschriebenen Unterscheidung einmal abgesenen, entspricht das Formular dem gändigen Aufbau bei der Forms Authentication. Gibt der Benutzer seine Daten ein, werden diese gegen die Datenbank überprüft und die Anmeldung gegebenenfalls freigegeben beziehungsweise einer Fehlermeldung ausgegeben.
Abbildung X.17 Über die Seite kann sich ein Benutzer anmelden.
Sind die so genanten Credentials korrekt, wird ein Redirect auf die ursprünglich angeforderte Seite durchgeführt. Im vorliegenden Beispiel handelt es sich in aller Regel um die default.aspx. Diese Seite enthält einen Link auf die admin.aspx, die über die web.config speziell geschützt ist. Das Ergebnis sehen Sie in der nächsten Abbildung.
30 _____________ X.10 … eigene Rollen/Gruppen mit Forms Authentication verwenden?
Abbildung X.18 Der Zugriff wird verweigert.
Der Zugriff auf die Administrationsseite ist nicht möglich, obwohl der Benutzer „patrick“ Mitglied der entsprechenden Gruppe ist. Warum? Nun ja, in der Datenbank ist die Zuordnung korrekt hinterlegt, nur „weiß“ ASP.NET noch nichts davon.
Zuweisung der Gruppenmitgliedschaften Um ASP.NET die Gruppen beizubringen, bedarf es schon einige Hürden zu umschiffen. Der prinzipielle Ansatz sieht vor, innerhalb des Application_AuthenticateRequest-Ereignisses der global.asax die Gruppenmitgliedschaften eines Benutzers aus der Datenbank zu lesen und an die Engine zu übergeben. Das ist soweit nicht unbedingt schwierig, doch wäre es wirklich performant, diese Informationen bei jedem Seitenaufruf abzufragen? Natürlich nicht, also müssen die Informationen irgendwo abgelegt werden. „Session“ sagen Sie? Geht nicht, denn das Ereignis AcquireRequestState wird prinzipiell nach AuthenticateRequest ausgelöst und der Session-State steht ab zur Verfügung. „Cookie“ meinen Sie? Kann man die nicht relativ leicht manipulieren? Hmm, Fragen über Fragen ... Das soll keine allgemeine Rätselstunde werden und ich möchte Sie nicht länger auf die Folter spannen. Da der Session-State nicht zur Verfügung steht, müssen die Informationen tatsächlich in einem Cookie abgespeichert werden. Damit dieser vor Manipuliation und somit Missbrauch geschützt ist, muss er verschlüsselt werden. Dazu können die Techniken verwendet werden, die ASP.NET auch zum Schutz des regulären Forms Authentication Tickets nutzt. Nachfolgend finden Sie komplette Listing der global.asax – mehr benötigen Sie nicht, um die Rollen auszulesen und sicher in einem Cookie abzuspeichern. Bitte schauen Sie sich die verschiedenen Methoden und deren Ablauf kurz an, bevor Sie die im Anschluss an das Listing folgende Beschreibung lesen.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 31
Listing X.17 global.asax
<script runat="server"> void Application_AuthenticateRequest(object sender, EventArgs e) { if((this.User != null) && (this.User.Identity != null) && (this.User.Identity.IsAuthenticated)) { ReadRolesFromCookie(); } } private void ReadRolesFromCookie() { bool ticketOk = false; HttpCookie cookie = Request.Cookies["roles"]; if(cookie != null) { FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value); if((ticket.Name == this.User.Identity.Name) && (!ticket.Expired)) { string[] roles = ticket.UserData.Split('|'); this.Context.User = new GenericPrincipal(this.User.Identity, roles); ticketOk = true; } } if(!ticketOk) CreateNewRoleCookie(); } private void CreateNewRoleCookie() { OleDbConnection conn = new OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;" + @"Data Source=c:\inetpub\wwwroot\aspnet\users.mdb"); conn.Open(); OleDbCommand cmd = new OleDbCommand(); cmd.Connection = conn;
32 _____________ X.10 … eigene Rollen/Gruppen mit Forms Authentication verwenden?
cmd.CommandText = "SELECT Name FROM Groups WHERE ID IN (SELECT GroupID FROM Users_Groups WHERE UserID IN (SELECT ID FROM Users WHERE Username=@Username));"; cmd.Parameters.Add("@Username", this.User.Identity.Name); OleDbDataReader reader = cmd.ExecuteReader(); ArrayList roleList = new ArrayList(); while(reader.Read()) { roleList.Add(reader.GetString(0)); } conn.Close(); string[] roles = (string[]) roleList.ToArray(typeof(string)); FormsAuthenticationTicket ticket = new FormsAuthenticationTicket( 1, this.User.Identity.Name, DateTime.Now, DateTime.Now.AddHours(1), false, string.Join("|", roles)); HttpCookie cookie = new HttpCookie("roles"); cookie.Value = FormsAuthentication.Encrypt(ticket); Response.Cookies.Add(cookie); this.Context.User = new GenericPrincipal(this.User.Identity, roles); }
Das Listing ist in drei Bereich gegliedert. Die Ereignisbehandlung ruft die Methode ReadRolesFromCookie auf, allerdings nur sofern der Benutzer bereits authentifiziert wurde. Diese Bedingung ist notwendig, da zumindest eine Seite frei zugänglich sein muss: die Anmeldung. Die aufgerufene Methode ReadRolesFromCookie überprüft, ob ein Cookie mit dem Namen „roles“ existiert. Ist dies nicht der Fall, so wird dieMethode CreateNewRoleCookie aufgerufen. Diese ließt die Gruppenzugehörigkeiten auf Basis des authentifizierten Benutzernamens aus der Datenbank und befördert Sie in ein stringArray. Damit die Gruppen vor Missbrauch geschützt sind, wird ein AuthenticationTicket angelegt, das verschlüsselt als Cookie mit dem Namen „roles“ gespeichert wird. Dem Ticket werden der Benutzername sowie das in eine einzelne Zeichenkette umgewandelte string-Array übergeben. Als Ablaufzeitpunkt wird eine Stunde veranschlagt. Spätestens dann wird das Ticket neu aufgebaut. Um ASP.NET die Rollen mitzuteilen, wird ein neuer GenericPrincipal angelegt und der Eigenschaft User der Klasse Context übergeben; nur hier ist ein set-
X ASP.NET mit C# Kochbuch – Update _____________________________________ 33
Accessor vorhanden. Dem neuen Principal wird auch das Rollen-Array übergeben, so dass ASP.NET die Zugehörigkeiten für diesen einen Seitenaufruf kennt. Ab dem zweiten Aufruf greift die gezeigte Bedindung innerhalb der Methode ReadRolesFromCookie. Nun wird das Ticket aus dem Cookie ausgelesen, die Gruppen extrahiert und analog ein einer GenericPrincipal angelegt. Hier sollte unbedingt darauf geachtet werden, den im Ticket enthaltenen Namen mit dem aktuellen zu überprüfen. Ansonsten könnte sich der Benutzer ab- und mit neuen Daten anmelden. Die Gruppen des vorherigen Accounts würden jedoch bestehen bleiben.
Abbildung X.19 Dank global.asax kann der Admin nun endlich auf seinen Bereich zugreifen.
Wie Sie in der Abbildung sehen können, funktioniert das System tadellos. Der Zugriff auf den geschützten Administrationsbereich ist nun möglich. Dies ist allerdings nur der Fall, wenn man Mitglied in der entsprechen Gruppe ist, ansonsten wird der Zugriff wie weiter oben gezeigt verweigert.
X.11 ... das erste Eingabefeld eines DataGrid-Controls automatisch selektieren? Das Editieren von Daten innerhalb eines DataGrid-Controls ist ausgesprochen komfortabel. Zum Wechseln in den Bearbeitungsmodus bedarf es jedoch eines Round Trips zum Server. Sofern SmartNavigation nicht eingschaltet ist, geht die aktuelle Fensterposition verloren und unter Umständen ist die zu bearbeitende Zeile nicht direkt sichtbar. Es bietet sich daher an, das erste Eingabefeld einer solchen Zeile automatisch zu selektieren. Das folgende Listing enthält eine derartige Umsetzung, die auf dem DataGridEreigis ItemDataBound basiert. Hier wird die ID der TextBox abgefragt und einem
34 _____ X.11 ... das erste Eingabefeld eines DataGrid-Controls automatisch selektieren?
kleinen JavaScriplet übergeben. Leider wird das HTML-Attribute des TextBoxControls bei der dynamischen Anlage mit Doppelpunkten versehen, obwohl die Eigenschaft ClientID einer mit Unterstrichen ersetzte Version liefert. Aus diesem Grund musste ich auf die document.all-Collection zugreifen, ein DHTML-Objekt das in dieser Form nur im Internet Explorer zur Verfügung steht. Folgerichtig ist funktioniert das Listing nur in diesem Browser. Damit andere Besucher keine Fehlermeldung erhalten, habe ich eine kleine Browserweise implementiert. Listing X.18 DataGridEdit1.aspx <script runat="server"> ... void dg_ItemDataBound(object sender, DataGridItemEventArgs e) { if(e.Item.ItemType == ListItemType.EditItem) { if(Request.Browser.Browser.ToUpper().IndexOf("IE") > -1) { string clientId = e.Item.Cells[0].Controls[0].UniqueID; string script = "<script language=\"javascript\">\r\n"; script += string.Format(" document.all[\"{0}\"].select(); document.all[\"{0}\"].focus();\r\n"; this.RegisterStartupScript("editTextBoxFocus", script); } } }
Die Abbildung zeigt das Beispiel im Live-Einsatz. Nachdem eine Zeile in den Bearbeitungsmodus versetzt wurde, wird das erste Eingabefeld automatisch angesprungen und der Inhalt selektiert. Selbstverständlich sorgt der Browser zudem dafür, dass das Control sich im sichtbaren Bereich befindet.
Abbildung X.20 Das Eingabefeld wurde automatisch markiert.
X.12 ... den SQL-Operator LIKE mit Datenbankparametern verwenden? Der LIKE-Operator ist eine einfache Möglichkeit, Datenbankfelder mit Platzhaltern nach einem Begriff zu durchsuchen. Übergibt man den Suchbegriff – was ich drin-
36 ____________ X.12 ... den SQL-Operator LIKE mit Datenbankparametern verwenden?
gend empfehle – nicht als Zeichenkette, sondern als Parameter über die gleichnamige Collection der Command-Klasse, so werden die Datensätze in manchen Fällen nicht gefunden. Das Problem ist die Übergabe der Wildcards. Diese dürfen nicht innerhalb des Datenbankparameters enthalten sein, da sie ansonsten als zu suchender Inhalt und nicht als Platzhalter interpretiert werden. Die folgende Beispiel zeigt, wie Sie das Problem umgehen können. Das Listing enthält ein DataGrid, ein Eingabefeld sowie einen Button. Ein Klick initiiert eine Datenbankabfrage; der eingegebene Text dient als Suchbegriff. Listing X.19 Like1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { if(!IsPostBack) ExecuteDataBinding(null); } void ExecuteDataBinding(string searchTerm) { OleDbConnection conn = new OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;" + @"Data Source=c:\inetpub\wwwroot\aspnet\books.mdb"); conn.Open(); OleDbCommand cmd = new OleDbCommand(); cmd.Connection = conn; string sql = "SELECT * FROM AUTHORS"; if((searchTerm != null) && (searchTerm != string.Empty)) { sql += " WHERE Firstname LIKE '%'+@SearchTerm+'%' OR Lastname LIKE '%'+@SearchTerm+'%'"; cmd.Parameters.Add("@SearchTerm", searchTerm); } cmd.CommandText = sql; OleDbDataAdapter adapter = new OleDbDataAdapter(cmd); DataSet dataset = new DataSet(); adapter.Fill(dataset, "Authors"); conn.Close(); dg.DataSource = dataset; DataBind(); }
X ASP.NET mit C# Kochbuch – Update _____________________________________ 37
void bt_click(object sender, EventArgs e) { ExecuteDataBinding(tb.Text); } Suchbegriff:
Entscheidend für das Funktionieren des Listings ist die folgende Befehlszeile: sql += " WHERE Firstname LIKE '%'+@SearchTerm+'%' OR Lastname LIKE '%'+@SearchTerm+'%'";
Die Platzhalter – dargestellt durch das Prozentzeichen – werden zusätzlich zu dem Parameter notiert und mit diesem verknüpft. Die Abbildung zeigt deutlich, dass die so erzeugte Enthaltensuche korrekt funktioniert – hier am Beispiel der Autorentabelle.
38 _________________________ X.13 ... zwei Farbwerte der Struktur Color vergleichen?
Abbildung X.21 Es werden alle Autoren mit einem „V“ im Vor- oder Nachnamen gefunden.
X.13 ... zwei Farbwerte der Struktur Color vergleichen? Die Struktur Color aus dem Namespace System.Drawing repräsentiert einen (A)RGB-Farbwert. Mitunter ist es erforderlich, zwei gegebene Farbwerte zu vergleichen und so deren Übereinstimmung zu überprüfen. Die Struktur implementiert benutzerdefinierte zwei Vergleichsoperatoren, so dass die Überprüfung in C# ein Leichtes ist. Das folgende Listing enthält zwei DropDownList-Controls mit den englischen Namen einiger Farben. Ein Klick auf den ebenfalls enthaltenen Button ermittelt aus den beiden ausgewählten Elementen jeweils eine korrespondierende Instanz der Struktur Color. Anschließend werden beide Instanzen verglichen. Wurde die gleiche Farbe ausgewählt, muss das Ergebnis true liefern. Listing X.20 Color1.aspx <script runat="server"> void bt_click(object sender, EventArgs e) { Color color1 = Color.FromName(ddl_color1.SelectedItem.Value); Color color2 = Color.FromName(ddl_color2.SelectedItem.Value); if(color1 == color2) lb.Text = "Die Farben stimmen überein."; else lb.Text = "Die Farben stimmen nicht überein."; }
X ASP.NET mit C# Kochbuch – Update _____________________________________ 39
Farbe 1:
Farbe 2:
Abbildung X.22 Die ausgewählten Farben stimmen überein.
Wie in der Abbildung zu sehen ist, funktioniert das Beispiel unter Visual C# .NET korrekt; die beiden Farben werden mit Hilfe der benutzerdefinierten Operatoren der
40 _______________________________________ X.14 ... einen Web Handler erstellen?
Struktur vergleichen. Anders sieht es jedoch bei Visual Basic .NET aus. Schreibt man das Beispiel auf diese Sprache um, wird folgende Ausnahme geliefert: Operator '=' is not defined for types 'System.Drawing.Color' and 'System.Drawing.Color'.
Die Ursache für diesen Fehler ist in der Tatsache zu suchen, dass Visual Basic .NET in der vorliegenden Version benutzerdefinierte Operatoren nicht unterstützt. Das Problem gilt daher gleichermaßen für alle anderen Operatoren, die von Klassen speziell implementiert werden. Im konkreten Fall einer Vergleichsoperation können Sie sich mit einem Trick helfen. Implementiert man einen benutzerdefnierten Operator, zwing der Kompiler den Entwickler, auch die vn object geerbte Methode Equals zu überschreiben. Angewandt auf das gezeigte Beispiel sieht die Lösung so aus: Listing X.21 Color3.aspx <script runat="server"> Sub bt_click(sender As Object, E As EventArgs) Dim color1 As Color = Color.FromName(ddl_color1.SelectedItem.Value) Dim color2 As Color = Color.FromName(ddl_color2.SelectedItem.Value) If color1.Equals(color2) Then lb.Text = "Die Farben stimmen überein." Else lb.Text = "Die Farben stimmen nicht überein." End If End Sub
X.14 ... einen Web Handler erstellen? ASP.NET bietet die Möglichkeit, eine Client-Anfrage individuell zu behandeln. Hierbei erhält der Entwickler die vollständige Kontrolle über alle eingehenden wie ausgehenden Daten. Die Implementierung eines solchen Handler erfolgt auf Basis der Schnittsteller IHttpHandler. In aller Regel wird ein solcher Handler in Form einer Quellcode-Datei angelegt und als DLL im bin-Verzeichnis abgelegt. Um eine Zuordnung zu einem virtuellen Dateinamen zu ermöglichen muss zudem ein Eintrag in der Datei web.config hinterlegt werden.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 41
Es viel einen viel einfacheren Weg, einen Handler zu erstellen. Bis zur Beta Version 2 war dieser auch offiziell beschrieben, danach ist er jedoch aus der .NET Framework SDK-Dokumentation verschwunden. Warum, kann ich nicht abschätzen, denn die Funktionalität steht weiterhin und eingeschränkt zur Verfügung. Die Rede ist von dem so genannten Web Handler. Ein Web Handler ist eine Datei mit der Endung ashx. Diese wird wie eine reguläre ASP.NET-Seite angelegt, enthält jedoch eine Klasse, die die Schnittstelle IHttHandler unterstützen muss. Das Prinzip ist der Anlage von Web Services sehr ähnlich. Hier wie dort existiert eine spezielle Direktive, die zur Zuordnung der Klasse benötigt wird. Genau wie die Funktionalität an sich ist auch die @WebHandler-Direktive nicht beschrieben. Ich möchte ein absolut minimiertes Beispiel eines Web Handlers vorstellen. Das folgende Listing zeigt eine Klasse, die die genannte Schnittstelle implementiert. Diese sieht eine Eigenschaft IsReusable vor, die in aller Regel true zurückliefern sollte. Die eigentliche Behandlung erfolgt über die Methode ProcessRequest, der eine Instanz der Klasse HttpContext übergeben wird. Die verwendete @WebHandler- Direktive verfügt über zwei Attribute. Über Language wird wie gewohnt die Sprache angegeben und über class der Name der darunter notierten Klasse. Weitere Attribute sind mir nicht bekannt. Listing X.22 webhandler1.ashx using System; using System.Web; class HelloWorld : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.Write("Hallo Welt!"); } public bool IsReusable { get { return(true); } } }
Nachdem Sie das Listing gesehen haben, dürfen Sie einmal (nicht drei Male) raten, wie das Ergebnis der Seite im Browser aussieht. Ja, Sie hahen richtig gelesen, die Datei mit der Endung ashx können Sie direkt im Browser aufrufen und natürlich wird nun die legendäre Begrüßung ausgegeben.
42 _______________________________________ X.14 ... einen Web Handler erstellen?
Abbildung X.23 Ein einfacher Web Handler im Einsatz.
Vielleicht fragen Sie sich nun, ob man diese Funktionalität nicht auch mit einer regulären Seite realisieren kann? Natürlich kann man das, allerdings ist ein Web Handler wesentlich performanter, da der ganze Verwaltungsaufwand der Seitenlogik entfällt. Ein Web Handler bietet sich also immer dann an, wenn Sie ohnehin die Möglichkeiten der Klasse Page – die übrigens ebenfalls nicht die Schnittstelle IHttpHandler unterstützt – nicht benötigen. Ein Beispiel ist die dynamische Ausgabe von Grafiken, seien sie nun on-the-fly erstellt oder zufällig von der Festplatte ausgewählt. Das zweite Listing demonstriert die zwei Möglichkeit. Listing X.23 webhandler2.ashx using System; using System.IO; using System.Web; class HelloWorld : IHttpHandler { public void ProcessRequest(HttpContext context) { string directory = context.Server.MapPath(string.Empty); string[] files = Directory.GetFiles(directory, "*.gif"); string fileName
= files[new Random().Next(files.Length)];
context.Response.ContentType = "image/gif"; context.Response.WriteFile(fileName); } public bool IsReusable { get { return(true); } } }
X ASP.NET mit C# Kochbuch – Update _____________________________________ 43
Bei jedem Aufruf des Web Handlers wird zufällig ein Bild aus dem aktuellen Verzeichnis an den Client übertragen.
Abbildung X.24 Bei jedem Aufruf wird ein zufälliges Bild ausgegeben.
Wie reguläre Handler auch sind die Web Handler eine sehr leistungsfähige Technik, die leider allzu selten benutzt wird. Machen Sie von dieser Möglichkeit gebrauch und verzichten Sie auf den Overhead der Klasse Page wann immer Sie diesen nicht benötigen.
X.15 ... alle Validation Controls einer Seite manipulieren? Mitunter ist es gewünscht, alle Validation Controls innerhalb einer Seite anzupassen oder zu manipulieren. Mit einem kleinen Trick ist dies problemlos ohne großen Aufwand wie eine rekursive Schleife durch alle Controls möglich. Die
Klasse
Page
hält
über
die
Eigenschaft
Validartors
eine
ValidatorCollection bereit, über die sich alle auf der Seite enthaltenen
Validation Controls ansprechen lassen. Die Collection lässt sich auf Basis der Klasse BaseValidator wie gewöhnlich durchlaufen. Im Listing wird allen Validation Controls die Vordergrundfarbe Blau zugewiesen. Listing X.24 Validator1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e)
44 _______________________ X.15 ... alle Validation Controls einer Seite manipulieren?
{ foreach(BaseValidator validator in this.Validators) { validator.ForeColor = Color.Blue; } } void Button_Click(object sender, EventArgs e) { int value = int.Parse(numvalue.Text); Response.Write(string.Format("Sie haben {0} eingegeben.", value)); } Numerischer Wert:
Bitte geben Sie einen Wert an! Bitte geben Sie einen numerischen Wert an!
X ASP.NET mit C# Kochbuch – Update _____________________________________ 45
Abbildung X.25 Die Ausgabe der Validation Controls erfolgen in blau.
Auf die gleiche Art und Weise lassen sich auch die anderen Eigenschaften der Controls anpassen. Falls Sie es noch nicht selbst erraten haben, nenne ich Ihnen gerne den Hintergrund der ValidatorCollection. Diese wird von der Klasse Page genutzt, um die globale Methode zur serverseitigen Validierung einer Seite zu realisieren. Jedes Validation Control meldet sich bei der Initialisierung bei der Seite an. Die Methode Validate der Klasse Page dürfte daher intern in etwa so aufgebaut sein: public override void Validate() { _isValid = true; foreach(IValidator validator in this.Validators) { if(!validator.IsValid) { isValid = false; break; } } }
Sofern Sie Controls dynamisch zur Laufzeit erzeugen, sollten Sie die Manipulation nicht wie im Beispiel gezeigt innerhalb von Page_Load vornehmen, sondern in der überschriebenen Methode OnPreRender.
X.16 ... fehlerhafte Eingaben direkt im Control kennzeichnen? Verglichen mit classic ASP sind Validation Controls wirklich ein wahrer Segen. So umfassend und nützlich die Controls auch sind, eeine Funktionalität fehlt wirklich. Wäre es nicht praktisch, wenn zusätzlich zur Einblendung eines Hinweises auch das
46 ____________________ X.16 ... fehlerhafte Eingaben direkt im Control kennzeichnen?
fehlerhafte Eingabeelement optisch hervorgehoben würde? Naja, selbst ist der ASP.NET-Heimwerkerkönig und so lässt sich diese Funktionalität mit einfachen Mitteln nachrüsten. Intern wird zur Validierung einer Seite die Methode Page.Validate aufgerufen, die mit Hilfe der Validators-Collection alle Validation Controls der Seite abruft und deren Methode Validate aufruft. Sie können die Methode der Page-Klasse überschreiben und hier eine zusätzliche Logik zur Veränderung der Hintergrundfarbe implementieren. Folgende Schritte sind hierzu notwendig: 1. Abfrage des Naming-Containers 2. Ermittlung des zu überprüfen Controls setzen 3. Setzen der Hintergrundfarbe Das Listing enthält eine Implementierung nach diesem Schema. Sehr wichtig ist der Aufruf der Basismethode, da nur diese in der Lage ist, das interne Mitgliedsvariable zu setzen, die der Eigenschaft Page.IsValid zugrunde liegt. Listing X.25 Validator3.aspx <script runat="server"> void Button_Click(object sender, EventArgs e) { if(this.IsValid) { Response.Write(string.Format("Sie haben {0} eingegeben.", tb_value.Text)); } } public override void Validate() { base.Validate(); foreach(BaseValidator { Control container = string controlName = WebControl ctl = as WebControl); ctl.BackColor = } }
validator in this.Validators) validator.NamingContainer; validator.ControlToValidate; (container.FindControl(controlName) (validator.IsValid ? Color.White : Color.Red);
X ASP.NET mit C# Kochbuch – Update _____________________________________ 47
Obligatorische Eingabe:
Bitte geben Sie einen Wert an!
Ab sofort werden fehlerhafte Eingaben zusätzlich durch eine farbliche Hervorhebung des entpsrechenden Controls gekennzeichnet. In der Abbildung ist dies deutlich zu erkennen.
Abbildung X.26 Das Control mit der fehlerhaften Eingabe erhält einen roten Hintergrund.
Es bietet sich geradezu an, eine derartige Funktionalität in einer globalen Code Behind-Klasse zu implementieren, von der Sie die einzelnen Seiten direkt oder auch indirekt über eine zusätzliche Code Behind-Klasse leiten. ableiten. Beachten Sie bitte, dass es sich bei der hier vorgestellten Lösung um eine serverseitige Realisierung handelt, die einen Round Trip zum Server voraussetzt. Sie müssen daher die Eigenschaft EnableClientScript der eizelnen Validation Controls deaktivieren und so einen PostBack provozieren. Denken Sie in diesem Zusammenhang bitte unbedingt daran, in der serverseitigen Ereignisbehandlung (zum Beispiel des Buttons) die Eigenschaft Page.IsValid wie im Listing gezeigt abzuprüfen.
48 __________________ X.17 ... den ViewState in einer Session-Variablen abspeichern?
X.17 ... den ViewState in einer Session-Variablen abspeichern? Wenn Sie sich nicht sicher sind, ob die Überschrift dieses Rezept richtig ist, seien Sie beruhigt: sie ist es. Aktiviert ist der ViewState ist komfortable Angelegenheit, denn diverse Controls können sich ohne ein weiteres Zutun ihren Zustand merken. Viele Entwickler sehen der Verwendung des ViewStates jedoch kritisch gegenüber. Kein Wunder, denn die benötigten Daten werden serialisiert und in einem versteckten Formularfeld der Seite abgelegt. Das bedeutet im Klartext, dass ein mitunter nicht ganz kleines Datenpaket sowohl vom Server zum Client als auch beim PostBack zurück vom Client an den Server übertragen werden muss. Auf diese Weise kann der Umfang der Seite und somit die Übertragungsdauer deutlich zunehmen. Ein kleines Beispiel demonstriert das Problem. Die Seite enthält ein DataGridControl mit lediglich sechs Datensätzen, die über einen DataReader einer Datenbank entnommen wurden. Die Abbildung zeigt das Notepad mit dem Quelltext der übertragenen Seite – uff!
Abbildung X.27 Der ViewState nimmt den größten Teil der Seite ein.
Es bietet sich an, die Daten auf dem Server zu belassen und die Seite so nicht extra zu belasten. Kommt Ihnen da nicht auch sofort der Session-Scope in den Sinn? Mir schon!
X ASP.NET mit C# Kochbuch – Update _____________________________________ 49
Die als protected markierten Methode SavePageStateToPersistenceMedium und LoadPageStateFromPersistenceMedium der Klasse Page ermöglichen überschrieben die Ablage der ViewState-Daten in einem von der Seite unabhängigen Speicher, zum Beispiel dem Session-State. Das Listing enthält eine beispielhafte Implementierung. Listing X.26 ViewState2.aspx <script language="C#" runat=server> void Page_Load(object sender, EventArgs e) { if(!IsPostBack) { // Datenbindung } this.RegisterHiddenField("__EVENTTARGET", string.Empty); } protected override void SavePageStateToPersistenceMedium(object viewState) { Session[Request.Url.ToString()] = viewState; } protected override object LoadPageStateFromPersistenceMedium() { return(Session[Request.Url.ToString()]); }
Die Verwendung der beiden Method erklärt sich eigentlich von ganz alleine. Während die erste die Daten im Session-Scope abspeichert, ließt die zweite diese wieder aus. Das versteckte Formularfeld kann so komplett eingespart werden. Als Name für die Session-Variable wird die URL der aktuellen Seite verwendet, so dass auch mehrere Seiten parallel aufgerufen werden können.
50 __________________ X.17 ... den ViewState in einer Session-Variablen abspeichern?
Beachten Sie bitte, dass die beiden Methoden ohne weiteres Zutun nicht funktionieren. Als Problem hat sich erwiesen, dass die Seite den PostBack nicht erkennt. Um dies zu umgehen, habe ich den nachfolgenden Aufruf integriert, der für eine Registrierung des Formularfeldes __EVENTTARGET sorgt. Anschließend wird der PostBack korrekt erkannt und die Speicherung funktioniert. this.RegisterHiddenField("__EVENTTARGET", string.Empty);
Abbildung X.28 Ohne ViewState ist die Seite weniger als die Hälfte groß.
Man kann sich darüber streiten, ob diese Variante ideal ist. Meiner Meinung nach ist sie je nach Anwendungsfall das kleinere Übel. Bedenken Sie jedoch, dass die Daten auf diese Weise pro Seite und Benutzer im Arbeitsspeicher des Servers abgelegt werden. Auch bei diesem Mechanismus gilt: Sie sollten den ViewState vermeiden, wo immer dies möglich ist. Gerade umfangreiche Daten wie beim gezeigten DataGridControl haben im ViewState nichts verloren. Deaktivieren Sie diesen gegebenenfalls über das EnableViewState-Attribut der @Page-Direktive oder die gleichnamige Eigenschaft des entsprechenden Controls.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 51
X.18 ... den DataGrid-Pager mit Text erweitern? Das automatisch oder auch manuelle Paging ist eine sinnvolle Ergänzung des DataGrid-Controls. Zur Navigation werden dem Benutzer automatisch zwei Links zum vor- beziehungsweise zurückblättern angeboten. Alternativ können auch Seitenzahlen ausgegeben werden, die einen direkten Aufruf einer bestimmten Seite ermöglichen. Was hingegen nicht vorgesehen ist, ist die manuelle Ergänzung der Pager-Zeile um zusätzlichen Text, beispielsweise einen Hinweis wie „Seite:“ zur Erklärung. Da sich bei dem DataGrid-Control letztlich um das Table-Web Control handelt, stehen auch dessen Möglichkeiten zur Verfügung. So können Sie sich über einen kleinen Umweg in die Ereignisbehandlung des Controls einklinken und die Zeile auf diese Weise wie gewünscht passen. Das Listing zeigt, wie’s geht. Listing X.27 Pager1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { if(!IsPostBack) ExecuteDataBinding(); } void ExecuteDataBinding() { // Datenbindung } void dg_ItemCreated(object sender, DataGridItemEventArgs e) { if(e.Item.ItemType == ListItemType.Pager) e.Item.PreRender += new EventHandler(this.Pager_PreRender); } void Pager_PreRender(object sender, EventArgs e) { Literal literal = new Literal(); literal.Text = "Seite: "; DataGridItem item = (DataGridItem) sender; item.Cells[0].Controls.AddAt(0, literal); } void dg_PageIndexChanged(object sender, DataGridPageChangedEventArgs e) {
52 ______________________________ X.18 ... den DataGrid-Pager mit Text erweitern?
dg.CurrentPageIndex = e.NewPageIndex; ExecuteDataBinding(); } <PagerStyle Mode="NumericPages"/>
Die benötigte Funktionalität beginnt im ItemCreated-Ereignis des DataGridControls. Hier wird die Zeile für den Pager reservierte Zeile abgepasst und der unterliegenden DataGridItem-Instanz (abgeleitet von TableRow) eine Behandlung für das Ereignis PreRender zugewiesen. Die Zeile kann nicht direkt erweitert werden, da Sie erst im Zuge der Datenbindung gefüllt wird. Das ItemDataBoundEreignis kann nicht ebenfalls nicht verwendet werden, da dieses für die Pager-Zeile nicht ausgelöst wird. Bevor die Zeile mit dem Pager tatsächlich gerendert wird, tritt die PreRenderEreignisbehandlung in Kraft und ergänzt die zu diesem Zeitpunkt ansonsten fertige Zeile um den gewünschten.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 53
Abbildung X.29 Der Pager wurde um den erklärenden Text „Seite:“ ergänzt.
X.19 ... einen Datentyp per Reflection instanziieren? Wenn Sie mit Reflection arbeiten, sind Sie vielleicht hier da und schon einmal an die Frage gestoßen, ob, und wenn ja wie, Sie aus der Klasse Type eine Instanz des darunter liegen Datentyps erstellen können. Nun, die genannte Klasse selbst bietet hierzu keine Möglichkeit, die Klase Activator aus dem Haupt-Namespace System jedoch schon. Sie können der statischen Methode CreateInstance die Instanz der Klasse Type übergeben und erhalten eine Instanz der davon repräsentierten Klasse zurück. Listing X.28 activator1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { Type t = typeof(LiteralControl); LiteralControl literal = (LiteralControl) Activator.CreateInstance(t); literal.Text = "Hallo Welt!"; this.Controls.Add(literal); }
Im Beispiel wird mit Hilfe des Schlüsselwortes typeof eine Type-Instanz erzeugt, die die Klasse LiteralControl repräsentiert. Über die Activator-Klasse wird anschließend eine Instanz der Klasse erzeugt. Das Beispiel zeigt, dass das Objekt nun wie gewöhnlich verwendet werden kann. Die Ausgabe des zugewiesenen Textes in
54 ___________________________ X.19 ... einen Datentyp per Reflection instanziieren?
der Abbildung beweist dies. Beachten Sie bitte, dass die Methode CreateInstance die Instanz als Objekt vom Typ object liefert, sodass eine explizite Typenkonvertierung notwendig ist.
Abbildung X.30 Die Instanz des LiteralControl wurde per Reflection angelegt.
Die Methode CreateInstance liegt in diversen Überladungen vor. Im zuvor gezeigten Fall wird der Standardkonstruktor verwendet. Durch Übergabe zusätzlicher Parameter können jedoch auch überladene Konstruktoren genutzt werden. Dies sieht beispielsweise so aus: Listing X.29 activator2.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { Type t = typeof(LiteralControl); LiteralControl literal = (LiteralControl) Activator.CreateInstance(t, new object[] {"Hallo Welt"}); this.Controls.Add(literal); }
Die Parameter für den Konstruktor werden in Form eines object-Arrays übergeben. Dieses kann wie im vorliegenden Fall ganz einfach inline erzeugt werden. Fragen Sie sich, wo diese Art der dynamischen Instanziierung in der Praxis Anwendung finden könnte? Nun, denken Sie einmal an ein AddIn-System, bei dem Sie die Funktionalität durch Anfügen einer Assembly bereitstellen können. Statt einem festen Datentyp würde Sie hier vermutlich eine Schnittstelle benutzen. Beachten Sie in diesem Zusammenhang auch das Rezept „... alle Typen in einer Assembly finden, die eine Schnittstelle unterstützen?“.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 55
X.20 ... alle Typen einer Assembly finden, die eine Schnittstelle unterstützen? Bei der Entwicklung eines AddIn-Systems wie einer modularen Website ist es notwendig, eine gegebene Assembly zu durchsuchen und alle Datentypen zu ermitteln, die eine bestimmte Schnittstelle unterstützen. Zur Realisierung einer solchen Anforderung benötigen Sie selbstverständlich Reflection. Das sich anschließende Beispiel zeigt in stark vereinfachter Form die Verwendung der Klasse Assembly in diesem Zusammenhang. Darin wird zunächst eine Assembly – in diesem Fall System.Data.dll aus dem Global Assembly Cache – über die Methode Load in den Speicher geladen. Über die Methode GetTypes wird ein Array mit allen enthaltenen Datentypen abgefragt werden. Innerhalb einer Enumeration durch dieses Array werden für jeden Typ die implementierten Schnittstellen abgefragt. Auch hier wird ein Type-Array geliefert. Über reguläre ArrayOperationen kann auf die Existenz einer bestimmten Schnittstelle geprüft werden. Konkret werden im Beispiel alle Klassen herausgesucht, die die Schnittstelle IDataReader unterstützen. Die Namen der so ermittelten Klassen werden im Browser ausgegeben. Listing X.30 Assembly1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { Assembly assembly = Assembly.Load("System.Data, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); Type interfaceType = typeof(IDataReader); foreach(Type t in assembly.GetTypes()) { if(Array.IndexOf(t.GetInterfaces(), interfaceType) > -1) { Response.Write("" + t.Name + "
"); } } }
56 _______ X.20 ... alle Typen einer Assembly finden, die eine Schnittstelle unterstützen?
Abbildung X.31 Genau zwei Klassen unterstützen die Schnittstelle IDataReader.
Selbstverständlich können Sie nicht nur Assemblies verwenden, die wie hier im Global Assembly Cache (GAC) hinterlegt sind. Auch lokale Assemblies aus dem bin-Verzeichnis der Applikationen lassen sich einfach durch Angabe des Dateinamens (ohne .dll) einladen. Wie im Rezept „... einen Datentyp per Reflection instanziieren?“ erläutert, können Sie einen auf diese Weise ermittelten Datentyp direkt instanziieren. Folgende Zeile wäre dazu notwendig: IDataReader reader = (IDataReader) Activator.CreateInstance(t); /// ...
Im konkreten Fall liefert dies jedoch eine Ausnahme, da für die Klassen im Beispiel kein Standardkonstruktor implementiert ist. Das Beispiel ist wie erwähnt absolut vereinfacht. Die Möglichkeiten von Reflection sind sehr, sehr weitreichend und lassen insbesondere zur dynamischen Erweiterung von Produkten elegant einsetzen. Viele Programme, die bereits auf Basis von .NET realisiert wurden, enthalten daher eine AddIn-Schnittstelle. Beispiele hierfür sind SharpDevelop aber auch Web Matrix. Auch Web-Applikationen lassen sich auf diese Weise modular erweitern. Ein typisches Szenario würde eine allgemeine Modulschnittstelle vorsehen, die in Ihrer Web-Applikationen implementiert ist. Die einzelnen Module können in separaten Assemblies geliefert werden und müssen lediglich die Schnittstelle unetrstützen. Ein benutzerdefinierter Konfigurationszweig in der web.config könnte der Web-Applikationen mitteilen, welche Assemblies auf das Vorkommen von Modulen untersucht werden soll.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 57
X.21 ... einen externen SMTP-Server benutzen? Entgegen landläufiger Meinung kann die Klasse SmtpMail aus dem Namespace System.Web.Mail durchaus nicht nur mit dem in Windows integrierten SMTPDienst genutzt werden. Auch beliebige externe Mail-Server lassen sich problemlos verwenden. Hierzu muss die jeweilige Adresse oder des IP des Providers lediglich der statischen Eigenschaft SmtpServer zugewiesen werden. Listing X.31 SmtpMail1.aspx <script language="C#" runat=server> void Page_Load(object sender, EventArgs e) { MailMessage mail = new MailMessage(); mail.From = "[email protected]"; mail.To = "[email protected]"; mail.Subject = ":-))"; mail.Body = "Hallo Welt"; SmtpMail.SmtpServer = "mail.meinprovider.de"; SmtpMail.Send(mail); }
Beachten Sie, dass sich das Verändern der statischen Eigenschaft applikationsweit auswirkt. Sie sollten daher uim Zweifelsfall wieder manuell auf den internen IISDienst umschalten, sofern Sie diesen parallel verwenden möchten. Weisen Sie der Eigenschaft hierzu einfach null zu: SmtpMail.SmtpServer = null;
X.22 ... einen externen SMTP-Server für alle WebApplikationen benutzen? Das Rezept „... einen externen SMTP-Server benutzen?“ hat gezeigt, wie Sie einen externen SMTP-Server programmatisch zum Versenden von Emails vorgeben können. Sofern prinzipiell alle Emails des Servers über einen externen Server geleitet werden sollen, bietet sich eine alternative Konfiguration an.
58 _________ X.22 ... einen externen SMTP-Server für alle Web-Applikationen benutzen?
Abbildung X.32 Die SmartHost-Konfiguration
Sie können den integrierten SMTP-Dienst der Internet Information Services zur Umleitung der Emails an den externen Server benutzen. Anders als bei der gezeigten programmatischen Lösung wird der Server nicht direkt angesprochen, sondern die Emails weiterhin in der lokalen Queue (auf Dateiebene) abgelegt. Der lokale SMTP-Server überwacht diese Queue und leitet die aufgelaufenen Emails an den externen Server weiter. Die Konfiguration erfolgt innerhalb des IIS-SnapIns der Microsoft Management Console (MMC). Öffnen Sie aus dem Verwaltungsbereich der Systemsteuerung den Punkt „Internet-Informationsdienste“. Wählen Sie nun den Eintrag „Virtueller Standardserver für SMTP“ an und öffen Sie den Eigenschaftendialog aus dem Kontextmenü. Wechseln Sie anschließend auf den Registerreiter „Übermittlung“ und öffnen Sie den Dialog „Erweitert“ rechts unten. In diesem Dialog hinterlegten Sie wie in der Abbildung gezeigt unter „Smart Host“ die Adresse des gewünschten Servers. Sie können sowohl den Hostnamen des Servers als auch dessen IPAdresse angeben. Der SMTP-Dienst löst den Hostnamen automatisch auf. Bei IP-Adressen ist dies jedoch nicht nötig und verbraucht unnötig zusätzliche Zeit. Sofern Sie die IP-Adresse in eckigen Klammern angeben, wird Namensauflösung nicht durchgeführt. Beachten Sie bitte, dass der Dienst „Simple Mail-Transportprotokoll (SMTP)“ automatisch gestartet werden sollte, damit die Emails nicht in der lokalen Queue auf der Festplatte verschimmeln.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 59
X.23 ... einen externen SMTP-Server mit Authentifierung benutzen? Die Klasse SmtpMail aus dem Namespace System.Web.Mail erlaubt zwar die Angabe eines externen Mail-Servers, eine von Providern immer häufiger verwendete SMTP-Authentifizierung wird hingegen nicht unterstützt. Um dennoch mit dem externen Provider zusammen arbeiten zu können, müssen Sie den integrierten SMTP-Dienst der Internet Information Services verwenden. Konfigurieren Sie den Dienst zunächst wie im Rezept „... einen externen SMTPServer für alle Web-Applikationen benutzen?“ beschrieben. Haben Sie den SMTP-Server Ihres Providers als Smart Host eingetragen, können Sie im Dialog „Ausgehende Sicherheit“, den Sie über den Registerreiter „Übermittlung“ erreichen die zu verwendenden Benutzerdaten angeben. Markieren Sie dazu die Option „Standardauthentifizierung“ und geben Sie Benutzer und Passwort an. Nach dem Bestätigen des Dialogs werden fortan alle Emails authentifiziert über den eingetragenen Smart Host versendet.
Abbildung X.33 Im Dialog müssen Benutzername und Passwort eingetragen werden.
60 _________________________ X.24 ... die maximale Dateigröße für Uploads erhöhen?
X.24 ... die maximale Dateigröße für Uploads erhöhen? In Newsgroups und Foren geistert immer mal wieder die Aussage herum, dass die maximale Dateigröße für Uploads auf vier MB begrenzt ist. Indirekt ist das korrekt. Doch die Ursache ist eine minimal andere. In der globalen Konfigurationsdatei machine.config existiert im Abschnitt httpRuntime ein Attribut maxRequestLength, das mit dem Standardwert 4096 belegt ist. Dieser Wert in Kilobyte regelt die maximale Größe, die eine Anfrage an die ASP.NET-Engine umfassen darf. Dies gilt also nicht nur für Uploads, sondern beispielsweise auch die Datenübertragung bei Web Services. Es stellt sich die Frage, warum über eine Begrenzung implementiert wurde. Ganz klar aus Sicherheitsgründen, denn Attacken mit überdimensionierten HTTPAnfragen, die in den Arbeitsspeicher des Servers kopiert werden, wird zumindest auf dieser Ebene ein Riegel vorgeschoben. Sie können den Wert beliebig ändern, dabei sollten Sie jedoch die Sicherheitsaspekte berücksichtigen. Es empfiehlt sich daher, die Einstellung nicht global in der machine.config sondern auf Ebene der einzelnen Applikation oder besser speziell für ein Verzeichnis in der web.config zu ändern. Das nachfolgende Beispiel erlaubt eine maximale Anfragegröße von 10 MB. Listing X.32 web.config <system.web>
X.25 ... Forms Authentication mit einem individuellen Token verwenden? In Ermangelung einer wie auch immer gearteten Abwärtskompatibilität sind zahlreiche Unternehmen dazu übergegangen, bestehende classic ASP-Projekte mit .NET-Elementen zu erweitern. Prinzipiell problemematisch ist der Übergang der Seiten, da serverseitig keine direkte Kommunikation zwischen den beiden ASPVersionen möglicht ist. Das Problem macht sich beispielsweise auch dann bemerkbar, wenn bereits ein bestehender geschützter Bereich existiert, und dieser mit weiteren ASP.NET-Seiten
X ASP.NET mit C# Kochbuch – Update _____________________________________ 61
ausgebaut werden soll. Ganz klar ein Fall für Forms Authentication, doch soll sich der Benutzer doppelt anmelden müssen? Das wäre wenig komfortabel. Die Übertragung des Passwortes beispielsweise im Query-String ist hingegen aus Gründen der Sicherheit nicht zu empfehlen. Was bleibt ist die Übergabe eines BenutzerTokens. Dieser könnte von der classic ASP-Applikation in Form eines Datenbankeintrages angelegt werden. An die anzuzeigende ASP.NET-Seite wird der Token übergeben und dort dort von der Login-Seite mittels Forms Authentication verarbeitet. Das Beispiel zeigt die notwendige Implementierung der Login-Seite. Ein wenig trickreicht ist hier die Ermittlung des im Query-String übergebenen Tokens. Es wird dazu der als „ReturnUrl“ übergebene Wert der Login-Seite abgefragt, URLdekodiert und der Token über einen regulären Ausdruck abgefragt. Listing X.33 login.aspx <script language="C#" runat=server> void Page_Load(object sender, EventArgs e) { bool LoginOK = false; string returnUrl = HttpUtility.UrlDecode(Request.QueryString["ReturnUrl"]); Match match = Regex.Match(returnUrl, @"token=(\d+)"); if(match.Success) { int token = int.Parse(match.Groups[1].Value); // An dieser Stelle würde die // Überprüfung des Tokens erfolgen LoginOK = true; if(LoginOK) { FormsAuthentication.RedirectFromLoginPage(token.ToString(), false); } } } Login Die Anmeldung konnte nicht bestätigt werden (falsches Token).
62 ______________ X.26 ... Elemente einer DropDownList oder ListBox farbig gestalten?
Statt die Login-Seite aufzurufen, verlinken Sie einfach auf die tatsächlich anzuzeigende Seite. Diese führt einen Redirect zum Login durch, wo der Token überprüft wird und nach der Freigabe ein Redirect zurück auf die anzuzeigende Seite durchgeführt wird. Der Benutzer bekommt von dieser Aktion nichts mit. Der Aufruf der Seite könnte beispielsweise so aussehen: http://localhost/asp.net/protectedpage.aspx?token=3476612
Das Datenbank-Handling habe ich aus Platzgründen weg gelassen, da dies in aller Regel durch die bestehende Applikation bereits angeboten wird. Alternativ zu diesem Token könnten auch Benutzername sowie ein Hashcode des Passworts übergeben werden. Wie Sie einen solchen errechnen, erfahren Sie im Rezept „... einen Hashcode erstellen?“.
X.26 ... Elemente einer DropDownList oder ListBox farbig gestalten? Um einzelne Einträge in einer Liste hervorzuheben, können Sie diese mittels Cascading Style Sheets farbig formatieren. Das in HTML genutzte option-Tag erlaubt dazu die Angabe eines Style-Attributs. Der Internet Explorer unterstützt dies und stellt das Element wie gewünscht dar. Die von ASP.NET angebotenen Server Control repräsentieren ein derartiges Element einer DropDownList oder ListBox über die gemeinsam genutzte Klasse Listitem. Diese leitet sich zwar nicht von der Basis WebControl ab, bietet für derartige Zwecke jedoch eine Eigenschaft Attributes an, über die sich individuelle Attribute angeben lassen. Das Beispiel verwendet die angebotene Collection (string genommen ist es eigentlich keine) des platzierten ListBox-Controls, um alle Autoren der Beispieldatenbank, deren Vorname mit einem H beginnt, mit roter Hintergrundfarbe zu versehen. Dazu wird im Anschluss an die Datenbindung eine Schleife durch alle Elemente durchgeführt.
Listing X.34 ColoredListBox1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { if(!IsPostBack) { // Datenbindung
X ASP.NET mit C# Kochbuch – Update _____________________________________ 63
foreach(ListItem item in list.Items) { if(item.Text.StartsWith("H")) { item.Attributes.Add("Style", "background-color: red;"); } } } }
Abbildung X.34 Die Einträge werden leider nicht farbig hevor gehoben.
Die Abbildung zeigt das Ergebnis im Browser. Ganz offentsichtlich werden die gewünschten Elemente nicht farbig markiert. Was ist falsch gelaufen? Ein Blick auf den an den Browser übertragenen HTML-Quelltext löst das Rätsel: \r\n"); script.Append(string.Format(" window.setTimeout (\"location.href='{0}'\", {1});\r\n", refreshUrl, timeout)); script.Append("\r\n"); this.RegisterStartupScript("Refresh", script.ToString()); lb.Text = string.Format("Es ist genau {0:T} am {0:d}", DateTime.Now); }
Aufgerufen im Browser aktualisiert sich die Seite alle fünf Sekunden und gibt die jeweils aktuelle Uhrzeit auf dem Server aus. Statt der aktuellen Seite kann der Variablen refreshUrl auch eine beliebige andere URL zugewiesen werden.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 75
Abbildung X.37 Die Seite wird alle fünf Sekunden neu geladen.
Diese Lösung setzt zwingend voraus, dass auf dem Client JavaScript ausgeführt werden kann und diese Möglichkeit zudem auch aktiviert ist respektive die Anweisungen nicht durch ein Filterprogramm entfernt werden. Zudem ist es unbedingt erforderlich, dass ein serverseitiges Formular enthalten ist, da das Script ansonsten nicht an den Client übertragen wird.
META-Tag Eine weitere Möglichkeit, die aktuelle Seite oder ein andere Seite nach Ablauf einer Zeitspanne zu laden, bietet das META-Tag „Refresh“. Das Listing zeigt eine derartige Umsetzung. Dabei wird ein HtmlGenericControl dynamisch angelegt und einem im Layout hinterlegten PlaceHolder angehängt. Die Zeitspanne wird hier in Sekunden erwartet und daher zunächst durch 1.000 geteilt. Listing X.44 Refresh2.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { string refreshUrl = Request.Url.ToString(); int timeout = 5000; HtmlGenericControl refresh = new HtmlGenericControl("meta"); refresh.Attributes.Add("http-equiv", "refresh"); refresh.Attributes.Add("content", string.Format("{0};URL={1}", timeout/1000, refreshUrl)); metatags.Controls.Add(refresh); lb.Text = string.Format("Es ist genau {0:T} am {0:d}", DateTime.Now); }
76 ______________________________ X.30 ... nach n Sekunden eine Seite neu laden?
Im Ergebnis unterscheidet sich diese Variante nicht von der vorangegangenen, allerdings wird JavaScript hierbei nicht benötigt.
HTTP-Header Auch die dritte Variante kommt ohne clientseitiges JavaScript aus. Hier wird nun die Kopfziele „Refresh“ des Protokolls HTTP genutzt. Dieser werden ähnlich dem META-Tag die Zeitspanne in Sekunden sowie die danach aufzurufende URL übergeben. Selbstverständlich kann dies neben der aktuellen Seite auch jede beliebige andere Adresse sein. Listing X.45 Refresh3.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { string refreshUrl = Request.Url.ToString(); int timeout = 5000; Response.AppendHeader("refresh", string.Format("{0};URL={1}", timeout/1000, refreshUrl)); lb.Text = string.Format("Es ist genau {0:T} am {0:d}", DateTime.Now); }
Empfehlung Drei im Ergebnis gleichwertige Varianten stehen zur Auswahl. Dennoch fällt eine Empfehlung sehr leicht. JavaScript ist auf Grund möglicherweise mangelnder Unterstützung auf Seiten des Clients nicht zu verwenden. Das META-Tag sowie die HTTP-Kopfzeile bergen keine derartigen Schwachstellen, die Kopfzeile ist jedoch sehr kürzer notiert und daher zu empfehlen.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 77
Beachten Sie, dass bei allen drei vorgestellten Lösungen die Seite komplett neu geladen und kein PostBack durchgeführt wird. Der ViewState geht also verloren. Wenn Sie dies nicht wünschen, lesen Sie bitte das Rezept „... nach n Sekunden automatisch einen PostBack durchführen?“.
X.31 ... nach n Sekunden automatisch einen PostBack durchführen? Es ist nicht weiter schwierig, eine HTML-Seite nach einer festgelegten Zeitspannen zu aktualisieren. Das Rezept „... nach n Sekunden die aktuelle Seite neu laden?“ hat dazu einige Möglichkeiten aufgezeigt. Allen gemeinsam ist die Tatsache, dass die Seite zwar korrekt aktualisiert wird, deren Zustand jedoch verloren geht. Die Seite wird also beim Aufruf neu geladen. In vielen Fällen mag genau dieses Verhalten gewünscht sein. Enthält die Seite jedoch ein Formular und macht vom ViewState Gebrauch, so gehen alle bisherigen Eingaben verloren. Statt die Seite zu laden müsste also ein PostBack durchgeführt werden. Es stellt sich die Frage, wie ein solcher PostBack überhaupt funktioniert. Nun, schaut man sich eine ASP.NET-Seite mit einem enthaltenen serverseitigen Formular im Quelltext an, so fällt der Blick sehr schnell auf die JavaScript-Funktion __doPostBackl: <script language="javascript">
Die Methode wird dazu genutzt, den PostBack auszulösen und eventuelle Daten mitzugeben. In Verbindung mit den JavaScript-Funktion setTimeout können Sie sehr einfach zeitgesteuert einen PostBack auslösen. Sie sollten dabei allerdings die Funktion nicht fest „verdrahten“, sondern auf das Ergebnis der Methode Page. GetPostBackEventReference vertrauen. Im Listing sehen Sie die komplette Funktionalität im Einsatz. Alle fünf Sekunden wird ein PostBack ausgelöst. Der ViewState geht dabei nicht verloren. Den Beweis hierzu liefert die im ViewState gespeicherte Uhrzeit der letzten Aktualisierung:
78 ______________ X.31 ... nach n Sekunden automatisch einen PostBack durchführen?
Listing X.46 RefreshPostBack1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { string postbackFunction = this.GetPostBackEventReference(this); int timeout = 5000; StringBuilder script = new StringBuilder(); script.Append("<script language=\"javascript\">\r\n"); script.Append(string.Format(" window.setTimeout(\"{0}\", {1});\r\n", postbackFunction, timeout)); script.Append("\r\n"); this.RegisterStartupScript("AutoPostBack", script.ToString()); lb1.Text = string.Format("Es ist genau {0:T} am {0:d}", DateTime.Now); lb2.Text = string.Format("Die letzte Aktualisierung erfolgt um {0:T} am {0:d}", ViewState["lastUpdate"]); ViewState["lastUpdate"] = DateTime.Now; }
In der Abbildung ist die Uhrzeit der aktuellen und der letzten Anforderung zu erkennen. Beide Zeiten liegen genau fünf Sekunden auseinander.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 79
Abbildung X.38 Alle fünf Sekunden wird ein PostBack durchgeführt.
X.32 ... komfortabel clientseitige JavaScript-Funktionen erstellen? Es ist schon ein wenig mühsam, JavaScript-Funktionen dynamisch zusammen zu stellen und an den Browser zu übergeben. Die so erzeugten Listings sehen in aller Regel unübersichtlich aus und lassen sich daher schlecht warten. Eine einfache wie effektive Lösung bietet eine Hilfsklasse, die den Rumpf eines Scripts automatisch anlegt und ein elegantes Hinzufügen der einzelnen Befehlszeilen erlaubt. Die nachfolgend abgedruckte Klasse JavaScript ermöglicht genau dies. Listing X.47 JavaScript.cs using System; using System.Text; namespace PAL.Projects.AspNetKochbuch { public class JavaScript { private StringBuilder script = new StringBuilder(); public JavaScript() {} public JavaScript(string line) { this.AppendLine(line); } public void AppendLine(string line) { this.script.Append(line); this.script.Append("\r\n"); }
80 ________________ X.32 ... komfortabel clientseitige JavaScript-Funktionen erstellen?
public override string ToString() { StringBuilder result = new StringBuilder(); result.Append("<script language=\"javascript\">\r\n"); result.Append(this.script.ToString()); result.Append("\r\n"); return(result.ToString()); } } }
Als DLL kompiliert können Sie die Klasse verwenden, um innerhalb Ihrer ASP.NET-Seiten kleine JavaScript-Funktionen anzulegen und an den Client zu übergeben. So wird etwa bei jedem Aufruf der nachfolgenden Seite eine JavaScriptalert ausgegeben. Listing X.48 javascript1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { JavaScript js = new JavaScript(); js.AppendLine("alert('Hallo Welt');"); this.RegisterStartupScript("greeting", js.ToString()); }
Abbildung X.39 Der mittels Helferklasse erzeugte Quelltext zeigt eine Meldung an.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 81
X.33 ... Controls in einer Vorlage bei Bedarf ausblenden? Bei der Verwendung von Vorlagen in datengebundenen Controls wie Repeater, DataList und DataGrid stellt sich des öfteren die Anforderung, einzelne Elemente in Abhängigkeit von Inhalten des aktuellen Datensatzes auszublenden. Das Problem wird insbesondere bei Verwendung von Image-Controls deutlich. So ließt das nachfolgende Beispiel eine Liste von Bildern aus einer Datenbank. Listing X.49 Visible1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { // Datenbindung }
Bei vier der insgesamt fünf Datensätze wurde die Spalte Filename mit einem gültigen Wert versehen, so dass das entsprechende Bild angezeigt werden kann. Bei einem Datensatz ist die Spalte jedoch null – was passiert?
82 _______________________ X.33 ... Controls in einer Vorlage bei Bedarf ausblenden?
Abbildung X.40 Der Datensatz ohne Bild mit einem Platzhalter angezeigt.
Die Abbildung zeigt das wenig befriedigende Ergebnis. Der Eigenschaft ImageUrl des dritten Datensatzes wird der in der Datenbank enthaltene null-Wert zugewiesen. In der Folge wird das img-Tag zwar gerendert, zeigt aber kein Bild, sondern lediglich einen störenden Platzhalter an. Aus dieser Problematik ergibt sich die ursprüngliche Fragestellung. Wie also lässt sich das Image-Control bei Bedarf ausblenden? Nun, auch für die Eigenschaft Visible können Sie eine Datenbindung hinterlegen: Visible=''
Der Vergleich mit DBNull.Value reicht völlig aus, um das komplette ImageControl bei Bedarf auszublenden. Die Abbildung beweist’s.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 83
Abbildung X.41 Das nicht benötigte Image-Control wird ausgeblendet.
Je nach Einsatzgebiet ist ein Vergleich mit DBNull.Value, null oder auch string.Empty sinnvoll. Zur Übersicht kann statt des Vergleiches auch eine Negation der Equals-Methode verwendet werden: Visible=''
X.34 ... Quelltext mit dem CodeDOM kompilieren? Man mag sich über den Sinn und Zweck streiten, die Möglichkeit an sich sollte man aber schon kennen. Die Rede ist von der dynamischen Erzeugung von Quellcode über das CodeDOM. DOM steht dabei für Document Object Model. Konkret geht es darum, ein dynamisch aufgebautes Objektmodell bestehend aus Klassen und deren Mitgliedern on-the-fly zu kompilieren. Sie können das CodeDOM aber auch dazu benutzen, einen bestehenden Quelltext dynamisch zu kompilieren. Und genau davon handelt dieses Rezept. Bitte schauen Sie sich zunächst einmal das folgende Beispiel an. Es enthält ein Web-Formular mit zwei mehrzeiligen Eingabefeldern sowie einem Button samt Ereignisbehandlung. Listing X.50 CodeDOM1.aspx
84 _____________________________ X.34 ... Quelltext mit dem CodeDOM kompilieren?
<script runat="server"> void Page_Load(object sender, EventArgs e) { if(!this.IsPostBack) { string filename = Server.MapPath("test.cs"); StreamReader reader = new StreamReader(File.OpenRead(filename)); tb.Text = reader.ReadToEnd(); reader.Close(); } } void bt_click(object sender, EventArgs e) { CSharpCodeProvider provider = new CSharpCodeProvider(); ICodeCompiler compiler = provider.CreateCompiler(); CompilerParameters parameters = new CompilerParameters(); parameters.GenerateInMemory = true; parameters.ReferencedAssemblies.Add("System.Web.dll"); CompilerResults results = compiler.CompileAssemblyFromSource(parameters, tb.Text); if(results.Output.Count > 0) { tb_results.Text = "Compiler-Ausgaben:\r\n\r\n"; foreach(string o in results.Output) { tb_results.Text += (o + "\n"); } tb_results.Visible = true; } else { Assembly assembly = results.CompiledAssembly; Type type = assembly.GetType("Handler"); IHttpHandler handler = (IHttpHandler) Activator.CreateInstance(type); handler.ProcessRequest(this.Context); Response.End(); } }
X ASP.NET mit C# Kochbuch – Update _____________________________________ 85
Na, alles klar bei Ihnen? Die Frage der Fragen lautet: Was passiert hier? Nun, zunächst einmal wird eine besthende Quelltextdatei test.cs geöffnet und deren Inhalt in eines der beiden Textfelder eingelesen. Wie in der Abbildung zu sehen ist, handelt es sich dabei um einen Handler, also eine Klasse, die die Schnittstelle IHttpHandler unterstützt.
Abbildung X.42 Die TextBox nimmt den Quelltext auf und ermöglicht Änderungen.
86 _____________________________ X.34 ... Quelltext mit dem CodeDOM kompilieren?
Ein Klick auf den Button setzt eine ganze Maschinerie in Gang. Diese hat Ihren Ursprung in den Namespaces Microsoft.CSharp, System.CodeDom.Compiler und System.Reflection. Ganz konkret wird dynamisch ein C#-Kompiler instanziiert. Dieser wird mit einigen Optionen sowie dem (eventuell individuell angepassten) Quelltext aus dem Textfeld gefüttert. Das Ergebnis enthält eine im Speicher abgelegt Assembly mit dem kompilierten Ergebnis. Ein wenig Reflection hilft nun, die hoffentlich enthaltene Klasse Handler abzufragen, zu instanziieren und deren Methode ProcessRequest auszuführen. Da es sich um einen Http-Handler handelt, kann diesem die weitere Bearbeitung der Anfrage überlassen werden. Da standardmäßig eingeladene Beispiel gibt etwa den Text „Hallo Welt!“ im Browser aus.
Abbildung X.43 Der Http-Handler wurde dynamisch kompiliert und ausgeführt.
Und was passiert im Fehlerfall? Dann werden alle Ausgaben des Kompilers im Browser angezeigt. Hierbei lässt sich nun auch erkennen, wie der Kompiler intern arbeitet. Statt tatsächlich die Assembly ausschließlich im Speicher anzulegen, wird eine temporäre Quellcode-Datei erzeugt, diese mittels Aufruf von csc.exe kompiliert und das Ergebnis in den Speicher geladen. Nun ja, fumktionieren tut es ...
X ASP.NET mit C# Kochbuch – Update _____________________________________ 87
Abbildung X.44 Im Fehlerfall werden die Ausgaben des Kompilers angezeigt.
Sie dürfen dieses Beispiel in dieser Form auf gar keinen Fall auf Ihrem Produktions-Server ablegen. Das Beispiel öffnet Hackern mehr als Tür und Tor und dient ausschließlich einer Demonstration der technischen Möglichkeiten. Gehen Sie bitte absolut vorsichtig mit diesem Listing um! Auf der begleitenden Buch-CD-ROM finden Sie übrigens auch ein analoges Beispiel für Visual Basic .NET. Die entsprechenden Klassen finden sich hier im Namespace Microsoft.VisualBasic. Sie können übrigens noch jede Menge weitere schiecke Sachen mit dem CodeDOM realisieren. Dieses Rezept ist nur ein Beispiel von vielen und berührt die eigentliche Idee des objektorientierten Anlagens von Quelltext nur am Rande.
X.35 ... alle Controls eines Typs auf einer Seite modifizieren? Ausgesprochen häufig wird der Wunsch geäußert, alle Instanzen eines bestimmten Control-Typs, die sich auf einer Seite befinden, einheitlich zu verändern. Das Ziel sind dabei in aller Regel Eingabefelder, die einem bestimmten Schmema folgen sollen. Mal sollen diese allesamt deaktiviert werden und manchmal auch nur die Hintergrundfarbe angepasst werden. Eigentlich kein Problem, sollte man meinen, denn eine einfache foreach-Schleife durch die Controls-Collection der Seite gepaart mit einem Typenvergleich mittels as sollte zum gewünschten Ziel führen.
88 ____________________ X.35 ... alle Controls eines Typs auf einer Seite modifizieren?
Das Listing enthält eine derartige Umsetzung. Gezeigt werden mehrere Eingabeelemente, wobei eines fest im Formular hinterlegt ist und weitere über ein Repeater-Control dynamisch der Seite angefügt werden. Eine foreach-Schleife nach dem beschriebenen Schema soll Hinter- und Vordergrundfarbe aller TextBoxControls setzen. Listing X.51 ModifyControls1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { string[] dataSource = new string[20]; for(int i = 0; i < dataSource.Length; i++) { dataSource[i] = i.ToString(); } rp.DataSource = dataSource; rp.DataBind(); foreach(Control childControl in this.Controls) { if(childControl is TextBox) { TextBox tb = (TextBox) childControl; tb.BackColor = Color.Red; tb.ForeColor = Color.White; } } } TextBox außerhalb:
TextBox innerhalb:
X ASP.NET mit C# Kochbuch – Update _____________________________________ 89
Dieses Buch ist zwar schwarz/weiß, die Abbildung lässt aber dennoch den korrekten Schluss zu, dass die Routine keinerlei Auswirkungen auf die Einagbefelder der Seite hat.
Abbildung X.45 Die Farben der Eingabefelder wurden nicht verändert.
Im Trace-Modus lässt sich die Ursache des Probleme recht schnell erkunden, wenn man denn einen Blick auf den Control-Tree wirft. Hier wird deutlich, dass die Eingabefelder nicht direkt innerhalb der Controls-Collection der Seite abgelegt sind, sondern vielmehr in untergeordneten Objekten wie dem serverseitigen Formular und dem Repeater-Control.
Abbildung X.46 Der Control-Tree zeigt die Hierarchie der Objekte.
90 ____________________ X.35 ... alle Controls eines Typs auf einer Seite modifizieren?
Variante I – Rekursion Als Lösung bietet sich eine rekursive Schleife durch die Controls an. Dieses ist zwar insbesondere bei umfangreichen Seiten nicht unbedingt hoch-performant, führt aber auf jeden Fall zum gewünschten Ergebnis – alle Eingabefelder kommen nun in neuer Farbenpracht daher. Listing X.52 ModifyControls3.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { string[] dataSource = new string[20]; for(int i = 0; i < dataSource.Length; i++) { dataSource[i] = i.ToString(); } rp.DataSource = dataSource; rp.DataBind(); ModifyTextBoxRecursive(this); } void ModifyTextBoxRecursive(Control parentControl) { foreach(Control childControl in parentControl.Controls) { if(childControl is TextBox) { TextBox tb = (TextBox) childControl; tb.BackColor = Color.Red; tb.ForeColor = Color.White; } ModifyTextBoxRecursive(childControl); } } ...
X ASP.NET mit C# Kochbuch – Update _____________________________________ 91
Abbildung X.47 Dank Rekursion werden alle TextBox-Controls modifiziert.
Selbstverständlich können Sie auf diese Weise auch mit anderen Controls arbeiten. Dabei ist es auch denkbar, gewisse Control-Gruppen über die jeweils angebotenen Basisklassen zu manipulieren. So können Sie durch einen Typenvergleich gegen WebControl die standardisierten Eigenschaften aller Web Controls setzen.
Variante II – Erweiterte TextBox Wie schon erwähnt ist dieser Ansatz zwar absolut vertretbar, im Hinblick auf die Performance aber zumindest in der Theorie nicht optimal. Hier würde sich ein System analog zu den Validation Controls anbieten. Diese registrieren sich intern in der Validators-Collection der enthaltenen Seite und erlauben dadurch einen zentralen Zugriff ohne Rekursion. Auch ein Beispiel für diesen Ansatz möchte ich Ihnen nicht vorenthalten, wenn gleich dieser doch etwas mehr Aufwand erfordert. Sie benötigen zunächst eine von TextBox abgeleitete Klasse, die die Methoden OnInit und OnUnload überschreibt und sich hier in der zentralen TextBox-Collection registriert. Natürlich wird dazu noch eine entsprechende Collection sowie eine von Page abgeleitete Seitenvorlage benötigt. Das Listing zeigt die neuen Klassen. Listing X.53 TextBoxCollection.cs using using using using using
System; System.Collections; System.Web; System.Web.UI; System.Web.UI.WebControls;
92 ____________________ X.35 ... alle Controls eines Typs auf einer Seite modifizieren?
namespace PAL.Projects.AspNetKochbuch { public class TextBoxExt : TextBox { protected override void OnInit(EventArgs e) { TextBoxCollectionPage page = (this.Page as TextBoxCollectionPage); if(page != null) { page.TextBoxControls.Add(this); } base.OnInit(e); } protected override void OnUnload(EventArgs e) { TextBoxCollectionPage page = (this.Page as TextBoxCollectionPage); if(page != null) { page.TextBoxControls.Remove(this); } base.OnUnload(e); } } public class TextBoxCollection : CollectionBase { public void Add(TextBox textBox) { this.InnerList.Add(textBox); } public void Remove(TextBox textBox) { this.InnerList.Remove(textBox); } public TextBox this[int index] { get { return((TextBox) this.InnerList[index]); } } } public class TextBoxCollectionPage : Page { private TextBoxCollection textBoxControls = new TextBoxCollection(); public TextBoxCollection TextBoxControls {
X ASP.NET mit C# Kochbuch – Update _____________________________________ 93
get { return(this.textBoxControls); } } } }
Als DLL kompiliert können die neuen Klassen in einer ASP.NET-Seite verwendet werden. Diese muss sich von TextBoxCollectionPage ableiten und statt der regulären TextBox-Controls die erweiterte Klasse nutzen. Anschließend reicht eine einfache Schleife durch die TextBoxControls-Collection, um alle Eingabefelder zu modifizieren. Listing X.54 ModifyControls4.aspx ... foreach(TextBox tb in this.TextBoxControls) { tb.BackColor = Color.Red; tb.ForeColor = Color.White; } ...
X.36 ... Daten per HTTP-POST senden und empfangen? Über das Protokoll HTTP lassen sich bekanntlich verschiendenste Arten von Daten übertragen. Das World Wide Web benutzt das Protokoll für HTML-Seiten, XML Web Services senden mit SOAP garniertes XML über den Äther. Ab und an steht man selbst vor Aufgabe, individuelle Daten zwischen zwei Endpunkten im Internet zu übertragen. Auch hier lässt sich das Protokoll problemlos einsetzen – mittels der Klassen aus dem Namespace System.Net ein Kinderspiel. Dieses Rezept zeigt die Übertragung einer Bilddatei zwischen einem Client und einem Server. Beide sind als Web Handler realisiert und liegen „zufällig“ auf dem selben Rechner. Natürlich würde das System ohne Einschränkungen auch über Kontinente hinweg funktionieren.
Client Der Client verwendet die Klassen WebRequest sowie die davon abgeleitete HttpWebRequest, um eine Anforderung an den Server zu senden. Als HTTP-
Methode wird „POST“ angegeben. Über die Methode GetRequestStream wird anschließend ein Stream abgefragt, in den die Daten der Anforderung geschrieben
94 _______________________ X.36 ... Daten per HTTP-POST senden und empfangen?
werden können. Dies übernimmt die Methode StreamCopy.Copy aus dem Rezept „... einen Stream kopieren?“. Kopiert wird eine zuvor angelegte Instanz der Klasse FileStream mit dem zu übertragenden Bild. Im Anschluss wird die Anforderung abgesendet und das Ergebnis über einen StreamReader im Browser-Fenster ausgegeben. Listing X.55 posthandler_client1.ashx using using using using using
System; System.IO; System.Web; System.Net; PAL.Projects.AspNetKochbuch;
class PostHandlerClient1 : BaseHttpHandler { protected override void Process() { string url = "http://localhost/asp.net/posthandler_server1.ashx"; HttpWebRequest request = (HttpWebRequest) WebRequest.Create(url); request.Method = "POST"; string fileName = Server.MapPath("vivendi-universal.gif"); Stream requestStream = request.GetRequestStream(); FileStream fileStream = File.OpenRead(fileName); StreamCopy streamCopy = new StreamCopy(); streamCopy.Copy(fileStream, requestStream); fileStream.Close(); requestStream.Close(); HttpWebResponse response = (HttpWebResponse) request.GetResponse(); StreamReader reader = new StreamReader(response.GetResponseStream()); Response.Write(reader.ReadToEnd()); reader.Close(); } }
Elementar wichtig ist in diesem Zusammenhang das explizite Schließen des Request-Streams. Ansonsten wird die Größe der Anforderung (ContentLength) nicht gesetzt und der Server wartet „ewig“ auf die vollständige Übertragung. In der Folge wird eine WebException (WebExceptionStatus.Timeout) geworfen.
X ASP.NET mit C# Kochbuch – Update _____________________________________ 95
Abbildung X.48 Der Client gibt die Rückmeldung des Servers aus.
Server Natürlich benötigen Sie für die Übertragung auch eine Gegenstelle, die die Anforderung samt Dateninhalt entgegen nimmt und verarbeitet. Der Server ist in diesem Beispiel ebenfalls als Web Handler realisiert. Die Klasse legt eine neue Daten mit einem zufälligen Namen (GUID) an. In diese wird erneut über die Methode StreamCopy.Copy der InputStream der RequestKlasse kopiert. Als Rückmeldung an den Client wird ein kurzer Bestätigungstext ausgegeben. Dieser wird vom Client an den Benutzer weiter geleitet und ist in der Abbildung zu erkennen. Listing X.56 posthandler_server1.ashx using using using using
System; System.IO; System.Web; PAL.Projects.AspNetKochbuch;
class PostHandlerServer1 : BaseHttpHandler { protected override void Process() { string fileName = Server.MapPath(Path.ChangeExtension( Guid.NewGuid().ToString(), "gif")); FileStream fileStream = File.Create(fileName); StreamCopy streamCopy = new StreamCopy(); streamCopy.Copy(Request.InputStream, fileStream); fileStream.Close(); Response.Write("Die Datei wurde an den Server übertragen ..."); }
96 ______ X.37 ... einen Standard-Button für ein editierbares DataGrid-Control defnieren?
}
Alles klar? Das Beispiel mag auf den ersten Blick etwas kompliziert aussehen. Probiert man es jedoch einmal aus, wird der Ablauf sehr schnell deutlich. In diesem Zusammenhang sei noch einmal daraufhin gewiesen, dass es sich um ein konstruiertes Beispiel handelt. Im Realfall werden Sie vermutlich nur eine der beiden Seiten realisieren müssen. Beim Client ist insbesondere auf das erwähnte Schließen des AnforderungStreams zu achten.
X.37 ... einen Standard-Button für ein editierbares DataGrid-Control defnieren? Das Rezept „... einen Standard-Button defnieren?“ hat eine Möglichkeit vorgestellt, einen Standard-Button zu definieren, so dass ein Web-Formular mittels dem gewohnten Druck auf die Enter-Taste bestätigt werden kann. Ein vergleichbares Verhalten wäre auch für editierbare DataGrid-Controls ausgesprochen nützlich. Die Vorgehensweise muss hier jedoch deutlich aussehen, da die zu verwendenden Buttons dynamisch erzeugt werden.
PushButton Sofern Sie ein editbares DataGrid in Verbindung mit einer Spalte vom Typ EditCommandColumn und der Einstellung PushButton verwenden, werden reguläre Button-Objekte im Browser ausgegeben. Im DataGrid-Ereignis ItemCreated können Sie deren ID festlegen, anschließend abfragen und in das versteckte Feld __EVENTTARGET kopieren. Nun wird der gewünschte Button (zumeist „Übernehmen“ oder „Speichern“) mittels Tastendruck auf Enter ausgelöst. Listing X.57 DataGridEdit_DefaultButton1.aspx ... void dg_ItemCreated(object sender, DataGridItemEventArgs e) { if(e.Item.ItemType == ListItemType.EditItem) { Button bt = (Button) e.Item.Cells[2].Controls[0]; bt.ID = "ApplyButton";
X ASP.NET mit C# Kochbuch – Update _____________________________________ 97
this.RegisterHiddenField("__EVENTTARGET", bt.ID); } }
98 ______ X.37 ... einen Standard-Button für ein editierbares DataGrid-Control defnieren?
Abbildung X.49 Ein Tastendruck reicht zum Übernehmen der Änderungen aus.
LinkButton Sofern Sie statt den klobigen Button-Objekten lieber reguläre Links verwenden möchten, müssen einen erneut etwas modifizierten Ansatz wählen. In diesem Fall muss das Absenden des Formulars durch die Eingabefelder selbst ausgelöst werden. Dazu wird eine kleine JavaScript-Behandlung des clientseitigen Ereignisses onkeydown benötigt. Dieses löst den PostBack aus und übergibt dabei die eindeutige ID des gewünschten LinkButton-Controls. Da diese ID erst nach der Datenbindung zur Verfügung steht, wird diesmal das Ereignis ItemDataBound genutzt. Listing X.58 DataGridEdit_DefaultButton2.aspx ... void dg_ItemDataBound(object sender, DataGridItemEventArgs e) { if(e.Item.ItemType == ListItemType.EditItem) { LinkButton bt = (LinkButton) e.Item.Cells[2].Controls[0]; string script = string.Format("if(window.event.keyCode == 13) __doPostBack('{0}','')", bt.UniqueID); ((TextBox) e.Item.Cells[0].Controls[0]). Attributes. Add("onkeydown", script); ((TextBox) e.Item.Cells[1].Controls[0]). Attributes.Add("onkeydown", script);
X ASP.NET mit C# Kochbuch – Update _____________________________________ 99
} } ...
Abbildung X.50 Auch bei Verwendung der LinkButtons ist ein Standard-Button definierbar.
X.38 ... eine Kreditkartennummer syntaktisch überprüfen? Kreditkarten werden im Internet immer wichtiger. Kaum ein Shop kann es sich leisten, die Plastikkarten nicht als Zahlungsmittel zu akzeptieren. Die Vorteile liegen auf beiden Seiten, denn mit einem geeigneten Payment-Gateway lassen sich online Beträge reservieren und buchen, was eine recht hohe Transaktionssicherheit für den Händler bedeutet. Doch bevor, eine Zahlung an das Gateway weitergegeben wird, sollte bereits innerhalb der eigenen Web-Applikation eine rudimentäre Prüfung der Daten vorgenommen werden. Wenngleich es (vielleicht zum Glück?) nicht allgemein bekannt ist, verfügen Kreditkartennummern über eine Prüfziffer. Diese lässt sich nach einem relativ einfachen Algorithmus mit dem Namen Luhn berechnen. Alle gängigen Kartentypen wie VISA, MasterCard/EuroCard, American Express aber auch heute nicht mehr so verbreitete Typen wie Diners Club lassen sich nach dem Schema syntaktisch überprüfen. Es bietet sich an, eine derartige Aufgabenstellung in Form eines Validation Controls vorzunehmen. Nachfolgend finden Sie eine entsprechende Implementie-
100 ______________________ X.38 ... eine Kreditkartennummer syntaktisch überprüfen?
rung. Ich möchte dabei anmerken, dass der vorgestellte Luhn-Algorithmus nicht meiner Feder entstammt, sondern vielmehr einer freien Quelle aus dem Internet. Vielen Dank an dieser Stelle dem mir unbekannten Autoren. Listing X.59 CreditCardNumberValidator.cs using using using using
System; System.Web; System.Web.UI; System.Web.UI.WebControls;
namespace PAL.Projects.AspNetKochbuch { public class CreditCardNumberValidator : BaseValidator { public CreditCardNumberValidator() { this.EnableClientScript = false; } protected override bool EvaluateIsValid() { string number = GetControlValidationValue(this.ControlToValidate); if((number != null) && (number != string.Empty)) { return(this.IsCreditCardNumberValid(number)); } else { return(true); } } protected bool IsCreditCardNumberValid(string number) { bool result = false; int[] int[] int[] int[] int k
intNumber = new int[16]; arr = new int[16]; arr1 = new int[16]; sum = new int[16]; =0;
for(int i=0;i void Page_Load(object sender, EventArgs e) { iframe.NavigateUrl = "http://localhost/asp.net/"; } Inline Frame
Abbildung X.56 Das InlineFrame-Control zeigt kann dynamisch andere Seiten anzeigen.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 121
Kaum erwähnenswert ist die Möglichkeit, das Control gar komplett dynamisch zu instanziieren und zu platzieren. Das zweite Listing zeigt diesen Ansatz, der im Ergebnis dem vorherigen entspricht. Listing X.67 inlineframe2.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { InlineFrame iframe = new InlineFrame(); iframe.NavigateUrl = "http://localhost/asp.net/"; iframe.Width = Unit.Percentage(100); iframe.Height = Unit.Pixel(200); this.Controls.Add(iframe); } Inline Frame
X.44 ... eine globale Seitenvorlage mit Visual Studio .NET erstellen? Nachdem das Rezept „... eine globale Seitenvorlage erstellen?“ gezeigt hat, wie Sie Web-Applikationen ohne Visual Studio .NET ein einheitliches Design verpassen können, zeigt Ihnen dieses Rezept eine angepasste Version für die Entwicklungsumgebung. Das Projekt besteht aus einer einzigen ASP.NET-Seite, die im Zuge der Projektanlage automatisch von der Entwicklungsumgebung erzeugt wurde. Diese und weiteren Seiten müssen zur Verwendung der Vorlage nur minimal angepasst werden. Zunächste müssen Sie die Klasse statt direkt von Page von der zwischen geschalteten Basis TemplatedPage ableiten: public class WebForm1 : Templates.TemplatedPage { ...
Optional haben Sie nun die Möglichkeit, einen Titel für die Seite zu vergeben. Dieser Titel wird einerseits für das HTML-Tag title und andererseits innerhalb eines
122 _____________ X.44 ... eine globale Seitenvorlage mit Visual Studio .NET erstellen?
h1-Überschriftenelements genutzt. Die Zuweisung erfolgt typischerweise innerhalb der Ereignisbehandlung Page_Load: private void Page_Load(object sender, System.EventArgs e) { this.Template.Title = "Individuelle Überschrift"; }
Im letzten Schritt müssen Sie in die HTML-Ansicht der Seite wechseln und hier jegliche HTML-Tags entfernen. Diese werden später automatisch aus der Vorlage geladen. Diese wenigen Anpasungen reichen aus, damit die modifizierte Seite ab sofort auf Basis des globalen Layouts im Browser ausgegeben wird. Die Vorlage selbst befindet sich im Unterverzeichnis Templates in Form des Users Controls PageTemplate.ascx. Das Control und auch die zentrale Basisklasse TemplatedPage agieren im Wesentlichen analog zu dem oben angesprochenen Rezept und werden daher an dieser Stelle nicht näher vorgestellt.
Abbildung X.57 Links die neue Seite und rechts die globale Vorlage
X ASP.NET mit C# Kochbuch – Update ____________________________________ 123
X.45 ... Konfigurationsdaten in der web.config ablegen? Die Ablage von einfachen Konfigurationsdaten innerhalb der Datei web.config ist ausgesprochen simpel. Das Framework stellt einen speziellen Abschnitt appSettings zur Verfügung, in dem sich vergleichbar mit einem Dictionary Daten in einer Schlüssel-Wert-Kombination ablegen lassen. Eine entsprechende Konfiguration sieht beispielsweise so aus: Listing X.68 web.config
Der Zugriff auf die Daten erfolgt über eine NameValueCollection, die von statischen Eigenschaft ConfigurationSettings.AppSettings geliefert wird. Das Listing zeigt die Abfrage des hinterlegten Konfigurationswertes. Das Ergebnis im Browser lässt sich recht einfach ausmalen, ich enthalte es Ihnen dennoch nicht vor. Listing X.69 configTest1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { NameValueCollection appSettings = ConfigurationSettings.AppSettings; lt.Text = appSettings["test"]; }
Abbildung X.58 Die Begrüßung wurde der web.config entnommen.
124 _______________________ X.45 ... Konfigurationsdaten in der web.config ablegen?
Zugriff auf nicht-Zeichenketten Die Verwendung des appSettings-Abschnitts ist eigentlich schon ein alter Hut. Weniger bekannt ist jedoch, dass auch ein typensicherer Zugriff auf die enthaltenen Daten möglich ist. So können Sie beispielsweise numerische Werte ablegen und typensicher abfragen. Möglich macht dies die Klasse AppSettingsReader. Nach der parameterlosen Instanziierung der Klasse wird der einzigen Methode GetValue der Name des abzufragenden Wertes sowie der gewünschte Datentyp übergeben. Postwendend erhalten Sie die entsprechend aufbereiteten Daten zurück. Listing X.70 configTest2.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { AppSettingsReader reader = new AppSettingsReader(); int numericValue = (int) reader.GetValue("numericValue", typeof(int)); lt.Text = string.Format("{0:n}", numericValue); }
Strukturierter Zugriff auf Konfigurationsdaten Der Konfigurationsabschnitt appSettings ist optimal geeignet für „quick & dirty“Lösungen, bei denen kurzerhand ein Wert aus dem Quelltext heraus in die leicht änderbare web.config übernommen werden soll. Für komplexe Daten ist das Schlüssel-Wert-Prinzip allerdings weniger tauglich. Aber auch für den strukturieren Zugriff auf komplexe Konfigurationen lässt sich die Datei web.config ohne weiteres nutzen. Das System basiert auf der Schnitstelle IConfigurationSectionHandler. Diese sieht ein einzige Methode Create vor, der im Wesenlichen eine XmlNode-Instanz übergeben wird, die die Daten des zugewiesenen Abschnitts in der Konfigurationsdatei enthält. Aufbau der Methode ist es nun, den XML-Abschnitt in eine objektorientierte Form zu bringen, was immer das im konkreten Anwendungsfall auch bedeutet. Die so erzeugte Klasse wird von der Methode an den Aufufer zurückgeliefert. Ein Beispiel …
X ASP.NET mit C# Kochbuch – Update ____________________________________ 125
Listing X.71 ConfigurationSectionHandler1.cs using using using using
System.Collections; System.Collections.Specialized; System.Configuration; System.Xml;
namespace Pal.AspNetKochbuch.Configuration { public class ConfigurationSectionHandler1 : IConfigurationSectionHandler { public object Create(object parent, object configContext, XmlNode section) { HybridDictionary dictionary = new HybridDictionary(); if(parent != null) { foreach(DictionaryEntry de in (IDictionary) parent) { dictionary.Add(de.Key, de.Value); } } foreach(XmlNode child in section.ChildNodes) { switch(child.Name.ToLower()) { case "add" : dictionary.Add(child.Attributes["name"].Value, child.Attributes["value"].Value); break; case "remove" : dictionary.Remove(child.Attributes["name"].Value); break; case "clear" : dictionary.Clear(); break; default : throw(new ConfigurationException("Unknown configuration node " + child.Name)); } } return(dictionary); } }
126 _______________________ X.45 ... Konfigurationsdaten in der web.config ablegen?
}
Was passiert im Listing? Naja, eigentlich so ziemlich das gleiche wie bei den zuvor vorgestellten appSettings. Innerhalb des zugewiesenen Konfigurationsabschnitts können Tags-Elemente mit dem Namen add, remove und clear notiert werden. Diese werden jeweils auf ein zuvor angelegtes HybridDictionary angewendet. Auf diese Weise lassen sich mittels Konfigurationsdatei Werte zu dem Dictionary hinzufügen und wieder entfernen. Die nachfolgende web.config zeigt den Einsatz. Listing X.72 web.config <section name="testSettings" type="Pal.AspNetKochbuch.Configuration. ConfigurationSectionHandler1, ConfigurationSectionHandler1" />
Innerhalb der selben web.config geschieht übrigens auch die Zuordnung zwischen Abschnitt und Handler. Dabei muss der vollständige Namespace-Pfad der Klasse sowie mit einem Komma getrennt die Assembly angegeben werden. Der Zugriff auf die hinterlegten Daten erfolgt über die statische Methode ConfigurationSettings.GetConfig, die eine Instanz vom Typ object liefert.
Hierbei handelt es sich selbstverständlich um die im oben gezeigten Handler angelegte Klasse, so dass eine Typenumwandlung zu IDictionary möglich ist. Listing X.73 configTest3.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { IDictionary dictionary = (IDictionary) ConfigurationSettings.GetConfig("testSettings"); foreach(DictionaryEntry de in dictionary) { lt.Text += string.Format("{0} -> {1}
", de.Key, de.Value); }
X ASP.NET mit C# Kochbuch – Update ____________________________________ 127
}
Die Ausgaben des Beispiels enthalten lediglich einen Konfigurationswert. Der Grund hierfür liegt in der notierten remove-Anweisung, die den zweiten Eintrag gleich wieder aus dem Dictionary gelöscht hat.
Abbildung X.59 Die Daten wurden über den ConfigurationSectionHandler ausgelesen.
Richtig komplexe Daten Das vorherige Beispiel war nur im Grunde nur eine etwas bessere Variante des ohnehin verfügbaren appSettings-Abschnitts. Das nächste Beispiel zeigt, dass sich auch richtig komplexe Daten in der Konfigurationsdatei hinterlegen und als Objekte ansprechen lassen. Abgespeichert werden dabei Benutzerdaten in der folgenden Form. Listing X.74 web.config <section name="persons" type="Pal.AspNetKochbuch.Configuration. PersonsConfigurationSectionHandler, PersonsConfigurationSectionHandler" />
128 _______________________ X.45 ... Konfigurationsdaten in der web.config ablegen?
Die hinterlegten Einträge sollten in einer Klasse Person mit drei korrespondierenden Eigenschaften abgelegt werden. Diese Instanzen werden in einer PersonsCollection gesammelt. public class Person { private string firstname; private string lastname; private DateTime birthday; public string Firstname { get { return(this.firstname); } set { this.firstname = value; } } public string Lastname { get { return(this.lastname); } set { this.lastname = value; } } public DateTime Birthday { get { return(this.birthday); } set { this.birthday = value; } } } public class PersonCollection : CollectionBase { public void Add(Person person) { this.InnerList.Add(person); } public void Remove(Person person) { this.InnerList.Remove(person); }
X ASP.NET mit C# Kochbuch – Update ____________________________________ 129
public Person this[int index] { get { return((Person) this.InnerList[index]); } } }
Mit diesen Vorarbeiten ist es nun sehr einfach, die Daten aus der Datei in das Objektmodell zu überführen. Mittels SelectNodes werden alle person-Tags abgefragt und anschließend durchlaufen und in Form einer Person-Instanz in der Collection abgelegt. Listing X.75 PersonsConfigurationSectionHandler.cs using using using using using
System; System.Collections; System.Collections.Specialized; System.Configuration; System.Xml;
namespace Pal.AspNetKochbuch.Configuration { public class PersonsConfigurationSectionHandler : IConfigurationSectionHandler { public object Create(object parent, object configContext, XmlNode section) { PersonCollection persons = new PersonCollection(); if(parent != null) { foreach(Person person in (PersonCollection) parent) { persons.Add(person); } } foreach(XmlNode child in section.SelectNodes("person")) { Person p = new Person(); p.Firstname = child.Attributes["firstname"].Value; p.Lastname = child.Attributes["lastname"].Value; p.Birthday = DateTime.Parse(child.Attributes["birthday"].Value); persons.Add(p); } return(persons); } }
130 _______________________ X.45 ... Konfigurationsdaten in der web.config ablegen?
}
Ein kleines Beispiel zaubert die Daten in das Browserfenster. Die zurückgelieferte Collection wird dabei als Datenquelle für ein DataGrid genutzt. Dieses zieht sich mittels Reflection automatisch die Spalteninformationen und stellt die Personen tabellarisch dar. Listing X.76 configTest4.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { PersonCollection persons = (PersonCollection ) ConfigurationSettings.GetConfig("persons"); dg.DataSource = persons; dg.DataBind(); }
Abbildung X.60 Die Personen werden als Datenquelle für ein DataGrid-Control genutzt.
Vererbung Haben Sie sich gefragt, was ich in den vorherigen Listings mit dem parentParameter der Methode Create angestellt habe? Dessen Inhalt wurde ausgewertet und dem neu erstellten Objekt angefügt. Warum das? Nun, bei ASP.NET haben Sie
X ASP.NET mit C# Kochbuch – Update ____________________________________ 131
die Möglichkeit, Konfigurationseinstellungen zu vererben. Die Datei web.config ist ein gutes dafür, denn diese erbt die globalen Einstellungen der machine.config und verändert diese individuell für die einzelne Web-Applikationen. Bei vielen Optionen ist zudem möglich, diese über zusätzliche web.config-Dateien in Unterverzeichnissen speziell für diesen Ordner zu manipulieren. Das gleiche Prinzip wird auch bei den beiden vorgestellten Klassen verfolgt. Der Parameter parent liefert dabei beispielsweise die PersonsCollection der übergeordneten Konfiguration. Um dieses Vorgehen nachvollziehen zu können, legen Sie bitte die folgende Konfigurationsdatei in einem Unterverzeichnis ab. Listing X.77 web.config
Rufen Sie anschließend das unveränderte Beispiel configTest4.aspx aus genau diesem Verzeichnis auf. In der Abbildung ist deutlich erkennbar, dass neben der lokal hinterlegten Person auch die beiden aus der übergeordneten Konfiguration übernommen wurden. Die Daten wurden geerbt.
Abbildung X.61 Die Konfiguration wurde geerbt.
X.46 ... eine allgemeine Basisklasse für Konfigurationsdaten erstellen? Im vorherigen Rezept haben Sie gelesen, wie Sie Konfigurationsdaten in der Datei web.config hinterlegen und diese über individuelles Objektmodell abfragen kön-
132 ___________ X.46 ... eine allgemeine Basisklasse für Konfigurationsdaten erstellen?
nen. Diese Möglichkeit ist durchaus sinnvoll, doch vielleicht geht es Ihnen wie mir und Sie halten es für etwas umständlich, für jede einzelne Konfiguration einen neuen Handler entwickeln und hier alle ewaigen Änderungen nachziehen zu müssen. Um auf einfache Weise Abschnitte in der web.config eigenen Klassen zuweisen zu können, habe ich eine abstrakte Basisklasse entwickelt, die mittels Reflection die XML-Attribute eines Konfigurationsabschnitts den gleichnamigen Eigenschaften und Feldern einer individuellen Klasse zuweist. Sie können diese Klasse dadurch mit sehr wenigen Zeilen in der web.config abbilden. Die Basisklasse ist auf der beliegenden Buch-CD-ROM als Visual Studio .NETProjekt in der Sprache C# abgelegt. Sie können die Datei BaseConfigurationSectionhandler.cs allerdings ohne Einschränkungen auch mittels des kostenlosen Kommandozeilenprogramms csc.exe kompilieren. Listing X.78 BaseConfigurationSectionhandler.cs using using using using using
System; System.Collections; System.Configuration; System.Reflection; System.Xml;
namespace PAL.Configuration { public abstract class BaseConfigurationSectionHandler : IConfigurationSectionHandler { private Type settingsClassType; private object settingsClassInstance; protected Type SettingsClassType { get { return(this.settingsClassType); } set { this.settingsClassType = value; } } protected object SettingsClassInstance { get { return(this.settingsClassInstance); } } public virtual object Create(object parent, object context, XmlNode section) { // Inherited class needs to supply class type this.CreateSettingsClassType(); if(this.SettingsClassType == null) throw(new ArgumentException("No valid SettingsClassType")); if(parent != null) {
X ASP.NET mit C# Kochbuch – Update ____________________________________ 133
// Use parent object? if(parent.GetType().Equals(this.SettingsClassType)) this.settingsClassInstance = this.CreateMemberwiseClone(parent); else throw(new ArgumentException("No valid parent.")); } else { // Create new instance this.settingsClassInstance = Activator.CreateInstance(this.SettingsClassType); this.ApplyInitiellSettings(); } // Parse all attributes MemberInfo[] members = this.SettingsClassType.GetMembers(); foreach(XmlAttribute attribute in section.Attributes) { bool handled = false; foreach(MemberInfo member in members) { if(string.Compare(member.Name, attribute.Name, true) == 0) { // Found matching member if(member.MemberType == MemberTypes.Property) handled = ParseKnownProperty((PropertyInfo) member, attribute.Value); else if(member.MemberType == MemberTypes.Field) { handled = ParseKnownField((FieldInfo) member, attribute.Value); } break; } } if(!handled) { // No matching member found handled = ParseUnknownAttribute(attribute.Name, attribute.Value); if(!handled) { throw(new ConfigurationException("Unrecognized configuration attribute " + attribute.Name)); } } } // Apply closing settings this.ApplyConclusionSettings(); // Return settings return(this.settingsClassInstance); } protected abstract void CreateSettingsClassType(); protected virtual void ApplyInitiellSettings() {}
134 ___________ X.46 ... eine allgemeine Basisklasse für Konfigurationsdaten erstellen?
protected virtual void ApplyConclusionSettings() {} protected virtual bool ParseKnownProperty(PropertyInfo property, string value) { object parsedValue = ParseValue(property.PropertyType, value); if(parsedValue != null) { property.SetValue(this.SettingsClassInstance, parsedValue, null); } return(parsedValue != null); } protected virtual bool ParseKnownField(FieldInfo field, string value) { object parsedValue = ParseValue(field.FieldType, value); if(parsedValue != null) { field.SetValue(this.SettingsClassInstance, parsedValue); } return(parsedValue != null); } protected virtual object ParseValue(Type type, string value) { object parsedValue = null; if(type.Equals(typeof(string))) { // string parsedValue = value; } else if(type.Equals(typeof(bool))) { // bool IList trueValues = (IList) new string[] {"1", "-1", "true", "yes", "ja"}; parsedValue = trueValues.Contains(value.ToLower()); } ... return(parsedValue); } ... }
Vielleicht hat Sie das Listing ein wenig erschlagen, dabei es ist noch nicht einmal vollständig. Sieht man von den vielen Code-Zeilen ab, ist das Prinzip ziemlich einfach. Um eine eigene Klasse oder Struktur zur Konfiguration zu verwenden, müssen Sie wie gewohnt einen ConfigurationSectionHandler erzeugen. Statt die Schnittstelle zu implementieren leiten Sie sich jedoch von der Basisklasse BaseConfigurationSectionHandler ab. Hier überschreiben Sie die abstrakte Methode CreateSettingsClassType und weisen hier der Eigenschaft settingsC-
X ASP.NET mit C# Kochbuch – Update ____________________________________ 135
lassType den zur Konfiguration zu verwenden Datentyp zu. Alles weitere über-
nimmt die Klasse. Der Typ wird bei Bedarf mittels Activator.CreateInstance instanziiert und anschließend über Reflection mit den Daten des XML-Abschnitts gefüllt. Ein Beispiel ... Listing X.79 SettingsConfigurationSectionHandler.cs using using using using using using
System; System.Collections; System.Collections.Specialized; System.Configuration; System.Xml; PAL.Configuration;
namespace Pal.AspNetKochbuch.Configuration { public enum SimpleEnum { value1, Value2 } public class Settings { public int NumericValue; public string StringValue; public DateTime DateValue; public SimpleEnum EnumValue; public bool BoolValue; } public class SettingsConfigurationSectionHandler : BaseConfigurationSectionHandler { protected override void CreateSettingsClassType() { this.SettingsClassType = typeof(Settings); } } }
Das im Vergleich zum vorherigen sehr knapp geratene Listing enthält eine beispielhafte Enumeration sowie eine Konfigurationsklasse mit vier öffentlichen Feldern. Diese dienen zur Demonstrationen der verschieden Datentypen, die typensicher unterstützt werden. Außerdem ist eine von der abstrakten Basis abgeleitete Klasse SettingsConfigurationSectionHandler implementiert, die den Datentyp Settings der geschützten Eigenschaft SettingsClassType übergibt.
136 ___________ X.46 ... eine allgemeine Basisklasse für Konfigurationsdaten erstellen?
Im zweiten Schritt folgt die Konfigurationsdatei, die einen neuen Abschnitt settings enthält. Die Daten werden als Attribute des Abschnitts hinterlegt, wobei die Namensgebung der öffentlichen aus der Konfigurationsklasse eingehalten werden muss. Listing X.80 web.config <section name="settings" type="Pal.AspNetKochbuch.Configuration. SettingsConfigurationSectionHandler, SettingsConfigurationSectionHandler" /> <settings NumericValue="123" StringValue="Hallo Welt!" DateValue="16.10.2002 22:31:15" EnumValue="value2" BoolValue="yes" />
Im dritten und letzten Schritt können Sie nun auf die hinterlegten Daten zugreifen. Dazu fragen Sie wie gewohnt den gewünschten Abschnitt ab. Anschließend können Sie typensicher direkt auf die hinterlegten Informationen zugreifen. Zauberhand? Nein, Reflection! Listing X.81 configTest5.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { Settings settings = (Settings) ConfigurationSettings.GetConfig("settings"); Response.Write(string.Format("Zahl: {0:n}
", settings.NumericValue)); Response.Write(string.Format("Zeichenkette: {0}
", settings.StringValue)); Response.Write(string.Format("Datum: {0:d}
", settings.DateValue));
X ASP.NET mit C# Kochbuch – Update ____________________________________ 137
Response.Write(string.Format("Enumeration: {0}
", settings.EnumValue)); Response.Write(string.Format("Boolean: {0}
", settings.BoolValue)); }
Wie Sie sehen können, werden zahlreiche Standarddatentypen unterstützt. Dazu gehören neben anderen beispielsweise string, int, decimal, bool und DateTime. Daneben werden auch beliebige Enumerationen unterstützt, wie im Beispiel gezeigt. Viel Spaß beim Konfigurieren!
Abbildung X.62 Die Daten wurden per Reflection der Konfigurationsklasse zugewiesen.
Stichwort Vererbung Die Basisklasse unterstützt auch die Vererbung von Konfigurationsdaten. Fall der Methode Create ein Elternobjekt übergeben wird, wird ein Kopie dieses Objekts statt einer „leeren“ Instanz der Konfigurationsklasse verwendet: if(parent.GetType().Equals(this.SettingsClassType)) this.settingsClassInstance = this.CreateMemberwiseClone(parent);
Es wird ein so genannter memberwise clone angelegt. Dabei werden alle öffentlichen Felder und Eigenschaften aus dem bestehenden Objekt in eines neues übernommen. Bei Wertetypen wird der Inhalt kopiert, bei Referenztypen die Referenz. In diesem Fall zeigen das alte und neue das Objekt auf das selbe Ziel. Die verwendete Methode CreateMemberwiseClone enstammt dem Rezept „... eine Kopie eines Objekts anlegen?“ aus dem Kapitel „Sprachelemente“.
138 __________________________________ X.47 ... eine Kopie eines Objekts anlegen?
X.47 ... eine Kopie eines Objekts anlegen? Hin und wieder ist es notwendig, eine Kopie eines gegebenen Objektes zu erstellen. Die Mutter aller Objekte, die Klasse object bietet dazu eine Methode MemberwiseClone an, die eine Kopie des Objektes zurückliefert. Eine Kopie meint bedeutet dabei, dass alle öffentlichen Felder und Eigenschaften aus dem bestehenden Objekt in eines neues übernommen werden. Bei Wertetypen wird der Inhalt kopiert, bei Referenztypen die Referenz. In diesem Fall zeigen das alte und neue das Objekt auf das selbe Ziel. Die Methode lässt sich prima verwenden, allerdings ausschließlich innerhalb des Objekts oder einer davon abgeleiteten Klasse – die Methode ist als protected markiert. Nicht verzagen, Reflection fragen lautet das Motto. Mittels Reflection können Sie sehr einfach eine Kopie aller Mitglieder anlegen. Dazu müssen Sie lediglich den Typ des zu kopierenden Objekts abfragen, daraus eine neue Instanz erstellen und anschließend alle öffentlichen wie privaten Felder und Eigenschaften eins zu eins übernommen. Die Methode CreateMemberwiseClone aus dem folgenden Listing tut genau dies. Listing X.82 ClassCloner.cs using System; using System.Reflection; namespace PAL.AspNetKochbuch { public class ClassCloner { public static object CreateMemberwiseClone(object obj) { Type t = obj.GetType(); object objCopy = Activator.CreateInstance(t); foreach(FieldInfo f in t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { object value = f.GetValue(obj); f.SetValue(objCopy, value); } foreach(PropertyInfo p in t.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { if(p.CanRead && p.CanWrite) { object value = p.GetValue(obj, null); p.SetValue(objCopy, value, null);
X ASP.NET mit C# Kochbuch – Update ____________________________________ 139
} } return(objCopy); } } }
Ein kleines Beispiel beweist, dass die Methode problemlos funktioniert. Gegeben ist eine Klasse Person mit zwei Eigenschaft Firstname und Lastname. Einer Instanz der Klasse werden die gewünschten Beispielwerte zugewiesen. Anschließend wird eine Kopie angelegt, die Eigenschaft abgefragt und siehe da, die Daten stehen auch in der Kopie zur Verfügung. Listing X.83 ClassCloner1.aspx <script runat="server"> public class Person { private string firstname; private string lastname; private DateTime birthday; public string Firstname { get { return(this.firstname); } set { this.firstname = value; } } public string Lastname { get { return(this.lastname); } set { this.lastname = value; } } } void Page_Load(object sender, EventArgs e) { Person p1 = new Person(); p1.Firstname = "Homer"; p1.Lastname = "Wells"; Person p2 = (Person) ClassCloner.CreateMemberwiseClone(p1); Response.Write(string.Format("{0} {1}", p2.Firstname, p2.Lastname)); }
140 __________________________________ X.47 ... eine Kopie eines Objekts anlegen?
Abbildung X.63 Die Daten wurden in eine neue Instanz der Klasse kopiert.
Das vorliegende Beispiel würde übrigens auch dann funktionieren, wenn nur die Felder kopiert würden. Wenngleich diese als private gekennzeichnet sind, ist der Zugriff per Reflection ungehindert möglich. Dies gilt für sämtliche Klassen, sofern diese den Zugriff auf interne Felder nicht über die CodeAccessPermission ReflectionPermission verhindern. In diesem Fall könnten nur öffentliche Felder kopiert werden.
Die Schnittstelle ICloneable Der Ordnung halber möchte ich Ihnen an dieser Stelle noch die Schnittstelle ICloneable vorstellen. Sofern es sich bei der zu kopierenden Klasse um ihre
handelt, bietet es sich möglicherweise an, eine ganz „reguläre“ Kopierfunktionalität anzubieten. Hierfür definiert die Schnittstelle eine einzige Methode Clone. Angewendet auf die Klasse Person sieht deren Implementation beispielsweise so aus: Listing X.84 ICloneable1.aspx <script runat="server"> public class Person : ICloneable { private string firstname; private string lastname; private DateTime birthday; public string Firstname { get { return(this.firstname); } set { this.firstname = value; } } public string Lastname { get { return(this.lastname); }
X ASP.NET mit C# Kochbuch – Update ____________________________________ 141
set { this.lastname = value; } } public object Clone() { Person personCopy = new Person(); personCopy.Firstname = this.Firstname; personCopy.Lastname = this.Lastname; return(personCopy); } } void Page_Load(object sender, EventArgs e) { Person p1 = new Person(); p1.Firstname = "Homer"; p1.Lastname = "Wells"; Person p2 = (Person) p1.Clone(); Response.Write(string.Format("{0} {1}", p2.Firstname, p2.Lastname)); }
X.48 ... eine Zeile zur Neuanlage im DataGrid realisieren? Das DataGrid-Control besitzt eingebaute Funktionen zum Selektieren und Editieren der angezeigten Datensätze. Eine Möglichkeit zur Anlage weiterer Einträge wird jedoch von Haus aus nicht geboten. Dies wird typischerweise über Eingabefelder außerhalb des Grids umgesetzt. Viel schöner wäre allerdings, wenn dies direkt innerhalb der tabellarischen Ansicht möglich wäre. Mit einem kleinen Trick geht’s. Wie schon in einigen anderen Beispielen gezeigt, handelt es sich bei den vom DataGrid-Control angebotenen Ereignissen ItemCreated und ItemDataBound um regelrechte eierlegende Wollmilchkuhsäue. Auch in diesem Rezept greife ich wieder auf deren Möglichkeit zurück. Konkret sollen die zur Eingabe des neuen Datensatzes notwendigen Eingabefelder im Fußbereich der Tabelle einblendet werden. Hierzu muss zunächst die standardmäßig deaktivierte Option ShowFooter auf true gesetzt werden. Im ItemCreatedEreignis wird die automatisch angelegte Fußzeile nun manipuliert. In den ersten Zellen werden Eingabefelder angefügt, im letzten ein Button, um die Eingabe zu bestätigen.
142 _____________________ X.48 ... eine Zeile zur Neuanlage im DataGrid realisieren?
Listing X.85 DataGridAdd1.aspx ... void dg_ItemCreated(object sender, DataGridItemEventArgs e) { if(e.Item.ItemType == ListItemType.Footer) { e.Item.Cells[0].Controls.Add(new TextBox()); e.Item.Cells[1].Controls.Add(new TextBox()); Button bt = new Button(); bt.Text = "Neu"; bt.Click += new EventHandler(this.btAdd_Click); e.Item.Cells[2].Controls.Add(bt); } } void btAdd_Click(object sender, EventArgs e) { DataGridItem item = (DataGridItem) ((Control) sender).NamingContainer; TextBox tb_fn = (TextBox) item.Cells[0].Controls[0]; TextBox tb_ln = (TextBox) item.Cells[1].Controls[0]; OleDbConnection conn = new OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;" + @"Data Source=c:\inetpub\wwwroot\aspnet\books.mdb"); conn.Open(); string SQL = "INSERT INTO Authors (Firstname, Lastname) VALUES (@Firstname, @Lastname);"; OleDbCommand cmd = new OleDbCommand(SQL, conn); cmd.Parameters.Add("@Firstname", tb_fn.Text); cmd.Parameters.Add("@Lastname", tb_ln.Text); cmd.ExecuteNonQuery(); conn.Close(); tb_fn.Text = string.Empty; tb_ln.Text = string.Empty; ExecuteDataBinding(); }
Dem Button wird eine serverseitige Ereignisbehandlung zugewiesen. Über den NamingContainer wird dort das zugehörige DataGridItem und darüber die einzelnen Zellen und die darin enthaltenen Eingabefelder abgefragt. Anschließend wird der neue Datensatz über so ermittelten Benutzereigaben mit den Bordmitteln von ADO.NET angelegt, die Eingabefelder wieder geleert und die Datenbindung neu ausgeführt – es funktioniert!
144 _____________________ X.48 ... eine Zeile zur Neuanlage im DataGrid realisieren?
Abbildung X.64 Über die Fußzeile lässt sich ein neuer Datensatz anlegen.
Validierung der Eingaben vor der Neuanlage Selbstverständlich lassen sich die Benutzereingaben bei der Neuanlage wie auch sonst üblich mittels den angebotenen Validation Controls überprüfen. So sind beispielsweise leere Datensätze in aller Regel wenig sinnvoll. Wie auch die Eingabefelder müssen die Validation Controls programmatisch eingefügt werden. Auch dies geschieht innerhalb des ItemCreated-Ereignisses des DataGrid-Controls. Um eine Zuordnung zu ermöglichen, müssen den TextBoxElementen nun explizit IDs zugewiesen werden, auf sich die Validatoren beziehen können. Listing X.86 DataGridAdd2.aspx ... void dg_ItemCreated(object sender, DataGridItemEventArgs e) { DataGrid dg = (DataGrid) sender; if(e.Item.ItemType == ListItemType.Footer) { if(dg.EditItemIndex == -1) { TextBox tb; RequiredFieldValidator validator; // Vorname
X ASP.NET mit C# Kochbuch – Update ____________________________________ 145
tb = new TextBox(); tb.ID = "tb_fn"; tb.MaxLength = 20; e.Item.Cells[0].Controls.Add(tb); validator = new RequiredFieldValidator(); validator.ControlToValidate = tb.ID; validator.Display = ValidatorDisplay.Dynamic; validator.Text = ""; validator.ErrorMessage = "Bitte geben Sie einen Vornamen an!"; e.Item.Cells[0].Controls.Add(validator); // Nachname tb = new TextBox(); tb.ID = "tb_ln"; tb.MaxLength = 20; e.Item.Cells[1].Controls.Add(tb); validator = new RequiredFieldValidator(); validator.ControlToValidate = tb.ID; validator.Display = ValidatorDisplay.Dynamic; validator.Text = ""; validator.ErrorMessage = "Bitte geben Sie einen Nachnamen an!"; e.Item.Cells[1].Controls.Add(validator); // Button Button bt = new Button(); bt.Text = "Neu"; bt.Click += new EventHandler(this.btAdd_Click); e.Item.Cells[2].Controls.Add(bt); } else // dg.EditItemIndex == -1 { e.Item.Visible = false; } } } void btAdd_Click(object sender, EventArgs e) { if(this.IsValid) { ... } } ...
146 _____________________ X.48 ... eine Zeile zur Neuanlage im DataGrid realisieren?
Um zusätzlich die serverseitige Validierung in Anspruch zu nehmen, muss die Neuanlage bedingt stattfinden. Hierzu wird wie auch sonst üblich die Eigenschaft IsValid der Page-Klasse bemüht.
Abbildung X.65 Leere Eingaben werden nicht akzeptiert.
Prinzipiell problematisch ist die Validierung von zwei logischen Formularen innerhalb eines serverseitigen ASP.NET-Formulars. Die Validierung wird jeweils für alle Eingabefelder durchgeführt, was in aller Regel nicht gewünscht. Auch in diesem Fall stellt das Problem, nämlich immer dann, wenn Sie einen Datensatz bearbeiten wollen. Ich habe daher das Beispiel so angepasst, dass die Fußzeile komplett ausgeblendet wird, sobald ein Datensatz bearbeitet wird. Ist die Bearbeitung abgeschlossen, wird die Zeile zur Neuanlage wieder angezeigt.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 147
Abbildung X.66 Die Fußzeile wird im Bearbeitungsmodus ausgeblendet.
X.49 ... RadioButtons zur Auswahl einer DataGrid-Zeile benuten? Es bietet sich im Grunde an, die Auswahl einer DataGrid-Zeile einer Spalte RadioButton-Controls zu überlassen. Mittels AutoPostBack muss nur noch jeweilige Control angeklickt werden, und schon ist die Zeile selektiert. Umsetzung bedarf es einer TemplateColumn und einer Behandlung serverseitigen Ereignisses CheckedChanged. Listing X.87 DataGridRadioButton1.aspx ... void rb_CheckedChanged(object sender, EventArgs e) { RadioButton ctl = (RadioButton) sender; if(ctl.Checked) { DataGridItem currentItem = (DataGridItem) ctl.NamingContainer; dg.SelectedIndex = currentItem.ItemIndex; ExecuteDataBinding(); } } <SelectedItemStyle BackColor="Red" />
Die Ereignisbehandlung sorgt dafür, dass beim Klick auf eines der RadioButtonControls die entsprechende Zeile markiert wird. Andersherum wird der Status der Checked-Eigenschaft mittels DataBinding-Syntax im Layout-Bereich festgelegt. Hierbei wird der Index der jeweils aktuellen Zeile mit der ausgewählten verglichen. Auf diese Weise wird die Selektion auch dann visualisiert, wenn diese programmatisch gesetzt würde.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 149
Abbildung X.67 Die Zeile wurde über das RadioButton-Control selektiert.
Prinzipiell ist die Verwendung von RadioButton-Controls innerhalb von Data Controls nicht ganz unproblematisch. Das Problem ergibt sich dabei aus der Eigenschaft GroupName, über die die einzelnen Controls zu einer Gruppe zusammengefasst werden. Pro Gruppe kann nur jeweils ein Control ausgewählt sein. Jede Zeile des DataGrid-Controls arbeitet als autarker NamingContainer. Als Folge wird die Eigenschaft GroupName mit einem fortlaufenden Namenszusatz an den Client übergeben. Der HTML-Quelltext des vorherigen Beispiels sieht beispielsweise so aus: ... ... ...
Da das aus der Eigenschaft GroupName erzeugte Attribut name in jedem Control unterschiedlich gesetzt ist, geht der Browser von unterschiedlichen Gruppen aus und erlaubt daher die unerwünschte Mehrfachselektion. Um dies zum umgehen, sollten Sie bei einer weitergehenden Verwendung der Controls die Eigenschaft Checked programmatisch setzen. Sofern Sie ohne PostBack arbeiten, müssen Sie das Verhalten des RadioButton-Controls manipulieren, indem Sie ein entsprechend geändertes Control ableiten.
150 __________________________ X.50 ... aus einer Web-Applikation heraus drucken?
X.50 ... aus einer Web-Applikation heraus drucken? Das .NET Framework bietet integrierte Möglichkeiten zum Drucken von einfachen wie auch komplexen Dokumenten. Diese basieren auf speziellen Klassen zur Druckeransteuerung sowie den bereits bekannten GDI+-Klassen aus dem Namespace System.Drawing. Es spielt also keine wesentliche Rolle, ob Sie eine Grafik für den Bildschirm erzeugen oder diese auf dem Drucker ausgeben möchten das Prinzip bleibt. Jede physikalische Seite wird dabei als separate Grafik behandelt. Eine ausführliche Beschreibung der zu Grunde liegenden Klassen würde den Rahmen und Sinn dieses Buches sprengen. Ich verweise in diesem Zusammenhang gerne auf die Werke „Visual C# .NET Grundlagen und Profiwissen“ respektive „Visual Basic .NET Grundlagen und Profiwissen“ des Autorenteams Doberenz/Kowalski, die beide im Carl Hanser Verlag erschienen sind. Einen elementarer Grundstein für den Druck stellt die Klasse PrintDocument dar. Eine neue Instanz der Klasse repäsentiert ein auf dem Drucker auszugebendes Dokument. Der Start des Drucks erfolgt über die Methode Print. Die Datenübergabe wird anschließend über das Ereignis PrintPage realisiert, das für jede einzelne Seite ausgelöst wird, solange Sie über die Eigenschaft HasMorePages der Ereignisargumente zurückmelden, dass weitere Seiten gedruckt werden sollen. Die Argumente vom Typ PrintPageEventArgs liefern eine Reihe von Informationen sowie die benötige Graphics-Instanz zur Ausgabe der Daten mittels GDI+. Das Beispiel2 demonstriert die Verwendung der Klassen. Das Web-Formular enthält eine DropDownList zur Auswahl des gewünschten Druckers. Ein Klick auf den Button gibt die in der darunter befindlichen TextBox eingegebene Zeichenfolge auf dem gewählten Drucker aus. Listing X.88 print1.aspx <script runat="server"> private Font printFont; private StringReader stringToPrint;
2
Wer sich bereits etwas mit den Druckmöglichkeiten von .NET beschäfigt hat, wird das Beispiel möglicherweise wieder erkennen. Es handelt sich um eine angepasste Variante eines Listings aus der .NET Framework SDK Dokumentation.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 151
void Page_Load(object sender, EventArgs e) { if(!IsPostBack) { ddl_printers.DataSource = PrinterSettings.InstalledPrinters; ddl_printers.DataBind(); } } void btPrint_click(object sender, EventArgs e) { stringToPrint = new StringReader(tb.Text); printFont = new Font("Arial", 10); PrintDocument pd = new PrintDocument(); pd.PrinterSettings.PrinterName = ddl_printers.SelectedItem.Value; pd.PrintPage += new PrintPageEventHandler(this.pd_PrintPage); pd.Print(); stringToPrint.Close(); } private void pd_PrintPage(object sender, PrintPageEventArgs e) { float linesPerPage = 0; float yPos = 0; int count = 0; float leftMargin = e.MarginBounds.Left; float topMargin = e.MarginBounds.Top; string line = null; linesPerPage = e.MarginBounds.Height / printFont.GetHeight(e.Graphics); while(count < linesPerPage && ((line = stringToPrint.ReadLine()) != null)) { yPos = topMargin + (count * printFont.GetHeight(e.Graphics)); e.Graphics.DrawString(line, printFont, Brushes.Black, leftMargin, yPos, new StringFormat()); count++; } e.HasMorePages = (line != null); }
152 __________________________ X.50 ... aus einer Web-Applikation heraus drucken?
Drucker:
Abbildung X.68 Ein Klick auf den Button gibt den Text auf dem gewählten Drucker aus.
Einrichtung der Druckfunktionalität In den Newsgroups wurde des öfteren nach einer Möglichkeit gefragt, aus ASP.NET heraus zu drucken. Die Entwickler sind in aller Regel an einer Fehlermeldung des RPC-Servers gescheitert. Der Grund hierfür liegt in der Tatsache, dass aus Service-Accounts des Betriebssystems nicht gedruckt werden kann. Diese Benutzer haben schlicht keinen Zugriff auf die Drucker.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 153
Um den Service-Accounts des Betriebssystems Zugriff auf die Drucker zu geben, sind eine Reihe von Eintragungen in der Registrierung notwendig. Der Artikel Q184291 aus der Microsoft Knowledge Base beschreibt das notwendige Vorgehen. Der Artikel wurde zwar für klassische ASP geschrieben, hat aber auch bei ASP.NET weiterhin Gültigkeit. Folgende Schritte sind demnach notwendig: 1. Starten Sie die Registrierung (regedit.exe) und suchen Sie folgenden Schlüssel im Zweig HKEY_CURRENT_USER: \Software\Microsoft\Windows NT\Current Version\Devices
2. Exportieren Sie den Schlüssel über das Datei-Menü in eine Datei mit dem Namen devices.reg. 3. Verfahren Sie analog für die folgenden Schlüssel, die Sie in zwei Dateien mit den Namen printerports.reg und windows.reg exportieren: \Software\Microsoft\Windows NT\Current Version\PrinterPorts \Software\Microsoft\Windows NT\Current Version\Windows
4. Die drei Dateien enthalten nun die kompletten Druckereinstellungen des aktuell angemeldeten Benutzers. Sie müssen diese nun allen Benutzer inklusive den System-Accounts zur Verfügung stellen. Öffnen Sie dazu nacheinander alle drei Dateien im Editor notepad.exe. Ersetzen Sie jeweils die Zeichenfolge HKEY_CURRENT_USER durch HKEY_USERS\.DEFAULT. 5. Nachdem die Dateien angepasst sind, müssen Sie diese in die Registrierung importieren. Klicken Sie dazu einfach nacheinander doppelt auf die Dateien und bestätigen die Sicherheitsabfragen. Anschließend können auch ServiceAccounts auf die Druckereinstellungen zugreifen. Die Originalversion des Microsoft Knowledge Base-Artikels finden Sie unter folgender Adresse: http://www.combit.net/de/move?http://support.microsoft.com/ support/kb/articles/Q184/2/91.ASP
Noch ist es damit allerdings nicht ganz getan, denn möglicherweise verhindern noch die Sicherheitseinstellungen des Druckers die Zugriff durch den Benutzer „ASPNET“. Um die notwendigen Rechte zu vergeben, öffnen Sie bitte die Eigenschaften des gewünschten Druckers und wählen die Registerlasche „Sicherheitseinstellungen“ an. Sofern – wie in der Abbildung gezeigt – bereits Rechte für alle Benutzer („Jeder“ oder „Everyone“) vergeben sind, können Sie den Dialog gleich wieder schließen,
154 _________________ X.51 ... erkennen, ob es sich um ein mobiles Endgerät handelt?
der Druck sollte bereits möglich sein. Ansonsten können Sie speziell den Benutzer ASPNET hinzufügen und diesem das zum Druck notwendige Recht erteilen.
Abbildung X.69 Der Benutzer benötigt Druckrechte.
X.51 ... erkennen, ob es sich um ein mobiles Endgerät handelt? Während sich die erste WAP-Version als echter Flopp erwiesen hat, wird bereits intensiv an diversen neuen Lösungen gearbeitet. Nicht zuletzt UMTS und die Möglichkeiten von .NET sollen mobilen Geräten langfristig einen qualitativen wie auch quantitativen Zugriff auf Internetinhalte ermöglichen. Mit .NET haben Sie bereits heute die Möglichkeit, Web-Applikationen zu entwickeln, die speziell angepasste Ausgaben für diverse mobile Endgeräte erzeugen. Das dazu notwendige Mobile Internet Toolkit ist seit Version 1.1 ein integeraler Bestandteil des .NET Frameworks3.
3
Wer noch mit Version 1.0 des .NET Framework entwickelt, findet das Mobile Internet Tookit auf der begleitenden Buch-CD-ROM.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 155
Um Ihr Internet-Angebot für Desktop- und Mobilgeräte unter einer einzigen Adressen zugreifbar zu machen, benötigen Sie eine Weiche, die je nach Endgerät die jeweils optimierte Seite anzeigt. Sie können sich dazu die BrowserCapabilities zu Nutze machen, die Ihnen von der Eigenschaft Request.Browser geliefert werden. Ist das Mobile Internet Tookit installiert, wird eine Instanz der abgeleiteten Klasse MobileCapabilities geliefert. Über deren Eigenschaft IsMobileDevice können Sie erfragen, ob es sich um ein mobiles Gerät handelt oder nicht.
Listing X.89 IsMobile1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { if(this.IsMobileDevice()) { Server.Transfer("IsMobile2.aspx"); } } bool IsMobileDevice() { MobileCapabilities capabilities = (Request.Browser as MobileCapabilities); return((capabilities != null) && (capabilities.IsMobileDevice)); } Herzlich willkommen! Sie setzen kein mobiles Gerät ein!
156 _________________ X.51 ... erkennen, ob es sich um ein mobiles Endgerät handelt?
Abbildung X.70 Mit einem Desktop-Browser wird die reguläre Seite angezeigt.
Abbildung X.71 Der mobile Besucht erhält eine spezielle Seite.
Nachdem über die Startseite eine Weiche ausgeführt wurde, können Sie ausgehend von der spezifischen Startseite eine speziell für das jeweilige Endgerät angepasste Web-Applikation anbieten.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 157
X.52 ... eine Array in Visual Basic .NET dimensionieren? Beim klassischen ASP in Verbindung mit VBScript ließt sich ein Array sehr einfach anlegen und dimensionieren. Das folgende Listing zeigt beispielsweise die Anlage eines Arrays mit fünf Elementen. Listing X.90 array1.asp
Recht ähnlich ließen sich Arrays auch in der Schwestersprache Visual Basic 6.0 anlegen und dimensionieren. Dim MyArray(4) As String
Bei Visual Basic .NET lässt sich diese Syntax weiter verwenden. Das minimal angepasste Beispiel funktioniert tadellos: Listing X.91 array1.aspx <script runat="server"> Sub Page_Load(Sender As Object, E As EventArgs) Dim MyArray(4) As String Dim I As Integer For I = 0 To 4 MyArray(I) = I Response.Write(CStr(I) & "
") Next End Sub
158 ____________________ X.53 ... eine Sicherheitsabfrage ohne JavaScript anzeigen?
In aller Regel werden Sie ein Array aber bei Visual Basic .NET ohne Dimension deklarieren und erst in der Zuweisung die Größe angeben. Hierzu ist eine etwas ungewöhnliche Schreibweise notwendig: Dim MyArray() As String = new String(4) {}
Die geschweiften Klammern sind notwendig, damit der Compiler den Unterschied zwischen dem Konstruktor des Datentyps String und der Dimensionierung des Arrays erkennen kann. Alternativ können Sie geschweiften Klammern auch direkt zur Zuweisung der gewünschten Elemente nutzen: Dim MyArray() As String = {"0", "1", "2", "3", "4"}
In C# ist dieser Umweg übrigens nicht notwendig, da hier zur Differenzierung zwischen Konstrzktor und Dimensionierung unterschiedliche Klammerntypen genutzt werden: string[] myArray = new string[5];
Im Unterschied zu Visual Basic .NET wird in C# die Größe des Arrays und nicht etwa die obere Begrenzung angegeben. In allen Beispielen wird also ein Array mit fünf Elementen dimensioniert, die über den Index 0 bis 4 abzufragen sind.
X.53 ... eine Sicherheitsabfrage ohne JavaScript anzeigen? Sicherheitsabfragen sind mit Sicherheit verpönt – welch Wortspiel. In manchen Fällen ist es aber notwendig, den Benutzer noch einmal auf die Konsequenzen der geplanten Operation hinzuweisen und ihn um eine erneute Bestätigung zu bitten. Oftmals werden diese clientseitig mittels JavaScript realisiert. ASP.NET bietet jedoch dank ViewState einen alternativen, serverseitigen Weg, der gerantiert mit jedem Browser und unabhängig von den Sicherheitseinstellungen des Besuchers funktioniert. Die Abfrage kann auf Basis mehrerer Panel-Controls geschehen, die wechselweitig ein- und ausgeschaltet werden. Das erste Control enthält das eigentliche Formular, das zweite die Sicherheitsabfrage und das dritte die Bestätigung. Das Listing enthält eine derartige Realisierung. Listing X.92 Confirmation1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e)
X ASP.NET mit C# Kochbuch – Update ____________________________________ 159
{ if(!IsPostBack) { pnl_confirmation.Visible = false; pnl_message.Visible = false; } } void btConfirm_Click(object sender, EventArgs e) { pnl_form.Visible = false; pnl_confirmation.Visible = true; pnl_confirmation.DataBind(); } void btCreate_Click(object sender, EventArgs e) { // Anlage des Datensatzes ... pnl_confirmation.Visible = false; pnl_message.Visible = true; pnl_message.DataBind(); } void btCancel_Click(object sender, EventArgs e) { pnl_confirmation.Visible = false; pnl_form.Visible = true; } Neuen Datensatz anlegen Vorname:
Nachname:
160 ____________________ X.53 ... eine Sicherheitsabfrage ohne JavaScript anzeigen?
Sicherheitsabfrage Sind Sie sicher, dass den Datensatz anlegen möchten?
Danke! Der neue Datensatz wurde wie gewünscht angelegt!
Abbildung X.72 Nach der Eingabe erfolgt eine Sicherheitsabfrage.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 161
Über den ViewState bleiben die eingegeben Daten auch während der Sicherheitsabfrage gespeichert. Um innerhalb dieser die Eingaben noch einmal anzuzeigen, wird die DataBind-Syntax genutzt. Damit diese ausgeführt wird, erfolgt ein Aufruf der DataBind-Methode des jeweiligen Panel-Controls. Statt einem Panel ließe sich im Beispiel auch ein PlaceHolder verwenden. Haben Sie sich auch schon einmal gefragt, wo denn genau der Unterschied zwischen diesen beiden liegt? Es gibt derer zwei. Zum einen wird das Panel-Control über das HTML-Tag im Browser dargestellt und ermöglicht daher die Formatierung des Inhalts über Cascading Stylesheets. Das PlaceHolder-Control wird hingegen nicht auf dem Client abgebildet und erzeugt selbst keinerlei Ausgaben. Der zweite Unterschied wird innerhalb der Entwicklungsumgebung Visual Studio .NET deutlich. Während sich die Inhalt des Panel hier visuell bearbeiten lassen, wird der PlaceHolder tatsächlich nur als statischer Platzhalter angezeigt.
X.54 ... eine angepasste Management Console anlegen und benutzen? Kennen Sie die Microsoft Management Console? Bestimmt, denn diese wird beispielsweise aufgerufen, wenn Sie den „Internetdienste-Manager“ aus dem Verwaltungsbereich starten. Die Konsole – auch MMC abgekürzt – dient als zentrale Konfigurationsschnittstelle für Administratoren. Auf übersichtliche Weise lassen sich sowohl der lokale als auch andere Rechner im Netzverbund administrieren. Die einzelnen Funktionen werden dabei über so genannte Snap-Ins angeboten.
162 ___________ X.54 ... eine angepasste Management Console anlegen und benutzen?
Abbildung X.73 Die angepasste Konsole enthält alle relevanten Snap-Ins.
Die MMC bietet die Möglichkeit, ein angepasste Konsole zu erstellen und zu speichern. Auf diese können Sie die für Sie relevanten Konfigurationseinstellungen übersichtich anordnen und jederzeit innerhalb nur eines Fensters zugreifbar halten. Zur Anlage einer neuen Konsole starten Sie zunächst das Programm mmc.exe. Die nun angezeigte Konsole ist noch leer. Über den Menüpunkt Konsole > Snap-In hinzufügen/entfernen ... können Sie nun die gewünschten Funktionen hinzufügen. Im Umfeld von ASP.NET sind dabei insbesondere die folgenden SnapIns wichtig: • .NET Konfiguration • Computerverwaltung • Dienste • Ereignisanzeige • Microsoft Servererweiterungen • Internet-Informationsdienste • Leistungsprotokolle und Warnungen • Lokale Benutzer und Gruppen • Microsoft SQL Servers
X ASP.NET mit C# Kochbuch – Update ____________________________________ 163
Die Abbildung zeigt bereits eine derartig angepasste Konsole mit den aufgeführten Snap-Ins. Sie können die Konsole nun (auf dem Desktop) abspeichern und so mit einem Klick jederzeit wieder aufrufen. Zuvor sollten Sie allerdings den Zugriff als Benutzer einstellen. Hierzu ändern Sie den Konsolenmodus im Dialog Konsole > Optionen ... wie in der zweiten Abbildung gezeigt. In diesem Benutzermodus sind keine Konfigurationen der Konsole mehr möglich, was unbeabsichtigen Änderungen vorbeugt. Um dennoch Änderungen vorzunehmen, wählen Sie die abgespeicherte Datei mit der Endung .mmc mit der rechten Maustaste an und wählen dort den Menüpunkt „Autor“.
Abbildung X.74 Der Benutzermodus berhindert unbeabsichtige Änderungen.
X.55 ... den Timeout einer Seite verändern? Das Rezept „... die maximale Dateigröße für Uploads erhöhen?“ hat gezeigt, dass sich die maximal mögliche Größe einer HTTP-Anforderung an die ASP.NETEngine individuell anpassen lässt. In diesem Zusammenhang empfiehlt es sich auch den Timeout für die Anforderung zu erhöhen. Dieser liegt in der Voreinstellung bei 90 Sekunden. Diese Zeitspanne reicht jedoch für größere Uploads bei langsamen bis mittleren Bandbreiten nicht aus. Um den Timeout zu verändern, weisen Sie den neuen Wert in Sekunden dem Attribut executionTimeout zu, beispielsweise 5 Minuten:
164 ______ X.56 ... Grafiken proportional in eine vorgegebene Maximalgröße einpassen?
Listing X.93 web.config <system.web>
Auch diese Einstellung sollte nicht in der machine.config, sondern in einer web.config für die Applikation oder besser noch ein ausgesuchtes Verzeichnis eingestellt werden. Alternativ lässt sich der Timeout auch programmatisch über die Eigenschaft Server.ScriptTimeout festlegen. Der zugewiesene Wert gilt ausschließlich für aktuelle Anforderung und auch nur ab dem Zeitpunkt, da der Standardwert über die Eigenschaft geändert wurde. Uploads lassen sich daher auf diese Weise nicht beeinflussen, da das Page_Load-Ereignisses erst im Anschluss an die vollständige Übertragung der Anforderung ausgelöst wird.
X.56 ... Grafiken proportional in eine vorgegebene Maximalgröße einpassen? Um ein vorgegebenes Layout auch bei unterschiedlichen Grafiken einhalten zu können, ist es oftmals notwendig, diese in eine vorgegebene Maximalgröße einzupassen. Je nach Format des Bildes und der Maximalgröße dient entweder die Breite oder die Höhe zur Bestimmung des Umrechnungsverhältnisses. Im Listing ist eine derartige Berechnung zu erkennen. Der Methode AdjustBitmap wird neben einer Grafik auch eine Instanz der Struktur Size mit der maximal möglichen Größe übergeben. Mittels GetThumbnailImage wird die Grafik so proportional verkleinert oder vergrößert, dass Sie genau in diesen Bereich passt. Listing X.94 adjust1.ashx using using using using using
System; System.Drawing; System.Drawing.Imaging; System.IO; System.Web;
class Adjust1 : PAL.Projects.AspNetKochbuch.BaseHttpHandler
X ASP.NET mit C# Kochbuch – Update ____________________________________ 165
{ protected override void Process() { string file = Server.MapPath(Request.QueryString["file"]); Bitmap b = this.AdjustBitmap(new Bitmap(file), new Size(150, 150)); Response.ContentType = "image/jpeg"; b.Save(Response.OutputStream, ImageFormat.Jpeg); } protected Bitmap AdjustBitmap(Bitmap bitmap, Size maxSize) { float w, h; if(((float) maxSize.Width) / bitmap.Size.Width * bitmap.Size.Height void Page_Load(object sender, EventArgs e) { if(!IsPostBack) pnl_bankname.Visible = false; } void bt_Click(object sender, EventArgs e) { if(this.IsValid) lb_message.Text = "Alles OK... :-)"; else lb_message.Text = string.Empty; } void bankcode_ServerValidate(object sender, ServerValidateEventArgs e) { CustomValidator ctl = (CustomValidator) sender; e.IsValid = false; if(e.Value.Equals(ViewState["bankcode"]) && (lb_banknames.SelectedIndex != -1)) { e.IsValid = true; } else if(!e.Value.Equals(ViewState["bankcode"])) { lb_banknames.DataSource = this.LoadBanknames(e.Value); lb_banknames.DataBind(); if(lb_banknames.Items.Count == 0) { ctl.ErrorMessage = "Die eingegebene Bankleitzahl ist ungültig."; pnl_bankname.Visible = false; }
172 ______________________________ X.59 ... ein Bankleitzahlverzeichnis integrieren?
else if(lb_banknames.Items.Count == 1) { e.IsValid = true; lb_banknames.SelectedIndex = 0; pnl_bankname.Visible = true; } else { ctl.ErrorMessage = "Bitte wählen Sie ein Kreditinstitut aus."; pnl_bankname.Visible = true; } } ViewState["bankcode"] = e.Value; } IDataReader LoadBanknames(string bankcode) { OleDbConnection conn = new OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;" + @"Data Source=c:\inetpub\wwwroot\aspnet\bankcodes.mdb"); conn.Open(); string sql = "SELECT ID, Shortname FROM Bankcodes WHERE Bankcode=@Bankcode;"; OleDbCommand cmd = new OleDbCommand(sql, conn); cmd.Parameters.Add("@Bankcode", bankcode); return(cmd.ExecuteReader(CommandBehavior.CloseConnection)); } Bankleitzahl:
X ASP.NET mit C# Kochbuch – Update ____________________________________ 173
Bankinstitut:
Abbildung X.80 Nicht eindeutige Bankleitzahlen müssen konkretisiert werden.
174 ______________________________ X.59 ... ein Bankleitzahlverzeichnis integrieren?
Die Abbildung zeigt das Beispiel in Aktion. Es wurde eine Bankleitzahl eingegeben, für die mehrere Einträge in der Datenbank hinterlegt sind. Die Deutsche Bank 24 verfügt in dem Gebiet über mehrere Filialen mit der gleichen Kennnummer. Der Benutzer muss nun die kontoführende Filiale aussuchen. Ein weiterer Buttonklick führt anschließend zu der Ausgabe einer kleinen Erfolgsmeldung. Im Regelfall würden die Daten nun gespeichert werden. Das Formular sieht bisher nur die Eingabe einer Bankleitzahl vor. Eine erweiterte Version, die auch Eingabe und Validierung einer Kontonummer erlaubt, finden Sie im nachfolgenden Rezept.
Hintergrundinformationen Der Aufbau der deutschen Bankleitzahlen folgt einem festen Schema und erlaubt so den Rückschluß auf einige interessante Informationen, wie beispielsweise die Region der Bank. 1
2
3
Clr. Ortsnummer
4
5
6
7
NetzNr.
Niederlassungsnummer
8
Die erste Stelle der Bankleitzahl ist besonders interessant. Diese lässt den Schluß auf das so genannte Clearing-Gebiet zu: 1. Berlin, Brandenburg, Mecklenburg-Vorpommern 2. Bremen, Hamburg, Niedersachsen, Schleswig-Holstein 3. Rheinland (Regierungsbezirke Düsseldorf, Köln) 4. Westfalen 5. Hessen, Rheinland-Pfalz, Saarland 6. Baden-Württemberg 7. Bayern 8. Sachsen, Sachsen-Anhalt, Thüringen Die Netznummer an der vierten Stelle ermöglicht die Einordnung in die zugehörige Bankengruppe: 0. Bundesbank/Landeszentralbanken 1. bis 3. Kreditinstitute, soweit nicht in einer der anderen Gruppen erfasst 4. Commerzbank 5. Girozentralen und Sparkassen
X ASP.NET mit C# Kochbuch – Update ____________________________________ 175
6. und 9. Genossenschaftliche Zentralbanken, Kreditgenossenschaften sowie ehemalige Genossenschaften 7. Deutsche Bank 8. Dresdner Bank Die Bundesbank bietet ein Merkblatt in Form einer PDF-Datei an. Neben dem Aufbau der Bankleitzahlen werden in dem Dokument auch weitergehende Vorgehensweisen beschrieben, die auch in Verbindung mit dem Einzug von Gebühren per Lastschrift relevant sind.
X.60 ... eine Kontonummer validieren? Haben Sie im vorherigen Beispiel etwas vermisst? Genau, das Formular hat zwar die Eingabe einer Bankleitzahl ermöglicht, die zum Einzug ebenfalls dringend benötigte Kontonummer wurde Ihnen allerdings vorenthalten. Dies soll sich mit diesem Rezept ändern. Grund hierfür ist die Tatsache, dass die schlichte Eingabe nicht ausreicht. Vielemehr sollte die von Benutzer eingegebene Kontonummer validiert werden. Die allermeisten Kontonummern stellen eine Prüfziffer zur Verfügung, die über einen von derzeit insgesamt 102 Algorithmen (00 bis 99, A0, und A1) verifiziert werden kann. Die Art der zu verwendenden Berechnung ergibt sich aus der Bankleitzahl. Jedem Eintrag in der Datenbank ist dazu ein alphanumerisches Kennzeichen zugeordnet. Ein weiteres Merkblatt der Deutsches Bundesbank liefert ausführliche Informationen zu den jeweiligen Berechnungen. So ist dort zur Prüfzifferberechnung mit der Kennziffer 00 folgendes zu lesen: Die Stellen der Kontonummer sind von rechts nach links mit den Ziffern 2, 1, 2, 1, 2 usw. zu multiplizieren. Die jeweiligen Produkte werden addiert, nachdem jeweils aus den zweistelligen Produkten die Quersumme gebildet wurde (z. B. Produkt 16 = Quersumme 7). Nach der Addition bleiben außer der Einerstelle alle anderen Stellen unberücksichtigt. Die Einerstelle wird von dem Wert 10 subtrahiert. Das Ergebnis ist die Prüfziffer (10. Stelle der Kontonummer). Ergibt sich nach der Subtraktion der Rest 10, ist die Prüfziffer 0. Nach einem ähnlichen System arbeiten auch die allermeisten anderen Verfahren. Es lassen sich dabei mehrere Basisberechnungen verwenden, die jeweils unterschiedliche Gewichtungen nutzen. In diesem Fall ist die Gewichtung beispielsweise 2, 1, 2, 1, 2, 1, 2, 1, 2. Die Buch-CD-ROM enthält eine in C# geschriebene Klasse, die eine ganze Reihe von Verfahren implementiert. Der erwähnte Algorithmus 00 sieht dabei wie nach-
176 _____________________________________ X.60 ... eine Kontonummer validieren?
folgend gezeigt aus. Es wird deutlich, wie stark sich die unterschiedlichen Verfahren mit Hilfe von Hilfsfunktionen standardisieren lassen. public bool Check(string accountNo, string checkId) { bool valid = false; if((accountNo.Length > 4) && (accountNo.Length void bt_Click(object sender, EventArgs e) { string text = Regex.Replace(tb.Text, "", string.Empty, RegexOptions.Singleline); text = text.Replace("\r\n\r\n", ""); text = text.Replace("\r\n", "
"); lb.Text = text; }
Ihre Eingabe:
180 ______________________________________________ X.61 ... HTML-Tags filtern?
Abbildung X.82 Die HTML-Tags wurden gefiltert.
Möglicherweis werden Sie nun einwerfen, dass sich doch die Methode Server.HtmlEncode sehr viel einfacher verwenden ließe, um die Ausgabe von HTML-Tags zu unterbinden. Das prinzipiell zwar richtig, allerdings werden die Tags hier nicht gefiltert, sondern in die entsprechenden Auszeichnungen umgewandelt. Dies bedeutet, dass die Tags wie in der Eingabe auch im Ergebnis erscheinen.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 181
Abbildung X.83 Die Methode Server.HtmlEncode führt nicht zum gewünschten Ziel.
X.62 ... einen File Upload validieren? Mit ASP.NET ist es ein Leichtes, dem Benutzer den Upload von Dateien zu ermöglichen. Aber bevor Sie die übertragenen Daten einfach so auf dem Server abspeichern und/oder weiterverarbeiten, sollten Sie zumindest ansatzweise überprüfen, was der Benutzer Ihnen schönes hat zukommen lassen. Die Validierung von Uploads kann in insgesamt vier Ansätzen vorgenommen werden: 1. Dateigröße 2. Dateiendung 3. MIME-Type 4. Dateiinhalt Während sich die ersten drei Ansätze ohne Schwierigkeiten in einer allgemein gültigen Weise überprüfen lassen, muss eine Validierung des Dateiinhalts individuell vorgenommen werden. Bei Grafiken ist es beispielsweise denkbar, diese mittels der Klasse Bitmap zu öffnen. Gelingt dies, handelt es sich um ein gültiges Bild, ansonsten eben nicht. In diesem Rezept möchte ich Ihnen vier neue Validation Controls vorstellen, die ich speziell zur Validierung von Benutzer-Uploads entwickelt habe. Die Controls decken die oben genannten Punkte allesamt ab. Um den Entwicklungsaufwand so gering wie nur eben möglich zu halten, habe ich eine neue Basisklasse angelegt, die sich von der ebenfalls abstrakten Basis BaseValidator ableitet. Alle vier neuen Validatoren leiten sich von der folgenden Klasse ab: public abstract class BaseFileValidator : BaseValidator {
182 ______________________________________ X.62 ... einen File Upload validieren?
protected HttpPostedFile GetPostedFile() { string cltId = this.ControlToValidate; HtmlInputFile ctl = (this.NamingContainer.FindControl(cltId) as HtmlInputFile); if(ctl != null) return(ctl.PostedFile); else return(null); } }
Das erste neue Validation dient zur Überprüfung der Dateigröße. Über die Eingenschaft MaxFileSize kann dabei die maximale Größe in Bytes angebeben werden. Die als protected markierte abstrakte Methode EvaluateIsValid wird implementiert und vergleicht die tatsächliche Größe mit der erlaubten. public class FileSizeValidator : BaseFileValidator { public long MaxFileSize { get { return((long) ViewState["MaxFileSize"]); } set { ViewState["MaxFileSize"] = value; } } protected override bool EvaluateIsValid() { HttpPostedFile file = this.GetPostedFile(); if(file != null) { return(file.ContentLength void bt_Click(object sender, EventArgs e) { if(this.IsValid) { lb.Text = "Alles OK, die Datei kann weiter verarbeitet werden ... :-)"; HttpPostedFile postedFile = upl.PostedFile; // ... } else lb.Text = string.Empty; } Upload:
X ASP.NET mit C# Kochbuch – Update ____________________________________ 185
In der Abbildung hat der Benutzer versucht, eine ZIP-Datei zu übertragen, die größer ist als das angegebene Limit. Die Validation Controls schlagen Alarm und lehnen den Upload kategorisch ab.
Abbildung X.84 Die Datei entspricht nicht den hinterlegten Vorgaben.
Beachten Sie in diesem Zusammenhang bitte auch die Rezepte „... die maximale Dateigröße für Uploads erhöhen?“ sowie „... den Timeout einer Seite verändern?“.
Validierung des Dateiinhalts War nicht weiter oben die Rede von vier neuen Validation Controls? Genau, eines habe ich Ihnen bisher vorenthalten. Die Validierung des Dateiinhaltes lässt sich leider nicht allgemein formulieren. Ich habe mich daher darauf beschränkt, ein Frameword für die Überprüfung zu erstellen. Konkret bietet das neue FileContentsValidator-Control ein Ereignis FileContentsValidate an, das über die Ereignisargumente Zugriff auf den übertragenen
186 ______________________________________ X.62 ... einen File Upload validieren?
Stream des Uploads bietet. Sie als Entwickler können den Inhalt nun individuell prüfen und ebenfalls über die Ereignisargumente zurückmelden, ob dieser korrekt ist. public delegate void FileContentsValidateEventHandler(object sender, FileContentsValidateEventArgs e); public class FileContentsValidateEventArgs : EventArgs { private Stream inputStream; private bool isValid; public FileContentsValidateEventArgs(Stream inputStream) { this.inputStream = inputStream; } public Stream InputStream { get { return(this.inputStream); } } public bool IsValid { get { return(this.isValid); } set { this.isValid = value; } } } public class FileContentsValidator : BaseFileValidator { public event FileContentsValidateEventHandler FileContentsValidate; protected override bool EvaluateIsValid() { HttpPostedFile file = this.GetPostedFile(); if(file != null) { FileContentsValidateEventArgs e = new FileContentsValidateEventArgs(file.InputStream); this.OnFileContentsValidate(e); return(e.IsValid); } return(true); } protected void OnFileContentsValidate( FileContentsValidateEventArgs e) {
X ASP.NET mit C# Kochbuch – Update ____________________________________ 187
if(FileContentsValidate != null) FileContentsValidate(this, e); } }
Auch zu diesem Control gibt es natürlich ein kleines Beispiel. Es basiert auf dem vorherigen und überprüft nun zusätzlich den Inhalt der übertragenen Datei. Dazu wird wie weiter oben beschrieben innerhalb eines try...catch-Blocks eine Instanz der Klasse Bitmap auf Basis des übertragen Stream angelegt. Sofern es hierbei zu einer Ausnahme kommt, ist die Datei nicht gültig. Listing X.102 ValidateUpload2.aspx ... void upl_FileContentsValidate(object sender, FileContentsValidateEventArgs e) { try { Bitmap b = new Bitmap(e.InputStream); b.Dispose(); e.IsValid = true; } catch { e.IsValid = false; } } ... ...
Der Aufruf des Beispiels beweist, dass nun auch vermeintlich korrekte Dateien als schlechte Fälschungen erkannt werden. Die Datei irgendwas.jpg enthält beispielsweise nur ein paar Textzeichen.
188 _________________________ X.63 ... mehrere Werte in einem Cookie abspeichern?
Abbildung X.85 Der Inhalt der übertragenen Datei ist nicht gültig.
X.63 ... mehrere Werte in einem Cookie abspeichern? In aller Regel werden Cookies zum Abspeichern eines einzigen Wertes verwendet. Doch das muss nicht sein, denn es durchaus auch möglich mehrere Werte in nur einem einzigen Keks abzulegen. Die Klasse HttpCookie bietet dazu über die Eigenschaft Values (mit s!) Zugriff auf eine NameValueCollection. Im folgenden Beispiel wird ein Keks Cookie, um die eingegebenen Benutzerdaten abzuspeichern und bei einem erneuten Aufruf der Seite wieder anzuzeigen. Damit der Cookie auch tatsächlich über die aktuelle Session hinaus persistent bleibt, muss die Eigenschaft Expires auf das gewünschte Ablaufdatum gesetzt werden. Im Beispiel ist der Cookie genau ein Jahr gültig. Listing X.103 CookieValues1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { if(!IsPostBack) { HttpCookie cookie = Request.Cookies["userdata"]; if(cookie != null) { tb_fn.Text = cookie.Values["firstname"]; tb_ln.Text = cookie.Values["lastname"]; tb_company.Text = cookie.Values["company"]; } }
X ASP.NET mit C# Kochbuch – Update ____________________________________ 189
} void bt_Click(object sender, EventArgs e) { HttpCookie cookie = new HttpCookie("userdata"); cookie.Values["firstname"] = tb_fn.Text; cookie.Values["lastname"] = tb_ln.Text; cookie.Values["company"] = tb_company.Text; cookie.Expires = DateTime.Now.AddYears(1); Response.Cookies.Add(cookie); } Vorname:
Nachname:
Firma:
Wann immer Sie neue Funktionalitäten implementieren, die auf Cookies basieren, ist der Trace-Modus von ASP.NET sehr hilfreich. Hier werden alle vom Client an den Server übertragenen Cookies samt Inhalt aufgelistet, auch dann wenn mehrere Werte darin abgelegt wurden. Der Trace-Modus lässt sich wie folgt über die @Page-Direktive einschalten:
Das Beispiel arbeitet bereits mit dem Trace-Modus und so lässt sich der neu angelegte Cookie in der Abbildung schnell wiederfinden.
190 _________________________________ X.64 ... einen temporären Cookie anlegen?
Abbildung X.86 Der Cookie wurde korrekt angelegt.
X.64 ... einen temporären Cookie anlegen? Bevor ich die Frage in der Überschrift beantworte, möchte ich klären, was ein temporärer Cookie überhaupt ist. Es handelt sich dabei um einen Cookie, der über keine festgelegte Ablaufzeit verfügt und vom Browser nicht persistent, sondern lediglich im Arbeitsspeicher abgelegt wird. Der Cookie geht also verloren, sobald der Browser geschlossen wird. Ein bekanntes Beispiel für einen derartigen Coookie liefert das ASP.NET SessionManagement. Hier wird ein Cookie mit dem Namen „ASP.NET_SessionId“ temporärer angelegt und und zur Identifikation des Besuchers genutzt. Die Beschreibung hat es im Prinzip schon verraten. Der Unterschied zwischen einem persistenten und einem temporären Cookie liegt in der fehlenden Angabe einer Ablaufzeit. Insofern müssen Sie lediglich die Zuweisung der Eigenschaft Expires unterlassen und schon wird der Cookie nicht mehr gespeichert. Listing X.104 TempCookie1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) {
X ASP.NET mit C# Kochbuch – Update ____________________________________ 191
if(!IsPostBack) { HttpCookie cookie = Request.Cookies["tempuserdata"]; if(cookie != null) { tb_fn.Text = cookie.Values["firstname"]; tb_ln.Text = cookie.Values["lastname"]; tb_company.Text = cookie.Values["company"]; } } } void bt_Click(object sender, EventArgs e) { HttpCookie cookie = new HttpCookie("tempuserdata"); cookie.Values["firstname"] = tb_fn.Text; cookie.Values["lastname"] = tb_ln.Text; cookie.Values["company"] = tb_company.Text; // --> cookie.Expires = DateTime.Now.AddYears(1); void Page_Load(object sender, EventArgs e) { if(!IsPostBack) { ... ddl_country.DataBind(); ddl_country.Items.Insert(0, new ListItem(" - Bitte auswählen - ", string.Empty)); } } Land:
X ASP.NET mit C# Kochbuch – Update ____________________________________ 193
Abbildung X.87 Die Auswahl des Benutzers wird erzwungen.
X.66 ... das Löschen eines Datensatzes im DataGrid bestätigen lassen? Das Rezept „... auf Click-Ereignisse in einem DataGrid-Control reagieren?“ hat gezeigt, wie Sie über eine zusätzliche Spalte einen im DataGrid angezeigten Datensatz löschen können. Doch ist es nicht etwas voreilig, diese Aktion so ganz ohne Benutzerrückfrage auszuführen? Ich finde schon! Im Listing finden Sie eine Möglichkeit, einen LinkButton einer ButtonColumn mit einer Sicherheitsabfrage zu verknüpfen. Dabei wird innerhalb des ItemCreatedEreignisses ein zusätzliches onClick-Attribut eingefügt, das die JavaScript-Funktion confirm zur Löschabfrage nutzt. Klickt der Benutzer auf OK, wird der PostBack und das damit verbundene Löschen des gewählten Datensatzes ausgeführt und ansonsten die Aktion abgebrochen. Listing X.106 ConfirmDelete1.aspx ... void dg_ItemCreated(object sender, DataGridItemEventArgs e) { if((e.Item.ItemType == ListItemType.Item) || (e.Item.ItemType == ListItemType.AlternatingItem)) { LinkButton btn = (LinkButton) e.Item.Cells[3].Controls[0]; btn.Attributes.Add("onClick", "return(confirm('Sind Sie sicher, dass Sie den Datensatz löschen wollen?'));"); } }
194 __________ X.66 ... das Löschen eines Datensatzes im DataGrid bestätigen lassen?
void dg_ItemCommand(object sender, DataGridCommandEventArgs e) { Response.Write("Gewählte Aktion: " + e.CommandName + "
"); int ID = (int) dg.DataKeys[e.Item.ItemIndex]; Response.Write("Datensatz-ID: " + ID.ToString() + "
"); /* switch(e.CommandName) { case "delete": // ... break; } */ ExecuteDataBinding(); } ...
Abbildung X.88 Das Löschen muss noch einmal bestätigt werden.
Das Beispiel basiert auf einem LinkButton-Control. Das Vorgehen lässt sich jedoch eins-zu-eins auf den alternativen (Push-)Button übertragen. Auf der begleitenden Buch-CD-ROM finden Sie zusätzlich ein entsprechend angepasstes Beispiel.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 195
Geht’s auch ohne JavaScript? Alternativ zu diesem Ansatz ist es auch möglich, eine serverseitige Sicherheitsabfrage zu integrieren, ähnlich wie dies im Rezept „... eine Sicherheitsabfrage ohne JavaScript anzeigen?“ vorgestellt wird. Das alternative Beispiel verwendet zwei Panel-Controls. Ein Klick auf den Löschen-Button schaltet die Ansicht um und speichert in diesem Zuge die ID des ausgewählten Datensatzes im ViewState. Der Benutzer hat nun die Möglichkeit, seine Entscheidung noch einmal zu überdenken. Erst wenn er die Operation bestätigt, wird der Datensatz tatsächlich gelöscht. Listing X.107 ConfirmDelete3.aspx ... void dg_ItemCommand(object sender, DataGridCommandEventArgs e) { int id = (int) dg.DataKeys[e.Item.ItemIndex]; switch(e.CommandName) { case "delete": ViewState["DeleteId"] = id; lt_ds.Text = string.Format("{0} {1}", e.Item.Cells[0].Text, e.Item.Cells[1].Text); pnl_form.Visible = false; pnl_confirmation.Visible = true; break; } } void btDelete_Click(object sender, EventArgs e) { int id = (int) ViewState["DeleteId"]; Response.Write(string.Format("Der Datensatz mit der ID {0} würde nun gelöscht.", id)); pnl_confirmation.Visible = false; pnl_form.Visible = true; } void btCancel_Click(object sender, EventArgs e) { pnl_confirmation.Visible = false; pnl_form.Visible = true; }
196 _____________________________ X.67 ... ein leeres DataGrid-Control ausblenden?
... Sicherheitsabfrage Sind Sie sicher, dass Sie den Datensatz löschen möchten?
Abbildung X.89 Die Sicherheitsabfrage kommt ohne clientseitiges JavaScript aus.
X.67 ... ein leeres DataGrid-Control ausblenden? DataGrid-Controls sehen schick aus, aber haben Sie schon einmal ein leeres Control gesehen? Das ist wirklich nicht sehr hübsch. Netter wäre es da doch, das gesamte Control auszublenden und stattdessen einen kurzen Hinweistext anzuzeigen, dass
X ASP.NET mit C# Kochbuch – Update ____________________________________ 197
keine Daten zur Verfügung stehen. Wieder einmal springt das Panel-Control in die Bresche. Das nachfolgende Beispiel basiert auf dem Rezept „... Daten in einem DataSet filtern?“ aus dem Datenbank-Kapitel. Es ermöglicht die Eingabe einer Filterbedingung, die angewandt auf ein DataSet eine gefilterte Ausgabe der zur Verfügung stehenden Daten in einem DataGrid-Control zur Folge hat. Ich habe das Beispiel um zwei Panel-Controls erweitert. Das eine enthält das DataGrid, das andere einen Hinweistext. Im Anschluß an die Datenbindung wird die Anzahl der zurück ehaltenen Elemente über die Items-Collection des DataGrid-Controls abgefragt und die Darstellung der Panel-Controls wechselseitig gesteuert. Listing X.108 HideDataGrid1.aspx <script language="C#" runat=server> void Page_Load(object sender, EventArgs e) { if(!IsPostBack) BindDataGrid(); } void BindDataGrid() { ... dg.DataSource = books; DataBind(); pnl_dg.Visible = (dg.Items.Count > 0); pnl_msg.Visible = (dg.Items.Count == 0); } void bt_click(object sender, EventArgs e) { BindDataGrid(); } Filter:
198 ____________________________ X.68 ... eine Kreditkartenabbuchung durchführen?
Die gewählte Filterbedingung hat keine Datensätze zurückgeliefert.
Abbildung X.90 Ein leeres DataGrid-Control wird ausgeblendet.
X.68 ... eine Kreditkartenabbuchung durchführen? In den vorangegangenen Rezepten haben Sie bereits einiges über die Theorie der Kreditkartenabbuchung erfahren. In diesem Rezept möchte ich Ihnen nun eine Möglichkeit aufzeigen, wie Sie direkt aus Ihrer Web-Applikation heraus Buchungen durchführen können.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 199
Auf der begleitenden CD-ROM finden Sie eine Klassenbibliothek in Form einer Visual Studio .NET Solution. Diese enthält die Implementierung eines möglichst allgemein gehaltenen Objektmodells zur Buchung von Kreditkartenzahlungen. Die Abbildung zeigt das Modell.
Abbildung X.91 Das iPayment-Objektmodell
An das Gateway wird eine Instanz der Klasse PaymentRequest übergeben. Diese enthält eine Collection mit allen Bestellungen, die wiederherum einzelne Transaktionen enthält. Das Gateway für die Buchung durch und liefert eine Instanz der Klasse PaymentResult zurück, die ihrerseits mehrere Instanzen der Klasse OrderResult sowie darunter TransactionResult in sicht vereint. Um die Realisierung nicht völlig theoretisch zu halten, habe ich zudem eine Gateway-Klasse implementiert, die die im Objektmodell enthaltenen Daten an die iPayment-Clearing-Schnittstelle von Schlund und Partner übermitteln kann. Derzeit unterstützt iPayment keine SOAP RPC-Aufrufe, also Web Services, wohl aber eine allgemeine SOAP-Schnittstelle. Die Daten daher in ein XML-Dokument umgewandelt und mittels SOAP an das Gateway übertragen. Das zurück erhaltenen Ergebnis hat ebenfalls eine XML-Form und wird von dort in das Objektmodell zurück gewandelt. Die übertragenen Daten sehen beispielsweise so aus:
200 ____________________________ X.68 ... eine Kreditkartenabbuchung durchführen?
cc xxxxx xxxxx xxxxx 1234 EUR My Comment ... <SIMULATIONMODE>TRUE 4242-4242-4242-4242 09/05 Ingo Fischer 123
Das nachfolgende Listing zeigt ein Beispiel, wie eine derartige Anforderung über das Objektmodell zu steuern ist. Der Quelltext basiert auf dem im vorherigen Rezept aufgebauten Formular und erweitert dies um die Eingabe eines EuroBetrages. Ein Klick auf den Button baut das benötigte Objektmodell auf und übergibt es an das Payment-Gateway. Listing X.109 creditcardform3.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { if(!IsPostBack) { for(int i=0; i void Page_Load(object sender, EventArgs e) { Response.Redirect("http://www.asp-buch.de", true); }
Abbildung X.97 Der Redirect wurde erfolgreich aufgelöst.
X.74 ... die Gültigkeit einer dauerhaften Forms Authentication einschränken? Die Forms Authentication von ASP.NET ist eine tolle Sache. Sie haben nicht nur die Möglichkeit, Benutzer zu authentifizierung und mittels Rollen individuell zu autorisieren. Darüber hinaus können Sie die Anmeldung des Benutzers über ein Authentication Ticket dauerhaft erhalten. Dieser Komfort ermöglicht es Ihnen, den
214 _______ X.74 ... die Gültigkeit einer dauerhaften Forms Authentication einschränken?
Besucher gleich wieder zu erkennen, ohne dass dieser sich erneut mit seinen Daten explizit anmelden muss. Bei der Entwicklung haben Sie die Wahl, ob die Anmeldung nur für aktuelle Sitzung oder dauerhaft freigeschaltet werden soll. Oftmals wird diese Entscheidung auch durch eine CheckBox an den Benutzer weiter gegeben, denn insbesondere bei öffentlich genutzten System ist eine dauerhafte Anmeldung absolut unerwünscht. Eine derartige Implementierung sieht beispielsweise so aus: FormsAuthentication.RedirectFromLoginPage(tb_username.Text, cb_persist.Checked);
Haben Sie sich einmal überlegt, was in diesem Zusammhang „persistent“ meint? Ich habe überprüft, wie lange der Cookie tatsächlich gültig ist und was soll ich sagen? Hätten Sie gedacht, dass eine dauerhafte Forms Authentication tatsächlich 50 Jahre verwendbar ist? Der Benutzer gibt heute seine Daten und im nächsten halben Jahrhundert nie wieder. Der folgende Ausschnitt aus einer Login-Seite beweist dies. Listing X.112 login2.aspx ... FormsAuthentication.SetAuthCookie(tb_username.Text, cb_persist.Checked); HttpCookie cookie = Response.Cookies[FormsAuthentication.FormsCookieName]; Response.Write(cookie.Expires.ToString("d")); Response.End(); ...
Abbildung X.98 Der Cookie ist bis ins Jahr 2052 gültig.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 215
Der Zeitrahmen sprengt zwar aus einfachem Grund die Lebensdauer jedes bis dato dagewesenen Online-Angebotes, macht aber auf eine ernst zu nehmende Sicherheitslücke aufmerksam. Es bietet sich daher an, die Persitenz auf eine kürzere Zeitdauer einzuschränken, beispielsweise einen Monat. Da das Framework hierzu keine Möglichkeit bietet, müssen Sie selbst Hand anlegen. Wie im vorherigen Beispiel gezeigt, können Sie den vom Framework angelegten Authentication Cookie abfragen und dessen Eigenschaft Expires auslesen. Sie können allerdings auch schreibend auf diese Eigenschaft zugreifen und so die Lebensdauer des Cookies festlegen. Im leicht abgeänderten Listing wird genau dies durchgeführt. Um im Anschluss die tatsächlich gewünschte anzuzeigen, wird zuvor deren Adresse abgefragt und in der Folge ein regulärer Redirect durchgeführt. Listing X.113 login.aspx ... string url = FormsAuthentication.GetRedirectUrl(tb_username.Text, cb_persist.Checked); FormsAuthentication.SetAuthCookie(tb_username.Text, cb_persist.Checked); HttpCookie cookie = Response.Cookies[FormsAuthentication.FormsCookieName]; cookie.Expires = DateTime.Now.AddMonths(1); Response.Redirect(url); ...
Durch diesen simplen Eingriff ist sicher gestellt, dass der persistente Cookie genaz einen Monat gültig ist, bevor sich der Benzutzer erneut anmelden muss. Sie können den Zeitraum selbstverständlich vollkommen frei wählen. Als Ergänzung ist es denkbar, den Cookie bei jedem Besuch des Benutzers automatisch zu erneuern. Dadurch wird sicher gestellt, dass der Benutzer sich neu anmelden, sofern er die Seite nicht regelmäßig benutzt. Nehmen Sie dazu den folgenden Code in die Ereignisbehandlung Page_Load Ihrer Hauptseite des geschützten Bereiches auf. Listing X.114 default.aspx void Page_Load(object sender, EventArgs e) { ... if(identity.IsAuthenticated) { FormsAuthentication.SetAuthCookie(Identity.Name, true);
216 _______________________ X.75 ... den letzten Besuch eines Benutzers speichern?
HttpCookie cookie = Response.Cookies[FormsAuthentication.FormsCookieName]; cookie.Expires = DateTime.Now.AddMonths(1); } }
Alternativ
bietet
sich
eine
entsprechende
Behandlung
des
Ereignisses
Application_AuthenticateRequest innerhalb der Datei global.asax an.
X.75 ... den letzten Besuch eines Benutzers speichern? Auf den ersten Blick erscheint es recht einfach, die Besuche eines Benutzers zu protokollieren und so jederzeit feststellen zu können, wann dieser Ihre Website zuletzt aufgerufen hat. Innerhalb einer Login-Funktionalität müssen Sie lediglich ein entsprechendes Datenbank-Flag aktualisieren. In Verbindung mit einer persistenten Forms Authentication ist dies allerdings nicht mehr ganz so einfach zu lösen. Die Anmeldung wird hier nur einmal explizit durchlaufen, spätere Anmeldungen erfolgen implizit über das als dauerhafter Cookie gespeicherte Authentication Ticket. Des Rätsels Lösung liegt im Ereignis Application_AuthenticateRequest in der global.asax Ihrer Web-Applikation. Insbesondere in Verbindung mit einem temporären Cookie der rollenbasierten Forms Authentication können Sie hier das entsprechende Datenbank-Flag setzen, sobald der Besucher eine neue Sitzung erzeugt. Zur Demonstration dieses Ansatzes habe ich das Beispiel aus dem Rezpet „… eigene Rollen/Gruppen mit Forms Authentication verwenden?“ erweitert. Zunächst mussten drei neue Datenbankfelder her. Die Abbildung zeigt die Struktur der Tabelle Users der gleichnamigen Datenbank.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 217
Abbildung X.99 Der erste und letzte Login sowie die Anzahl der Logins werden gespeichert.
Im zweiten Schritt wird die Methode CreateNewRoleCookie innerhalb der global.asax erweitert. Diese wird aufgerufen, sobald der bereits authentifizierte Benutzer die Website „betritt“ und die zugehörigen Rollen einmalig für die aktuelle Sitzung aus der Datenbank gelesen werden sollen. Listing X.115 global.asax <script runat="server"> ... private void CreateNewRoleCookie() { ... cmd = new OleDbCommand(); cmd.Connection = conn; cmd.CommandText = "UPDATE Users SET LoginCount = LoginCount + 1, LastLoginOn = Now WHERE Username=@Username"; cmd.Parameters.Add("@Username", this.User.Identity.Name); cmd.ExecuteNonQuery(); conn.Close(); }
Ohne dass Besucher etwas hiervon erfahren würde, werden sämtliche Besuche protokolliert. In Verbindung mit einem regelmäßigen Job (zum Beispiel im SQL Server) können Sie einen länger nicht angemeldeten Besucher beispielsweise auto-
218 _______________________________ X.76 ... anonyme Besucher wieder erkennen?
matisch an Ihre Website und die seit dem letzten Besuch hinzugefügten Inhalt erinnern – ein netter Service.
X.76 ... anonyme Besucher wieder erkennen? Über die Forms Authentication steht Ihnen ein flexibles System zur Verfügung, Benutzer zu authentifizieren und deren Zugriffe zu autorisieren. Dies setzt jedoch immer eine explizite Anmeldung des Benutzers voraus. Oftmals sind die Anforderungen jedoch wesentlich einfacher und ein Besucher soll lediglich wieder erkannt und die zugehörigen Einstellungen geladen werden. Ein derartiges System kann beispielsweise bei der Personalisierung von Websites genutzt werden. Der Ansatz des Beispiels ist die Erstellung einer eindeutigen ID für den anonymen Benutzer mit Hilfe der Struktur Guid. Diese ID wird überall dort verwendet, wo sonst der authentifizierte Benutzername benötigt würde. Das Beispiel verwendet die angepasste Datenbank users.mdb aus dem Rezept „... den letzten Besuch eines Benutzers speichern?“. Die Kernfunktionalität des Beispiel findet sich in der Ereignisbehandlung Application_PreRequestHandlerExecute der global.asax. Da ein Zugriff auf
das Session-Management notwendig ist, kommt nur dieses Ereignis in Frage. Hier wird zunächst abgefragt, ob die ID des Benutzers bereits in der Session vorliegt. Ist dies nicht Fall, wird überprüft, ob ein entsprechender Cookie vorhanden ist. Ist auch dies nicht gegeben, so wird ein neuer Cookie mit einer GUID sowie ein zugehöriger Datensatz in der Benutzertabelle angelegt. Diesem Datensatz lassen sich später alle gewünschten Informationen zuweisen beziehungsweise relational verknüpfen. Listing X.116 global.asax <script runat="server"> void Application_PreRequestHandlerExecute(object sender, EventArgs e) { if(Session["anonymousid"] == null) { OleDbConnection conn = new OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;" + @"Data Source=c:\inetpub\wwwroot\aspnet\users.mdb"); conn.Open(); OleDbCommand cmd; HttpCookie cookie = Request.Cookies["anonymousid"]; if(cookie == null) {
X ASP.NET mit C# Kochbuch – Update ____________________________________ 219
string anonymousId = Guid.NewGuid().ToString(); cookie = new HttpCookie("anonymousid"); cookie.Value = anonymousId; cookie.Expires = DateTime.Now.AddYears(1); Response.Cookies.Add(cookie); cmd = new OleDbCommand(); cmd.Connection = conn; cmd.CommandText = "INSERT INTO Users (Username, LoginCount, LastLoginOn) VALUES (@Username, 1, Now);"; cmd.Parameters.Add("@Username", anonymousId); cmd.ExecuteNonQuery(); Session["anonymousid"] = anonymousId; } else { Session["anonymousid"] = cookie.Value; cmd = new OleDbCommand(); cmd.Connection = conn; cmd.CommandText = "UPDATE Users SET LastLoginOn = NOW, LoginCount = LoginCount + 1 WHERE Username=@Username;"; cmd.Parameters.Add("@Username", Session["anonymousid"]); cmd.ExecuteNonQuery(); } conn.Close(); } }
Um diesen Ansatz zu überprüfen habe ich wie immer ein zusätzliches Beispiel angefügt. Die ASP.NET-Seite fragt die ID des anonymen Benutzers aus der Session-Variable ab, gibt diese aus und ermittelt auch die Anzahl der bisherigen Besuche. Listing X.117 AnonymousUser1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { OleDbConnection conn = new OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;" + @"Data Source=c:\inetpub\wwwroot\aspnet\users.mdb"); conn.Open();
220 _______________________________ X.77 ... das Ereignis Session_End benutzen?
OleDbCommand cmd = new OleDbCommand(); cmd.Connection = conn; cmd.CommandText = "SELECT LoginCount FROM Users WHERE Username=@Username;"; cmd.Parameters.Add("@Username", Session["anonymousid"]); Response.Write(string.Format("Id: {0}
", Session["anonymousid"])); Response.Write(string.Format("Dies ist Ihr {0}. Besuch
", cmd.ExecuteScalar())); conn.Close(); }
Abbildung X.100 Der anonyme Besucher wird wieder erkannt.
Der Gag dieses Systems ist in der Abbildung zu erkennen. Der Benutzer wurd wieder erkannt. Über den zugeordneten Datensatz wurde ermittelt, dass es sich um den dritten Besuch handelt. Auf diese Weise lassen sich Einstellungen und Personalisierungen speichern, ohne eine reale Anmeldung des Benutzers zu erzwingen. Wenn Sie die ID wie im Beispiel gezeigt in das reguläre BenutzerManagement und die damit verbundenen Datenbanken vornehmen, können Sie den anonymen Account später jederzeit in einen personalisierten umwandeln. Denn – und das ist der Nachteil des Systems – wenn sich der Benutzer von einem anderen Rechner einklingt oder gar seine Cookies löscht, hat er keinen Zugriff auf seine Einstellungen.
X.77 ... das Ereignis Session_End benutzen? Das Ereignis Session_End wird ausgelöst, sobald eine Sitzung mit Daten im Session-Scope per Timeout abgelaufen ist oder die Methode Session.Abandon aufgeru-
X ASP.NET mit C# Kochbuch – Update ____________________________________ 221
fen wurde. Das Ereignis dient primär der Kompatibilität zu klassischen ASP und sollte daher nicht weiter verwendet werden. Vielmehr sollten Sie alternative Möglichkeiten suchen, die individuellen Anforderungen zu realisieren. So kann das oft genutzte Löschen von temporären Dateien beispielsweise zeitgesteuert oder auch beim Start der Applikation vorgenommen werden. Problematisch ist Verwendung durch die fehlende durchgängige Unterstützung. So wird das Ereignis nur dann ausgelöst, wenn Sie in der web.config als Speicherort InProc angegeben haben. Bei der Speicherung im Session State Service oder dem SQL kann das Ereignis nicht genutzt werden, da es bedingt durch die Architektur nicht sinnvoll ausgelöst werden kann. Sofern das Ereignis trotz Verwendung von InProc nicht ausgelöst wird, liegt die Ursache zumeist in einer Exception innerhalb der Ereignisbehandlung. Durch den Zeitpunkt des Auslösens wird diese nicht im Browser angezeigt. In diesem sollten Sie den Quelltext unabhängig in einer Seite überprüfen und dabei insbesondere ein Augenmerk auf die eventuell enthaltenen SQL Abfragen werfen. Derartige Fehler können vom Kompiler nicht erkannt werden und resultieren daher typischerweise in Laufzeitfehlern.
SQL Server - wie es doch geht Sofern Sie den SQL Server zum Speichern Ihrer Session-Daten nutzen, wird das Ereignis Session_End, wie eben beschrieben, nicht ausgeführt. Das komplette Management der Daten erfolgt innerhalb des (zentralen) Datenbank-Servers und erlaubt daher keinen Rückgriff auf die Funktionalität in der global.asax. Intern wird das Löschen der Session-Daten über einen Job gelöst, der jede Minute die Stored Procedure DeleteExpiredSessions aufruft. Diese sieht in ihrem initiellen Zustand so aus: CREATE PROCEDURE DeleteExpiredSessions AS DECLARE @now DATETIME SET @now = GETDATE() DELETE tempdb..ASPStateTempSessions WHERE Expires < @now RETURN 0 GO
Rein theoretisch können Sie eventuell notwendige Abschlussarbeiten in dieser Stored Procedure ergänzen oder auch in den minütlichen Job integrieren. Im Zuge der
222 ______________ X.78 ... Bemerkungsfelder in einem DataGrid mehrzeilig darstellen?
neuen SQL Server Version (Codename „Yukon“), die Ende 2003 bis Anfang 2004 erscheinen soll, werden die Möglichkeiten stark erweitert. Dann wird das Schreiben von Stored Procedures über die CLR in jeder .NET-Sprache möglich sein. Ob dies allerdings Segen oder – durch die daraus entstehende Auslagerung der Geschäftslogik in die Datenbank – Fluch ist, mag jeder für sich selbst entscheiden.
X.78 ... Bemerkungsfelder in einem DataGrid mehrzeilig darstellen? Neben vielen anderen Daten werden auch mehrzeilige Bemerkungsfelder in einem DataGrid-Control ausgegeben. Allerdings werden die enthaltenen Umbrüche nicht auf das HTML-Format transferiert, so dass der Text ohne Umbruch hinter einander läuft. Um dies umgehen, lässt sich das Ereignis ItemDataBound des DataGridControls nutzen. Hier muss der Text lediglich wie im Rezept „... ein Memo-Feld im Browser darstellen?“ beschrieben ersetzt werden. Das Listing zeigt, wie’s geht. Listing X.118 MultiLineText1.aspx <script language="C#" runat=server> void Page_Load(object sender, EventArgs e) { if(!IsPostBack) { ... } } void dg_ItemDataBound(object sender, DataGridItemEventArgs e) { if((e.Item.ItemType == ListItemType.Item) || (e.Item.ItemType == ListItemType.AlternatingItem)) { string text = string.Format("{0}
", e.Item.Cells[4].Text); text = text.Replace("\r\n\r\n", ""); text = text.Replace("\r\n", "
"); e.Item.Cells[4].Text = text; } }
Abbildung X.101 Der Text wird nach dem Eingriff mehrzeilig dargestellt.
X.79 ... nach dem PostBack eine bestimmte Position anspringen? Aus dem Konzept von ASP.NET und dem damit verbundenen HTML ergibt sich die Schwierigkeit, dass nach dem Round Trip zum Server die aufgerufene Seite in der Ausgangsposition angezeigt wird. Gerade bei längeren Seiten ist dies ärgerlich, da der Benutzer nach jedem PostBack erneut an die gewünschte Position navigieren muss. SmartNavigation ist ein Ansatz, dieses Problem zu verhindern, allerdings lässt sich die gewünschte Position hierbei nicht programmatisch festlegen. Ein anderer Ansatz ist das Control ScrollInView, das wie jedes andere Server Control statisch platziert oder programmatisch eingefügt werden kann. Wo immer das unsichtbare Objekt auch angezeigt wird, es bringt den Browser nach dem PostBack automatisch an diese Position. Hinter dem Control steht die JavaScript-Funktion scrollIntoView, die derzeit leider noch nicht vom W3C abgesegnet und ausschließlich im Internet Explorer verfügbar ist. Das Control gibt ein span-Tag im Browser aus und sorgt über den
224 _______________ X.79 ... nach dem PostBack eine bestimmte Position anspringen?
eingefügten JavaScript-Abschnitt, dass dieses beim Laden der Seite in den sichtbaren Bereich gebracht wird. Listing X.119 ScrollInView.cs using System; using System.Text; using System.Web.UI; namespace PAL.AspNetKochbuch { public class ScrollInView : Control { protected override void Render(HtmlTextWriter writer) { writer.AddAttribute(HtmlTextWriterAttribute.Id, "siv"); writer.RenderBeginTag(HtmlTextWriterTag.Span); writer.RenderEndTag(); StringBuilder script = new StringBuilder(); script.Append("<script language=\"javascript\">"); script.Append("document.all.siv.scrollIntoView();"); script.Append(""); writer.WriteLine(script.ToString()); } } }
Ein kleines Beispiel demonstriert die programmatische Verwendung des Controls. In einem Formular wird einem Literal-Control dynamisch Text angefügt. Anschließend wird eine neue Instanz des ScrollInView-Controls am Ende der Controls-Collection eingefügt. Listing X.120 scrollinview1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { for(int i=0; i
Blindtext Blindtext Blindtext Blindtext Blindtext Blindtext Blindtext ... Blindtext Blindtext Blindtext Blindtext Blindtext Blindtext Blindtext
bla ...
Blindtext Blindtext Blindtext Blindtext Blindtext Blindtext Blindtext ... Blindtext Blindtext Blindtext Blindtext Blindtext Blindtext Blindtext
226 ___________________________ X.80 ... ein DataGrid in zwei Richtungen sortieren?
Abbildung X.103 Auch im Layout-Bereich lässt sich das Control einsetzen.
Durch die Integration in die bestehende Konzepte von ASP.NET können Sie das hier beschriebene Control sehr flexibel einsetzen. Wenn Sie beispielsweise ein bestimmtes Element in einem DataGrid anvisivieren wollen, fügen Sie das Control einfach dynamisch im ItemCreated-Ereignis hinzu.
X.80 ... ein DataGrid in zwei Richtungen sortieren? Sie kennen das bestimmt von der Listenansicht im Windows Explorer und anderen Programmen. Klickt man den Kopf eine Spalte an, so wird der Inhalt alphabetisch nach dieser Spalte sortiert. Klickt man den Kopf ein zweites Mal an, so erfolgt die Sortierung umgekehrt alphabetisch. Geht das auch mit dem DataGrid-Control von ASP.NET? Na klar! Das folgende Beispiel erweitert ein Listing aus dem Rezept „... ein DataGrid sortieren?“. Gezeigt ist die Behandlung des Sort-Ereignisses. Hier wird neben dem neuen Sortierausdruck auch der bisherige abgefragt, der im ViewState der Seite hinterlegt wurde. Entsprechen sich beide Werte, so hat der Benutzer die bereits Spalte noch einmal angeklickt. In diesem Fall wird der Ausdruck um ein „DESC“ erweitert und die Sortierung so umgekehrt. Listing X.122 Sort5.aspx ... void dg_Sort(object sender, DataGridSortCommandEventArgs e) { string newExpression = e.SortExpression; string oldExpression = (ViewState["SortExpression"] as string); if((oldExpression != null) && (oldExpression.Equals(newExpression))) { newExpression += " DESC";
X ASP.NET mit C# Kochbuch – Update ____________________________________ 227
} ViewState["SortExpression"] = newExpression; BindData(newExpression); } ...
Abbildung X.104 Die Sortierung ist in beide Richtungen möglich.
X.81 ... Windows Authentication und Impersonation differenzieren? Die Windows Authentication wird oftmals mit der Impersonation verwechselt und gar mit dieser gleichgesetzt. Es handelt sich dabei jedoch um zwei prinzipiell verschiedene Techniken, die sich nur „zufällig“ miteinander kombinieren lassen. Ich möchte daher die Gelegenheit nutzen, beide Techniken zu differenzieren. Ein kurzer Auflug ... Unter Windows 2000 und Windows XP mit installiertem IIS 5.0 beziehungsweise 5.1 wird ASP.NET in zwei unterschiedlichen Prozessen abgearbeitet. Auf der einen Seite steht der IIS-Prozess inetinfo.exe, der unter dem Benutzer-Account SYS-
228 ______________ X.81 ... Windows Authentication und Impersonation differenzieren?
TEM läuft. Der Prozess nimmt während der Abarbeitung der einzelnen Anfragen jedoch einen anderen Benutzer-Token an, nämlich den im IIS konfigurierten IUSR. ASP.NET selbst läuft in einem eigenen Prozess, dem so genannten ASP.NET Worker Process aspnet_wp.exe. Dieser Prozess verwendet standardmäßig den Benutzer-Account ASPNET. Impersonation sorgt dafür, dass der ASP.NET-Prozess (respektive die entsprechende AppDomain) während der Abarbeitung einer Anfrage einen alternativen Benutzer-Token annimmt, dessen Rechte für diese Anforderung gelten sollen. Sofern Sie Impersonation in der web.config aktivieren und dabei keinen Benutzer-Account explizit angeben, wird der vom IIS-Prozess gelieferte BenutzerToken übernommen. Im Regelfall handelt es sich dabei um den erwähnten IUSR. Für den Fall, dass Sie im IIS Windows Authentication aktiviert haben, übergibt der IIS den authentifizierten Benutzer-Token an die ASP.NET-Engine anstelle des IUSR. Das bedeutet, dass die Rechte dieses Benutzers aber nur dann übernommen werden, wenn Impersonation aktiviert ist. Tabelle X.3 Die Kombinationen von Authentication und Impersonation ASP.NET
Ohne Impersonation
Statische Impersonation
Dynamische Impersonation
Anonyme Anmeldung
ASPNET
Angegebener Benutzer
IUSR
Authentifizierter Zugriff (Windows Authentication)
ASPNET
Angegebener Benutzer
Authentifizierter Benutzer
IIS
Die Tabelle zeigt die verschiedenen Kombinationen von Impersonation und Windows Authentication. Letztere lässt sich aber auch problemlos autark ohne Impersonation nutzen. In diesem Fall erfolgt die Authentifizierung im IIS wie gewünscht über den Windows Benutzernamen. Dessen Rechte werden jedoch nicht übernommen, allerdings kann der Benutzer über User.Identity.Name abgefragt werden. Dies gilt auch für die zugeordneten Rollen, die im nachfolgenden Rezept Thema sind. Die verschiedenen Kombinationen lassen sich recht einfach mit dem folgenden Beispiel überprüfen. Es werden die Server-Variable AUTH_USER der IIS sowie der Name der aktuellen ASP.NET-Identität ausgegeben: Listing X.123 AuthUser1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) {
X ASP.NET mit C# Kochbuch – Update ____________________________________ 229
Response.Write(string.Format("Identity: {0}
", User.Identity.Name)); Response.Write(string.Format("Auth_User: {0}
", Request.ServerVariables["AUTH_USER"])); }
X.82 ... Windows Benutzerrollen zur Personalisierung verwenden? Das vorherige Rezept hat noch einmal die Unterschiede zwischen Windows Authentication und Impersonation gezeigt. Als Fazit lässt sich ziehen, dass die Authentifizierung auch ohne die Impersonation möglich und sinnvoll ist. Ein derartiger Ansatz ist die Personalisierung von Web-Seiten auf Basis der Rollen eines Windows Benutzer-Accounts. Um das folgende Beispiel nachvollziehen zu können, müssen Sie in der Konfiguration der IIS den anonymen Zugriff auf das verwendete Applikationsverzeichnis deaktivieren sowie die Standardauthentifizierung aktivieren.
Abbildung X.105 Der anonyme Zugriff muss ausgeschaltet sein.
Ist der anonyme Zugriff deaktiviert, werden beim Zugriff der Benutzername samt Passwort abgefragt. Der so authentifizierte Benutzer steht innerhalb des Objekts-
230 _______________ X.82 ... Windows Benutzerrollen zur Personalisierung verwenden?
modells über die Eigenschaft User zur Verfügung. Geliefert wird hier eine Instanz der Klasse WindowsPrincipal, die die Schnittstelle IPrincipal unterstützt. Die Klasse implementiert mehrere Überladungen der Methode IsInRole, über die ermitteln können, ob ein Benutzer einer bestimmten Rolle oder auch Gruppe angehört. Die in das Betriebssystem eingebauten Gruppen werde über die Enumeration WindowsBuiltInRole geliefert, für die auch eine Überladung existiert. Das Beispiel realisiert eine personalisierte Seite auf Basis dieser eingebauten Benutzergruppen. Je nach dem, welcher oder welchen Gruppen der Benutzer angehört, werden unterschiedliche Bereich freigegeben. Dazu wird der Eigenschaft Visible mehrerer Panel-Controls der Rückgabewert der Methode IsInRole zugewiesen. Listing X.124 WindowsAuthentication.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { DataBind(); } WindowsPrincipal Principal { get { return((WindowsPrincipal) this.User); } } Benutzername:
Diesen Bereich können alle Benutzer benutzen.
Dieser Bereich ist exklusiv für Administratoren.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 231
Gäste können nur diesen Bereich nutzen.
Das
Beispiel
enthält
eine
Eigenschaft
Principal,
die
den
aktuellen
WindowsPrincipal liefert. Im Prinzip ist diese Eigenschaft unnötig, allerdings
spart sie die sonst notwendige Typenkonvertierung bei jedem Zugriff auf die Eigenschaft Page.User, da diese als IPrincipal deklariert ist.
Abbildung X.106 TestAccount ist Mitglied der Benutzergruppe.
X.83 ... einen Leeraum nach Interpunktionszeichen erzwingen? Über reguläre Ausdrücke lassen sich so manche Fehleingaben von Benutzern bereinigen. Das Buch hat hierzu bereits einige Beispiele vorgestellt. Dieses Rezept zeigt eine weitere Möglichkeit. Mittels Regex werden eventuell fehlende Leerzeichen nach Punkt und Komma eingefügt. Deren Fehlen kann insbesondere bei der Ausgabe der Daten zu Anzeigefehlern führen, da der Browser keinen automatischen Zeilenumbruch einfügen kann. Konkret wird die statische Methode Regex.Replace benutzt. Jeder Punkt und jedes Komma, das direkt von einem Wortzeichen gefollt wird, wird durch dieses Interpunktionszeichen, einen Leerraum sowie das anschließende Wortzeichen ersetzt. Listing X.125 punction1.aspx <script runat="server"> void Bt_Click(object sender, EventArgs e) { lt.Text = this.ForceTextSpaces(tb.Text); }
232 _________________ X.83 ... einen Leeraum nach Interpunktionszeichen erzwingen?
public string ForceTextSpaces(string input) { return(Regex.Replace(input, @"([,|\.])(\S)", "$1 $2")); }
Abbildung X.107 Per regulären Ausdruck werden Leerzeichen erzwungen.