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 <% @WebHandler Language="C#" class="Adjust1" %> 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 <= maxSize.Height) { w = maxSize.Width; h = ((float) maxSize.Width) / bitmap.Size.Width * bitmap.Height; } else { w = ((float) maxSize.Height) / bitmap.Size.Height * bitmap.Width; h = maxSize.Height; } return((Bitmap) bitmap.GetThumbnailImage((int) w, (int) h, null, IntPtr.Zero)); } }
Das Beispiel ist als Web Handler realisiert und erwartet die Angabe des gewünschten Dateinamens im QueryString.
Abbildung X.75 Die Grafik wurde in die Maximalgröße eingepasst.
166 ______ X.56 ... Grafiken proportional in eine vorgegebene Maximalgröße einpassen?
In vielen Fällen bietet es sich an, das Bild tatsächlich in der angeforderten Größe zu liefern und es dazu gegebenenfalls in ein zweite mit spezilell dieser Größe angelegte Grafik zu kopieren. Hierzu muss lediglich das neue Bild angelegt und das bestehende mittels der Methode Graphics.DrawImage einfügt werden. Über eine Instanz der Struktur Point wird zudem sichergestellt, dass die Grafik zentriert dargestellt wird. Listing X.95 adjust2.ashx ... protected Bitmap AdjustBitmap(Bitmap bitmap, Size maxSize) { float w, h; if(((float) maxSize.Width) / bitmap.Size.Width * bitmap.Size.Height <= maxSize.Height) { w = maxSize.Width; h = ((float) maxSize.Width) / bitmap.Size.Width * bitmap.Height; } else { w = ((float) maxSize.Height) / bitmap.Size.Height * bitmap.Width; h = maxSize.Height; } Bitmap newBitmap = new Bitmap(maxSize.Width, maxSize.Height); Graphics g = Graphics.FromImage(newBitmap); g.Clear(Color.Red); Point point = new Point((int) (maxSize.Width - w) / 2, (int) (maxSize.Height - h) / 2); g.DrawImage(new Bitmap(bitmap.GetThumbnailImage((int) w, (int) h, null, IntPtr.Zero)), point); return(newBitmap); } }
X ASP.NET mit C# Kochbuch – Update ____________________________________ 167
Abbildung X.76 Das Bild wird in der vorgegebenen Größe geliefert.
X.57 ... den Inhalt einer TextBox rechtsbündig oder zentriert ausrichten? Wenngleich das TextBox-Control keine derartige Eigenschaft anbietet, so ist es doch mittels Cascacing Stylesheets problemlos möglich, den Inhalt eines Eingabefeldes anders im Normalfall rechts oder zentriert auszurichten. Verwenden Sie einfach die Eigenschaft Style in Verbindunt mit dem Attribut text-align. Listing X.96 textalign1.aspx
168 __________________________ X.58 ... null für float- oder double-Werte übergeben?
Abbildung X.77 Die Ausrichtung der Texteingaben kann individuell festgelegt werden.
Das CCS-Attribut text-align kann übrigens Eingabefeldern ohne Einschränkungen benutzt werden.
auch
bei
mehrzeiligen
X.58 ... null für float- oder double-Werte übergeben? Wenn Sie die Überschrift dieses Rezeptes gelesen haben, dann werden Sie möglicherweise ein wenig irritiert auf das null geschaut haben. Kein Wunder, denn null ist schließlich ein Verweis und die nummerischen Typen float und double sind Wertetypen. Und in der Tat, null lässt sich in keinem Fall in Verbindung mit diesen oder anderen Wertetypen nutzen. Es gibt allerdings eine andere Möglichkeit, einen ungültigen oder nicht vorhandenen Wert zu kennzeichnen, beispielsweise bei der Übergabe von Parametern. Beide Datentypen kennen ein statisches Feld (const) NaN, was „Not a Number“ steht, also einen ungültigen nummerischen Wert. Das Listing zeigt die Verwendung von NaN. Dabei erwartet eine Methode die Übergabe eines double-Wertes. Um den Parameter als nicht vorhanden oder ungültig zu kennzeichnen, wird NaN übergeben. Listing X.97 NaN1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { this.DoSomething(double.NaN); } void DoSomething(double value) { if(!double.IsNaN(value)) { // ... }
X ASP.NET mit C# Kochbuch – Update ____________________________________ 169
}
Ein wichtiges Einsatzgebiet von NaN ergibt sich in Verbindung mit der Speicherung von Daten in eine Datenbank. Wird hier ein ungültiger Wert geliefert, wird meist in der Datenbank NULL abgelegt. Eine Methode zur Überprüfung könnte beispielsweise so aussehen: object CheckDoubleForStoringInDB(double value) { if(double.IsNaN(value)) return(value); else return(DBNull.Value); }
Beachten Sie bitte unbedingt, dass ein Vergleich von zwei doubleWerten auf Basis von NaN nicht möglich ist. Vielmehr müssen Sie immer die statische Methode IsNaN benutzen. Dies gilt selbstverständlich auch für den Datentyp float.
X.59 ... ein Bankleitzahlverzeichnis integrieren? Während die Kreditkarte weltweit das Zahlungsmittel Nummer 1 im Internet ist, hat hierzulande die Akzeptanz von Zahlungen per Lastschrift stark zugenommen. Es tummeln sich mittlerweile eine ganze Reihe von Anbietern im Internet, die Shops eine Integration ermöglichen. Davon ab kann jeder Kontoinhaber Beträge von anderen Konten mittels Lastschrift einziehen. Dieser Weg bietet sich insbesondere für kleine Unternehmen mit einer guten Kundenbindung an, bei denen der Anteil missbräuchlicher Bestellungen gering ist oder das Produkt respektive dessen Lieferung einen Missbrauch ausschließen. Um den Kunden im Internet eine komfortable Eingabe Ihrer Daten zu ermöglichen und gleichwohl eine gewisse Validierung dieser vornehmen zu können, bietet sich die Integration eines Bankleitzahlverzeichnisses an. Die hierzu benötigen Daten werden von der Bundesbank in Form einer quartalsweise aktualisierten Textdatei zum Download angeboten: www.bundesbank.de
170 ______________________________ X.59 ... ein Bankleitzahlverzeichnis integrieren?
Abbildung X.78 Mittels DTS lassen sich die Daten importieren.
Die Textdatei besteht aus insgesamt 16 Spalten mit einer festen Breite. So besteht die Bankleitzahl beispielsweise immer aus acht Ziffern. Die Spalten sind im, der Textdatei beiliegenden, Word-Dokument ausführlich beschrieben. Zum Import bieten sich die Data Transformation Services (DTS) des SQL Servers an. Über das Import-/Export-Programm können Sie die Daten sehr komfortabel in den SQL Server aber auch andere Datenbanken wie Access übernehmen. Auf der begleitenden Buch-CD-ROM finden Sie eine Access-Datenbank mit den wichtigsten Spalten. Es handelt sich um den zur Drucklegung aktuellen Stand vom September 2002. Es empfiehlt sich, die Daten kurfristig nach dem Erscheinen der neuen Daten zu aktualisieren, da neue Bankleitzahlen hinzukommen und bestehende gelöscht oder geändert werden können. Die Abbildung zeigt einen Ausschnitt der Datenbank, die insgesamt gut 21.000 Einträge umfasst. Einzelne Bankleitzahlen können dabei mehrfach vorkommen, da diese häufig mehreren Filialen eines Instituts zugeordnet sind.
Abbildung X.79 Die Bankleitzahl der Bundesbank lässt sich sehr einfach merken.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 171
Auf Basis der Datenbank lässt sich recht einfach ein Eingabeformular entwickeln. Dieses besteht im ersten Schritt aus einem Eingabefeld für die Bankleitzahl sowie einem ListBox-Control. Dieses wird angezeigt, sofern die Eingabe des Benutzers nicht eindeutig ist, also die Bankleitzahl auf mehrere Institute zutrifft. Das nachfolgende Listing enthält ein derartiges Formular. Die Steuerung erfolgt hauptsächlich über ein CustomValidator-Control. Dieses fragt aus einer Datenbank alle Einträge für die eingegebene Bankleitzahl ab. Werden Einträge gefunden, ist die Eingabe ungültig. Bei einem wird diese direkt akzeptiert, bei mehr als einem Treffer muss der Benutzer das korrekte Institut in einem Schritt auswählen. Listing X.98 bankcodes1.aspx <script runat="server"> 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)); }
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 <= 10)) { long no = long.Parse(accountNo); switch(checkId) { case "00": valid = Verfahren00(accountNo, new int[] {2,1}); break; ... } private bool Verfahren00(string accountNo, int[] gewichtung) { int n = AddQuerGew(KontoRechtsOhneP(accountNo), gewichtung); n = (n % 10); n = (n == 0 ? 0 : 10 - n); return(n == Pruefziffer(accountNo)); }
Wenngleich die Klasse nur 25 der insgesamt über 100 Algorithmen abgebildet, decken diese doch rund 90% der vorhandenen Bankleitzahlen ab. Dies gilt nicht nur für die statische Auswertung des Bankleitzahlverzeichnisses selbst, sondern auch für das Vorkommen in der Praxis. Sie können diese Klasse daher in Ihren WebApplikationen zur Validierung von Kontonummern nutzen. Sofern der entsprechende Algorithmus nicht vorhanden ist, gilt die Kontonummer als korrekt. Ich habe das Beispiel aus dem vorherigen Rezept um die noch fehlende Eingabe einer Kontonummer ergänzt. Auch hier wird ein CustomValidator-Control genutzt, um die Richtigkeit serverseitig zu ermitteln. Als Trick frage ich innerhalb der Überprüfung das Ergebnis des anderen CustomValidator-Controls ab. Auf diese Weise kann eine erwünschte Reihenfolge erzwungen werden. Erst wird also die Bankleitzahl geprüft, danach die Kontonummer. Listing X.99 bankcodes2.aspx ... void accountno_ServerValidate(object sender, ServerValidateEventArgs e)
X ASP.NET mit C# Kochbuch – Update ____________________________________ 177
{ if(cv_bankcode.IsValid) { string checkType = this.GetCheckType(int.Parse(lb_banknames.SelectedItem.Value)); AccountNoCheck accountNoCheck = new AccountNoCheck(); e.IsValid = accountNoCheck.Check(tb_accountno.Text, checkType); } } string GetCheckType(int bankId) { OleDbConnection conn = new OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;" + @"Data Source=c:\inetpub\wwwroot\aspnet\bankcodes.mdb"); conn.Open(); string sql = "SELECT CheckType FROM Bankcodes WHERE ID=@ID;"; OleDbCommand cmd = new OleDbCommand(sql, conn); cmd.Parameters.Add("@ID", bankId); string checkType = (cmd.ExecuteScalar() as string); conn.Close(); return(checkType); } ...
Kontonummer
178 ______________________________________________ X.61 ... HTML-Tags filtern?
Abbildung X.81 Ganz so einfach lässt sich die Bankverbindung nicht fälschen
Wie erwähnt ist die Implementierung der verschiedenen Algorithmen nicht vollständig, wenngleich für die Praxis vollkommen ausreichend. Selbstverständlich können Sie die Klasse bei Bedarf um weitere Berechnungsarten ergänzen. Zur Überprüfung der jeweiligen Verfahren enthält das Dokument der Bundesbank zahlreiche Testkontonummern. Ich würde mich im Fall einer Erweiterung freuen, wenn Sie diese mir und damit den anderen Lesern zur Verfügung stellen würden. Beachten Sie in diesem Zusammenhang bitte, dass ich wie bei allen anderen Beispielen keine Gewähr für die korrekte Verwendbarkeit übernehmen kann.
X.61 ... HTML-Tags filtern? Gerade bei interaktiven Angeboten ist das Filtern von HTML-Tags immer wieder ein gefragtes Thema. Denken Sie nur an Ihr Gästebuch. Wie oft kommen irgendwelche Schlaumeier auf die Idee, Ihren Besuch mit besonders fetten Lettern oder gar der Einbindung einer externen Grafik festzuhalten. Mit dem Filtern von HTML-Tags ließe sich derartiger Unfug vermeiden.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 179
Wer mich kennt, der kennt auch meinen Hang zu regulären Ausdrücken. Auch in diesem Fall lassen sich Regex nutzen und der notwendige Ausdruck ist sogar ziemlich einfach: <.*?>
Die Bedeutung des Ausdrucks ist schnell erklärt. Die eckigen Klammern stehen für sich selbst und markieren den Beginn und das Ende des HTML-Tags. Der Punkt steht für ein beliebiges Zeichen inklusive dem Zeilenumbruch (Option Singleline). Danach folgt der Quantifizierer *, der besagt, dass vorangegangene Zeichen (also ein beliebiges) ein- oder mehrfach auftreten darf. Das Fragezeichen kennzeichnet den Ausdruck als non-greedy, also als nicht gierig. Das bedeutet, dass so wenige Zeichen gefunden werden, wie es eben geht. In Kombination mit der sich anschließenden eckigen Klammern zu werden also alle Zeichen gefunden, die innerhalb der beiden Klammern stehen und genau das ist ja gefordert. Das Listing demonstriert den Einsatz des Ausdrucks. Es enthält ein mehrzeiligen Eingabefeld. Eventuelle HTML-Auszeichnungen werden beim Klick auf den Button herausgefiltert. Das Ergebnis wird anschließend in einem Label-Control ausgegeben. Listing X.100 HtmlFilter1.aspx <script runat="server"> 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; }
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 <= this.MaxFileSize); } return(true); } }
Das zweite Control soll die Dateiendung des übertragenen Datei prüfen. Es wird eine Positiv- sowie eine Negativliste angeboten. Existiert die positive, wird überprüft, ob sich die tatsächliche Dateiendung darin wieder findet. Ansonsten wird sichergestellt, dass die Endung in der Negativliste nicht enthalten ist. Mehrere mögliche Endungen können dabei mit einem Komma separiert angegeben werden. Beachten Sie bitte, dass die intern verwendete Methode Path.GetExtension die Dateiendung inklusive dem Punkt liefert und dieser dementsprechend mit angegeben werden muss. public class FileExtensionValidator : BaseFileValidator
X ASP.NET mit C# Kochbuch – Update ____________________________________ 183
{ public string AllowedExtensions { get { return(ViewState["AllowedExtensions"] as string); } set { ViewState["AllowedExtensions"] = value; } } public string ForbiddenExtensions { get { return(ViewState["ForbiddenExtensions"] as string); } set { ViewState["ForbiddenExtensions"] = value; } } protected override bool EvaluateIsValid() { HttpPostedFile file = this.GetPostedFile(); if(file != null) { string ext = Path.GetExtension(file.FileName).ToLower(); string[] extList; if(this.AllowedExtensions != null) { extList = this.AllowedExtensions.ToLower().Split(','); return(((IList) extList).Contains(ext)); } else if(this.ForbiddenExtensions != null) { extList = this.ForbiddenExtensions.ToLower().Split(','); return(!((IList) extList).Contains(ext)); } } return(true); } }
Das dritte Validation Control soll den übertragenen MIME-Typen überprüfen. Der Aufbau des Controls entspricht dabei dem vorherigen, so dass eine Positiv- und eine Negativliste angeboten wird. Bedingt durch die Ähnlichkeit verzichte ich an dieser Stelle auf einen Abdruck des Quellcodes. Vielmehr möchte Ihnen ein Beispiel zum Einsatz der neuen Controls zeigen. Das Listing zeigt ein einfaches Formular zum Upload einer Datei. Die drei neuen Validation Controls sind darunter platziert und sorgen dafür, dass die Datei nicht größer als 50 KB ist, eine der Dateiendungen gif, jpg oder jpeg trägt und über den damit korrespondierenden MIME-Type verfügt.
184 ______________________________________ X.62 ... einen File Upload validieren?
Listing X.101 ValidateUpload1.aspx <% @Register Tagprefix="PAL" Namespace="PAL.AspNetKochbuch" Assembly="FileValidators" %> <script runat="server"> 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; }
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); }
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: <% @Page Language="C#" Debug="true" Trace="true" %>
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 <% @Page Language="C#" Debug="true" Trace="true" %> <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); <-Response.Cookies.Add(cookie); } ...
Das Beispiel basiert auf dem vorherigen, die Zuweisung der genannten Eigenschaft Expires ist in diesem Fall jedoch auskommentiert. Die eingegebenen Benutzerdaten werden weiterhin auf dem Client in einem Cookie gespeichert, gehen jedoch beim Schließen des Browsers verloren.
X.65 ... die Auswahl in einer DropDownList erzwingen? Anders als das ListBox-Control hat eine DropDownList keinen initiellen Status. Allein durch das Konzept des Controls bedingt ist immer ein Eintrag selektiert. Die Eigenschaft SelectedIndex liefert daher selbst direkt im Anschluss an die Datenbindung den Wert 0. Was also tun, wenn man die Auswahl eines Eintrags durch den Benutzer erzwingen und einen initiellen Status benutzen möchte? Der Trick ist so einfach wie effektiv. Fügen Sie im Anschluß an die Datenbindung einen zusätzlichen Eintrag an erster Stelle ein und überlassen Sie einem RequiredFieldValidator-Control die Überprüfung der Benutzerauswahl.
192 ________________________ X.65 ... die Auswahl in einer DropDownList erzwingen?
Das demonstriert das notwendige Vorgehen an Hand einer Länderliste. Um das Formular bestätigen zu können, muss explizit ein Land ausgesucht werden. Das Validation Control kann verwendet werden, da als Wert für den neu eingefügten Listeneintrag explizit (!) string.Empty angegeben wird. Listing X.105 DropDownList1.aspx <script runat=server> void Page_Load(object sender, EventArgs e) { if(!IsPostBack) { ... ddl_country.DataBind(); ddl_country.Items.Insert(0, new ListItem(" - Bitte auswählen - ", string.Empty)); } }
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?
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(); }
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 <% @Import Namespace="PAL.IPayment" %> <% @Register Tagprefix="PAL" Namespace="PAL.Projects.AspNetKochbuch" Assembly="CreditCardNumberValidator" %> <script runat="server"> void Page_Load(object sender, EventArgs e) { if(!IsPostBack) { for(int i=0; i<12; i++) { ccvalidmonth.Items.Add(String.Format("{0:00}", i+1)); } for(int i=0; i<5; i++) {
X ASP.NET mit C# Kochbuch – Update ____________________________________ 201
ccvalidyear.Items.Add(String.Format("{0}", DateTime.Now.Year + i)); } } } void ValidUntil_ServerValidate(object sender, ServerValidateEventArgs e) { e.IsValid = false; ListItem itemMonth = ccvalidmonth.SelectedItem; ListItem itemYear = ccvalidyear.SelectedItem; if((itemMonth != null) && (itemYear != null)) { e.IsValid = (DateTime.Now <= new DateTime(int.Parse(itemYear.Value), int.Parse(itemMonth.Value), 1).AddMonths(1).AddDays(-1)); } } void bt_click(object sender, EventArgs e) { // Request PaymentRequest paymentRequest = new PaymentRequest(); // Order Order order = new Order(); order.OrderId = "Test-Bestellung"; order.AccountId = "xxxxx"; order.UserId = "xxxxx"; order.Password = "xxxxx"; // PaymentInfo order.PaymentInfo.SimulationMode = true; order.PaymentInfo.PaymentData.CreditCardNumer = ccnumber.Text; order.PaymentInfo.PaymentData.CreditCardOwner = ccowner.Text; order.PaymentInfo.PaymentData.CreditCardCheckCode = int.Parse(cccode.Text); DateTime validDate = new DateTime(int.Parse(ccvalidyear.SelectedItem.Value), int.Parse(ccvalidmonth.SelectedItem.Value), 1).AddMonths(1).AddDays(-1); order.PaymentInfo.PaymentData.CreditCardValidDate = validDate; // Transaction Transaction transaction = new Transaction(); transaction.TransactionId = "test"; transaction.Amount = decimal.Parse(amount.Text); order.Transactions.Add(transaction);
202 ____________________________ X.68 ... eine Kreditkartenabbuchung durchführen?
paymentRequest.Orders.Add(order); SPPaymentGateway gateway = new SPPaymentGateway(); PaymentResult paymentResult = gateway.ProcessPayment(paymentRequest); TransactionResult transactionResult = paymentResult.Orders[0].Transactions[0]; if(transactionResult.ErrorCode == 0) { lb.Text = "Die Buchung wurde erfolgreich durchgeführt."; } else { lb.Text = string.Format("Die Buchung ist fehlgeschlagen: {0} ({1})", transactionResult.ErrorText, transactionResult.ErrorCode); } }
Abbildung X.92 Ups, so einfach geht das ...
Das Beispiel demonstriert, dass die Anbindung an ein Payment-Gateway im Prinzip ziemlich trivial ist. Natürlich werden Sie ein Produktsystem noch anpassen müssen, doch dieser Aufwand fällt unter die Kategorie Finetuning. Sollten Sie auf dem vorgestellten Objektmodell eine Anbindung realisieren, würde ich mich über eine kurze Email freuen. Das Beispiel kann in Verbindung mit einem Benutzer-Account bei iPayment ausprobiert werden. Um Ihnen eine kostenlose und unverbindliche Evaluation zu ermöglichen, bietet Ihnen das Unternehmen einen Test-
204 ____________________________ X.68 ... eine Kreditkartenabbuchung durchführen?
Account. Die notwendigen Benutzerdaten erhalten Sie kurzfristig auf Nachfrage per Email unter folgender Adresse:
[email protected]
Hintergrundinformationen Nachdem Sie erfahren haben, wie Sie in der Praxis eine Buchung durchführen können, möchte Ihnen noch ein wenig Theorie mit auf den Weg geben. Anders als beim Lastschriftverfahren kann nicht Jedermann „einfach so“ eine Kreditkartenabbuchung durchführen. Sie müssen zunächst zweierlei Arten von Verträgen abschließen: 1. Sie benötigen einen Clearing-Partner, der die technische Abwicklung übernimmt. Dieser tritt selbst nicht als Vertragspartner mit dem Endkunden auf und übernimmt ausschließlich die Vermittlung der Daten zwischen Ihnen und dem jeweiligen Kreditinstitut. Hierzu unterhält der Partner in der Regel DatexP-Leitungen und hat sein Angebot von den unterstützten Instituten individuell zertifizieren lassen. Ich persönlich habe in mehreren Projekten gute Erfahrung mit dem Dienst iPayment gemacht, der von Schlund und Partner (1&1, United Internet-Gruppe) unterhalten wird. Das weiter oben gezeigte Beispiel basiert auf diesem Anbieter. Weitergehende Informationen finden Sie auf der Website: www.ipayment.de
2. Bei der Gruppe von Vertragspartnern handelt es sich um die Kreditkarteninstitute respektive Banken, die ein entsprechendes Clearing anbieten. Mit diesem müssen Sie einen so genannten Akzeptanz- oder auch Mail-Order-Vertrag abschließen. Zu den bekanntesten Anbietern gehören die Citicorp Card Acceptance (CCA) sowie die FEDUCIA Cash. Beide Unternehmen bietet in der Regel gebündelte Verträge für die Akzeptanz von MasterCard/EuroCard sowie VISA an. Erfahrungsgemäß sind dies die in Europa am weitesten verbreiteten Karten. Sofern Sie Ihre Dienste jedoch auch außerhalb Europas, beispielsweise in den USA, anbieten wollen, sollten Sie zusätzlich American Express unterstützen. Einen Akzeptanzvertrag können Sie hier direkt mit AmEx Deutschland abschließen. www.cca.de www.feducia.de
X ASP.NET mit C# Kochbuch – Update ____________________________________ 205
www.americanexpress.de
Die Gebühren untereilen sich in monatliche Grundpreise und transaktionsabhängige Kosten. Die Kreditkarteninstitute beziehen Ihre Einnahmen in erster Linie aus den Provisionen. Sie können hier einen Betrag von knapp 4% des abgewickelten Bruttoumsatzes einrechnen. Grundgebühren werden in der Regel nicht berechnet. Hinzu kommen die Gebühren der Clearing-Schnittstelle. Im Fall von iPayment sind dies derzeit 0,25 € fix pro Transaktion sowie 25 € Grundpreis im Monat. So praktisch und einfach die Abwicklung von Buchungen über Kreditkarten auch sein mögen, so schwierig ist es derzeit leider auch, einen Akzeptanzvertrag zu erhalten. Ursache ist ein umstrittenes Urteil des Bundesgerichtshofs, das im Jahr 2001 der Klage eines Händlers gegen VISA stattgegeben hat. Dieses besagt, dass auch bei Mail-Order-Verträgen, bei denen dem Händler die Kreditkarte physikalisch nicht vorliegt, die prinzipielle Haftung beim Kreditkartenunternehmen und nicht etwa bei Händler liegt. Als Leitspruch des Urteils wurde eine Frage an einen anderen Zivilsenat des Gerichts formuliert, der bereits zuvor ein dem entgegen stehendes Urteil beschlossen hatte. Bis zur endgültigen Klärung durch diesen Senat halten sich viele Institute mit dem Abschluss weiterer Akzeptanzverträge zurück. Auf Grund der im Internet statistisch höhreren Chargeback-Quote vermuten sie ansonsten hohe Ausfälle. Ich empfehle Ihnen daher vor der Konkretisierung Ihrer Anforderungen zunächst Rücksprache mit einem der genannten Unternehmen oder einem alternativen Anbieter zu halten. Diese können Sie über den aktuellen Stand und die individuellen Möglichkeiten informieren.
X.69 ... HTTP-Kompression benutzen? Gerade wer innerhalb einer Web-Applikation viele Bilder überträgt, für den kann eine Kompression der übertragenen Daten sinnvoll sein. Ben Lowery hat ein .NETProjekt veröffentlicht, dass sich dieses Themas annimmt. Er hat ein HTTP Module entwickelt, dass auf Basis von SharpZipLib (Mike Krüger) die Übertragung vom Server an den Client on-the-fly komprimiert. Das Module verwendet dazu einen HttpOutputFilter. Die begleitende Buch-CD-ROM enthält den vollständigen Quelltexts des Projektes inklusive einem kleinen Beispiel. Es handelt sich um eine Visual Studio .NET Solution, die Sie jedoch auch ohne die Entwicklungsumgebung kompilieren und einsetzen können. Um zu überprüfen, dass die Komprimierung tatsächlich durchgeführt wird, können Sie das Programm ProxyTrace verwenden, das ich Ihnen im Rezept „...die Kom-
206 _______________________ X.70 ... reguläre Ausdrücke schnell und einfach testen?
munikation zwischen Client und Server belauschen?“ im Kapitel „Basics“ vorgestellt habe. Beim ersten Aufruf werden Sie vermutlich feststellen, dass die Verbindung doch nicht komprimiert stattfindet. In diesem Fall müssen Sie die Option „Use HTTP 1.1 through proxy connections“ in den erweiterten Optionen des Internet Explorers aktivieren. Anschließend sollten Sie beim Aufruf des Beispiel die Komprimierung mittels ProxyTrace nachvollziehen können. Die Abbildung zeigt ein Beispiel dafür.
Abbildung X.93 Die Übertragung verläuft komprimiert.
Das Projekt ist sehr schön gestaltet. Sie können es einerseits im Live-Betrieb zur Komprimierung der übertragenen Daten nutzen. Andererseits ist es auch ein gutes Lernmittel, wenn Sie mehr über die Entwicklung von HTTP Modulen erfahren möchten. An dieser Stelle noch ein herzlichen Dank an den Autoren des Projekts!
X.70 ... reguläre Ausdrücke schnell und einfach testen? Wie Sie vielleicht schon an anderer Stelle mitbekommen haben, bin ich ein großer Fan von regulären Ausdrücken. Doch so leistungsfähig diese Beschreibungssprache auch ist, so kompliziert ist sie mitunter auch. Häufig hilft da nur noch ein wenig Ausprobieren. Da dies in der laufenden Anwendung zumeist nicht oder wenn dann nur mit zusätzlichen Aufwand möglich ist, hat Christoph A. Müller ein kleines Testprogramm entwickelt.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 207
Das Programm ermöglicht die Eingabe eines regulären Ausdrucks sowie eines zu überprüfenden Textes. Zusätzlich können die üblichen Optionen der FlagsEnumerationen RegexOptions mittels CheckBox-Controls gesetzt werden. Bereits während der Eingabe werden die passenden Treffer in einer Liste ausgegeben.
Abbildung X.94 Reguläre Ausdrücke lassen sich vor der Verwendung ausprobieren.
Sie finden das Programm für Ihren persönlichen Gebrauch auf der beliegenden CDROM. Der Autor hat zudem zugestimmt, Ihnen im Rahmen dieses Buches den in C# geschriebenen Quelltext zur Verfügung zu stellen. Die Visual Studio .NET Solution ist ebenfalls Bestandteil der Buch-CD.
X.71 ... eine ASP.NET-Seite zeitgesteuert aufrufen? Sie würden sich wundern, wie häufig in den diversen Foren und Newsgroups der Wunsch geäußert wird, eine ASP.NET-Seite zeitgesteuert aufzurufen, beispielsweise um administrative Aufgaben innerhalb der Web-Applikation in regelmäßigen Abständen ausführen zu können. Das Betriebssystem liefert bereits alle Voraussetzungen mit, um eine derartige Anforderung in nur zwei Minuten zu realisieren. Sie benötigen lediglich einen
208 __________________________ X.71 ... eine ASP.NET-Seite zeitgesteuert aufrufen?
Rechner, der zu dem gewünschten Zeitpunkt eingeschaltet ist und über eine Verbindung zum Internet verfügt. Da administrative Aufgaben üblicherweise in den Nachstunden durchgeführt werden, bietet sich die Verwendung eines Servers, idealerweise sogar des entsprechenden Web-Servers an. Ist die Frage des zu nutzenden Rechners geklärt, können Sie den zeitgesteuerten Aufruf festlegen: 1. Öffnen Sie in der Systemsteuerung den Punkt „Geplante Vorgänge“ und wählen anschließend „Geplanten Task hinzufügen“. 2. Im folgenden Assistenten können Sie die aufzurufende Datei festlegen. Geben Sie dabei bitte nicht die HTTP-Adresse an, sondern wählen das Programm start.exe aus dem Windows-Verzeichnis aus. 3. In den beiden sich anschließenden Fenstern des Assistenten können Sie einen möglichst treffenden Namen angeben und die zeitliche Ausführung festlegen. Es lassen sich verschiedenste Intervalle angeben, beispielsweise täglich oder auch nur monatlich. Im Nachhinein können zudem mehrere Zeitpläne erzeugt werden. 4. Im letzten Schritt des Assistent können Sie das zu benutzende Benutzerkonto eintragen und müssen in diesem Zuge auch das zugehörige Passwort angeben. Dies ist notwendig, da der Vorgang wird später unter diesem Benutzer ausgeführt wird, selbst dann, wenn kein interaktiver Benutzer am System angemeldet ist, wie dies typischerweise bei Servern der Fall ist. 5. Schließen Sie den Assisten ab und öffnen Sie direkt im Anschluss die Eigenschaften des neu angelegten Vorganges. 6. Fügen unter „Starten“ im Anschluss an die eben konfigurierte start.exe die aufzurufende Web-Adresse in Anführungszeichen ein, so wie dies in der Abbildung zu sehen ist. Nachdem Sie den Dialog bestätigt und erneut dass Passwort des zu verwendenden Benutzers eingegeben haben, wird die WebSeite wie gewünscht zeitgesteuert aufgerufen. Leider ist der Task Planer zumindest in der aktuell vorliegenden Version nicht wirklich für das Web ausgelegt. Die Angabe einer Internet-Adresse ist daher nur über den Umweg der start.exe möglich. Es handelt sich dabei um ein Programm, das sonst für die DOS-Promt (cmd.exe) genutzt wird. Innerhalb der Kommandozeile können Sie über dieses Tool verknüpfte Dateien und Internet-Adressen aufrufen, wie Sie es sonst von Start > Ausführen gewohnt sind4.
4
Ja genau, liebe Win32 API-Fans, es handelt sich um ShellExecute.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 209
Abbildung X.95 Die Seite wird nun regelmäßig aufgerufen.
Um einen zumindest annähernden Schutz der aufzurufenden Seite zu ermöglichen und einen Missbrauch dieser zu verhindern, sollten Sie den Zugriff mittels Windows Authentication einschränken. Zudem sollte ein gültiges SSL-Zertifikat installiert sein, so dass Sie zum Aufruf HTTPS nutzen können. Die Übergaben des Benutzernamens samt Passwort kann in diesem Fall innerhalb der URL erfolgen: C:\WINDOWS\start.exe "http://benutzer:passwort@localhost/asp.net/"
Sofern der aufrufende Rechner über eine statische IP-Adresse verfügt (oder es sich gar um den lokalen Web-Server handelt, können Sie den Aufruf innerhalb der IISKonfiguration auf diese festgelegte IP-Adresse beschränken . Bei lokalen Aufrufen ist dies die Adresse 127.0.0.1.
Warum Sie vergessen sollten, was Sie gerade erfahren haben ... Zugegeben, es ist verlockend, administrative Aufgaben auf diese Weise zeitgesteuert erledigen zu lassen. Es ist allerdings nicht unbedingt Sinn und Zweck einer Web-Seite, für derartige Machenschaften missbraucht zu werden. Nein, es ist si-
210 ___________________ X.72 ... ein Programm zeitgesteuert im SQL-Server aufrufen?
cherlich nicht unbedingt moralisch verwerflich, bringt aber gewisse Nachteile mit sich. Dazu gehören unter anderem ... • .... die Tatsache, dass Sie eventuelle Fehler schlecht verarbeiten und zurückmelden können sowie ... • ... der Umstand, dass eventuelle Timeouts der Seite umfangreiche Aktionen nach Ablauf der konfigurierten Zeitspanne unterbrechen können. Es bietet sich die Entwicklung eines kleinen Kommandozeilenprogramms an, dass die administrativen Aufgaben vollständig ausführt. Sie können dies zeitgesteuert mittels dem Task Planer direkt auf dem Web Server starten oder auch auf einem externen System, das beispielsweise direkt auf den SQL-Server zugreift. Eine weitere Möglicht bietet der SQL Server selbst. Dieser integriert über den SQL Server Agend ein eigenes Programm zur Steuerung von Jobs. Typischerweise werden diese genutzt, um Stored Procedures und andere T-SQL-Anweisungen zeitgesteuert auszuführen. Sollten zusätzlich programmatische Aufgaben zu bewältigen sein, können Sie innerhalb eines solchen Jobs sogar ein Programm aufrufen. Das Rezept „... ein Programm zeitgesteuert im SQL-Server aufrufen?“ aus dem Kapitel „Datenbanken“ zeigt, wie dies geht.
X.72 ... ein Programm zeitgesteuert im SQL-Server aufrufen? Das Rezept „... eine ASP.NET-Seite zeitgesteuert aufrufen?“ aus dem Kapitel „Basics“ hat die Möglichkeit beschrieben, Web-Seiten und Programme zeitgesteuert über den Windows Task Planer zu starten. Auch der SQL Server verfügt über eine derartige Möglichkeit, die Sie über den Enterprise Manager konfigurieren können. Ich möchte an dieser Stelle nicht in die Details des SQL Server Agents gehen und setze voraus, dass Sie bereits über entsprechende Kenntnisse verfügen. 1. Um ein Programm über das Auftrags-Management des SQL Server zu starten, legen Sie zunächst einen neuen Auftrag an. 2. In einem ebenfalls neu angelegten Auftragsschritt wählen Sie aus der Liste „Typ“ den Punkt „Betriebssystembefehl (CmdExec)“. 3. Im nun darunter angezeigten Befehlsfenster geben Sie den Pfad des auszuführenden Programmes samt eventueller Parameter an. Beachten Sie dabei, dass es sich um den lokalen Pfad auf dem Server und nicht etwa Ihrem Entwicklungsrechner handeln muss. Sie haben die Möglichkeit, den Aufruf des Programms optimal in den gesamten Auftrag zu integrieren, der vermutlich aus weiteren Schritte besteht. Sie können dazu einen Exit-Code angeben, der als Erfolg respektive Fehlerfall gewertet wird.
X ASP.NET mit C# Kochbuch – Update ____________________________________ 211
Der bereits hinterlegte Standardwert 0 sollte beibehalten werden. Innerhalb des erstellten Programms können Sie nun einen Fehler melden, indem Sie den Rückgabewert der Main-Methode explizit auf einen anderen int-Wert setzen, beispielsweise –1.
Eine Frage des Benutzers Da der SQL Server unter anderem mit einem eigenen Benutzermodell arbeitet, stellt sich die Frage, unter welchem Benutzer-Account des Betriebssystems der Aufruf stattfindet. Es gibt zwei Möglichkeiten: • Sofern Sie es nicht explizit anders konfigurieren, haben nur System Administratoren das Recht, Programme aufzurufen. In diesem Fall wird der Prozess mit dem konfigurierten Benutzer des SQL Server Agent gestartet. Dieser kann in den Diensten angegeben werden, standardmäßig handelt es sich um den SYSTEM-Account. • Sollen auch andere Benutzer die Möglichkeit haben, Programme über den Agent auszuführen, können Sie in dessen Eigenschaften ein so genanntes Proxy-Konto angeben. Alle Prozesse werden nun unter diesem Account gestartet.
Abbildung X.96 Welcher Benutzer soll für Programmaufrufe genutzt werden?
212 __________________________________________ X.73 ... einen Redirect auflösen?
X.73 ... einen Redirect auflösen? Redirects werden mittlerweile als fester Bestandteil der Entwicklung mit classic ASP, ASP.NET und ähnlichen Technologien genutzt. Greift man programmatisch andere Seiten zu, wie dies bei beispielsweise bei der Entwicklung von Spidern der Fall ist, so ist es oftmals hilfreich, das Ziel eines Redirects zu ermitteln, ohne diesen tatsächlich auszuführen. Die Methode GetRedirectUri aus dem folgenden Beispiel demonstriert, wie Sie mit Hilfe der Klassen aus dem Namespace System.Net die Adresse eines Redirects ermitteln können. Um zu verstehen, was hier passiert, muss man sich nur den Ablauf eines Redirects vor Augen führen. Der Server meldet den Statuscode 302 und schickt über den Kopfzeileneintrag Location die neue Zieladresse. Genau diese Eintrag wird im Listing abgefragt. Listing X.110 GetRedirectLocation1.aspx <script runat="server"> void Page_Load(object sender, EventArgs e) { Uri url = new Uri(Request.Url, "redirect1.aspx"); Uri redirect = this.GetRedirectUri(url); if(redirect != null) Response.Write(redirect.ToString()); } public Uri GetRedirectUri(Uri uri) { try { HttpWebRequest request = (HttpWebRequest) WebRequest.Create(uri); request.AllowAutoRedirect = false; HttpWebResponse response = (HttpWebResponse) request.GetResponse(); string location = response.GetResponseHeader("Location"); if(location != null) return(new Uri(location)); return(null); } catch { return(null); } }
X ASP.NET mit C# Kochbuch – Update ____________________________________ 213
Beachten Sie, dass die Klasse HttpWebRequest im Regelfall einem Redirect automatisch folgt. Sie müssen dies explizit durch Deaktivieren der Eigenschaft AllowAutoRedirect verhindern, um anschließend mittels GetResponseHeader den beschriebenen Kopfzeileneintrag Location abfragen zu können. Um die Funktionalität auszuprobieren, bedarf es eines kleinen Beispiels, das durch die vorherige Seite (und nicht durch den Client-Browser) aufgerufen wird. Die Seite initiiert einen Redirect. Die angegebene Zieladresse wird ermittelt und im Browser ausgegeben. Listing X.111 Redirect1.aspx <script runat="server"> 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(""); script.Append("script>"); 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<50; i++) { lt.Text += string.Format("{0}
", i); } this.Controls.Add(new ScrollInView()); }
X ASP.NET mit C# Kochbuch – Update ____________________________________ 225
Beim Aufruf der Seite wird automatisch zum Ende selbiger navigiert. Die Abbildung zeigt dies.
Abbildung X.102 Durch das Control wird das Ende der Seite angezeigt.
Ein vergleichbares Verhalten lässt sich auch im zweiten Beispiel erkennen, in dem das Control statisch im Layout-Bereich der Seite platziert wurde. Listing X.121 scrollinview2.aspx <script runat="server">
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.