Von der Datenbank bis zur Oberfläche mit .NET, Teil 3: Eine Weboberfläche mit ASP.NET

Know-how  –  5 Kommentare

In den ersten beiden Teilen des Tutorials entstanden Datenbank sowie Datenzugriffs-, Geschäftslogik- und Webserviceschicht. Die Webservices soll nun nicht nur – wie bisher – eine Konsolen-, sondern auch eine HTML-Weboberfläche konsumieren können.

Die Technik zum Erstellen von Webanwendungen mit dem .NET Framework heißt ASP.NET, wobei sie inzwischen nicht mehr nur als eine einzige Bibliothek anzusehen ist, da es mehrere Varianten von ASP.NET gibt. Im Rahmen des Tutorials sollen die sogenannten ASP.NET Webforms zum Einsatz kommen. Webforms sind der ursprüngliche und bislang deutlich verbreitetere Ansatz im Vergleich mit den neueren ASP.NET MVC, ASP.NET Dynamic Data und ASP.NET Webpages (vgl. Statistiken).

Von der Datenbank bis zur Oberfläche mit .NET

Um das Beispiel mitzuprogrammieren, benötigt der Entwickler Grundkenntnisse in der C#-Syntax und der Handhabung von Visual Studio 2010. Das Tutorial verwendet die englische Version von Visual Studio, weil es in der deutschen Ausgabe einen Fehler gibt, durch den sich das Beispiel nicht ohne viel manuelle Arbeit zum Laufen bringen lässt.

Selbst innerhalb der Webforms gibt es mehrere Wege zum Ziel, und das fällt schon beim Anlegen eines neuen Projekts auf: Der Entwickler hat die Wahl zwischen einer "Web Application" und einer "Website". Das zweite ist das neuere, hier verwendete Modell. Man erstelle eine Website aber nicht über Add | New Project, sondern über Add | New Website. Hierbei ist darauf zu achten, dass der Pfad im Dialog so gewählt wird, dass die Website-Dateien dann im gleichen Ordner wie die anderen Projekte der Projektmappe liegen. Websites haben anders als andere Visual-Studio-Projekte keine .csproj-Datei.

Die Website benötigt Referenzen auf die Webprojekte WWWings_ServiceProxies und WWWings_GO sowie die Systembibliothek System.ServiceModel. Außerdem muss der Entwickler die Dienst-Clientkonfiguration für die Webservices in die Website übernehmen. Dafür kopiert er das Element <system.serviceModel> aus der WWWings_ServiceProxies/app.config in die WWWings_Web/web.config-Datei. Wichtig ist, dass <system.serviceModel> direkt ein Unterelement von <configuration> sein muss. Aufzupassen ist darauf, dass man <system.serviceModel> nicht versehentlich in das vorhandene <system.web>-Element verschachtelt.

Zielarchitektur für diesen Teil des Tutorials (Abb. 1)

Vorlagenseite

Für eine einheitliche Seitenstruktur über mehrere Seiten hinweg legt der Entwickler am besten zuerst eine Vorlagenseite an, indem er in der Website unter Add new Item die Vorlage "Master Page" wählt. Als Name sei hier WWWings.master vergeben. Eine .master- enthält die Grundstruktur einer HTML-Seite mit den Tags <html>, <head> und <body>. In den beiden ersten ist jeweils ein Platzhalter (<asp: ContentPlaceHolder>) eingebettet, der die von der Vorlagenseite abgeleiteten Seiten mit Inhalten befüllen kann. Listing 1 zeigt den möglichen Inhalt von WWWings.master. Es bindet eine Stylesheet- (wwwings.css) und eine Grafikdatei (WWWingsLogo.jpg) ein, deren Inhalt aber für die weitere Betrachtung nebensächlich ist. Die Dateien sind hier nicht wiedergegeben, aber natürlich via Download enthalten. Einen Platzhalter im Header benötigt man nicht; stattdessen gibt es einen Platzhalter für die Überschrift der Seite (C_Ueberschrift) und einen für den Inhalt (C_Inhalt). Die Vorlagenseite WWWings.master verfügt auch über eine sogenannte Code-Behind-Datei (WWWings.master.cs). Diese ist aber nicht anzupassen, da im vorliegenden Fall die Vorlagenseite keinen Programmcode benötigt.

Buchungsmaske

Die ASP.NET-Buchungsmaske in Aktion (Abb. 2)

Abbildung 2 zeigt den Aufbau der Buchungsmaske. Es gibt drei Rahmen: den oberen für den Flug, den mittleren für den Passagier und den unteren für die Buchung. Für die Buchungsmaske legt der Entwickler eine Detailseite mit Namen Buchung.aspx an. Dazu wählt er Add new item und Web Form, wobei das Häkchen "Select master page" zu aktivieren ist. Dadurch kann er im nächsten Schritt WWWings.master als Vorlagenseite wählen. Der folgende Code zeigt die allgemeine Grundstruktur der Detailseite mit zwei Inhaltselementen entsprechend den Platzhaltern. Die Grundstruktur ist nun mit Inhalten zu füllen.

<%@ Page Title="" Language="C#" MasterPageFile="~/WWWings.master" 
AutoEventWireup="true" CodeFile="Buchung.aspx.cs" Inherits="Buchung" %>

<asp:Content ID="Content1" ContentPlaceHolderID="C_Ueberschrift"
Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="C_Inhalt" Runat="Server">
</asp:Content>

Listing 2 zeigt das fertige Layout der Detailseite. Die drei Bereiche werden durch <asp:Panel>-Elemente gebildet. Die Zahl-Eingabefelder und das Namenseingabefeld sind vom Typ <asp:TextBox>. Die Flughäfen kann der Entwickler mit einem Auswahlfeld bestimmen (<asp:DropDownList>), wobei die Auswahl "Alle" schon statisch in der ASPX-Seite als <asp:ListItem> vordefiniert ist. Die Liste der Flughäfen wird dann in der Code-Behind-Seite ergänzt – dafür sorgt AppendDataBoundItems="true". Die Schaltflächen erhält man über <asp:Button>. Für die tabellarische Ausgabe von Flügen und Passagieren kommt <asp:GridView> zum Einsatz. Im GridView-Steuerelement hinterlegt der Programmierer die auszugebenden Spalten. Die erste dient der Darstellung des Auswahl-Hyperlinks.

Bei den drei <asp:Button>-Steuerelementen findet der Entwickler erwartungsgemäß in OnClick="..." einen Verweis auf den auszuführenden Programmcode in der Hintergrundcodedatei. Aber auch <asp:TextBox> und <asp:DropDownList> verweisen mit OnTextChanged="..." und OnSelectedIndexChanged="..." die Bindung an Ereignisbehandlungsroutinen. Der Browser löst ja nach Eingabe in die Eingabefelder normalerweise keinen Postback zum Server aus, also das erneute Laden der gleichen Seite. ASP.NET sorgt aber durch den Zusatz AutoPostBack="True" dafür, dass der Browser entsprechenden JavaScript-Code erhält, der den Postback auslöst, sobald der Benutzer das Eingabefeld verlässt. Somit kann er komfortabel Suchvorgänge starten, ohne immer wieder auf die Suchen-Schaltflächen klicken zu müssen.

Hintergrundcode

Hintergrundcode der Buchungsmaske

Listing 3 zeigt den Hintergrund-Code. Bei jedem Seitenaufruf wird zu Beginn Page_Load() aufgerufen – eine gute Gelegenheit, den Webservice-Proxy zu instanziieren. Diese Instanz verwendet man im Ablauf der Seite, bis sie in Page_Unload() wieder vernichtet wird. Page_Load() lädt vom Webservice die Flughäfenliste und bindet sie an die beiden DropDownList-Steuerelemente. Diese Datenbindung muss nur beim erstmaligen Laden erfolgen, denn ASP.NET speichert die Daten in einem versteckten HTML-Feld mit Namen "Viewstate", daher steht dieser Code in der Bedingung !Page.IsPostBack. Viele Entwickler stören sich an dem zuweilen großen Viewstate, der zwischen Browser und Webserver immer wieder hin- und hergesendet wird. Die Alternative wäre aber hier sonst, bei jedem Roundtrip die Daten neu vom Webservice abzuholen. Auch das ist nicht effizient. Nachzudenken wäre allerdings darüber, diese wenig veränderlichen Daten auf dem Webserver im RAM zwischenzuspeichern.

Die Ereignisbehandlungsroutinen C_PassagierSuchen_Click() und C_FlugSuchen_Click() sind analog aufgebaut: Als Erstes prüft Int32.TryParse(), ob eine gültige Zahl eingegeben wurde. Wenn ja, wird die Suche über den Primärschlüssel mit GetFlug() und GetPassagier() gestartet. Im anderen Fall erfolgt die Suche über die Flugroute (GetFluege()) oder einen Namensbestandteil (GetPassagiere()).

Das GridView-Steuerelement erwartet als Datenquelle immer eine Menge. Übergibt man ein einzelnes Objekt, würde ASP.NET das mit dem Fehler "Data source is an invalid type. It must be either an IListSource, IEnumerable, or IDataSource." quittieren. Daher muss der Entwickler das Einzelobjekt in eine Liste verpacken, idealerweise in den gleichen Listentyp (ObservableCollection), den auch die Methoden GetFluege() und GetPassagiere() liefern. Die Datenbindung an den Inhalt des Attributs DataSource ist durch den Aufruf der Methode DataBind() auszulösen.

In C_Buchen_Click() gilt es zunächst zu prüfen, ob eine Auswahl in der Tabelle der Flüge und der Passagiere erfolgt ist. Zum Ermitteln des gewählten Flugs und des gewählten Passagiers greift der Programmcode auf SelectedDataKey[0] zurück. Dafür ist die Voraussetzung, dass in beiden <asp:GridView>-Tags im Attribut DataKeyNames festgelegt wurde, welches Geschäftsobjektattribut der Primärschlüssel enthält. Um zu verhindern, dass die Buchung ohne Auswahl startet, ist in Page_PreRender() hinterlegt, dass die "Buchen"-Schaltfläche nur aktiviert ist, wenn es eine Auswahl bei Flug und Passagier gibt.

Bei den beiden GridView-Steuerelementen lässt sich beobachten, dass eine einmal erfolgte Auswahl auch bei neuer Suche erhalten bleibt. Wenn der Anwender zum Beispiel bei einer Suche der Flüge nach "Rom" den zweiten auswählt und sich dann aber vor der Buchung doch entscheidet, nach "Paris" zu fliegen, ist nun der zweite Flug nach Paris ausgewählt. Das ist in dem Fall aber nicht sinnvoll, weswegen der Entwickler dieses Standardverhalten im GridView-Steuerelement durch den Zusatz EnablePersistedSelection="true" abstellen kann.

AJAX für die Buchungsmaske

Die bisherige Software erfüllt den gewünschten Zweck, aber beim Benutzer der Seiten gibt es leider einen hässlichen Effekt: Wenn er oben aus einer Liste einen Flug auswählt, dann nach unten scrollt, um einen Passagier zu wählen, ist er wieder oben auf der Seite und muss erneut scrollen. Das liegt daran, dass jede Aktion auf der Seite einschließlich der Auswahl eines Elements ein Neuladen der kompletten Seite durchführt. Die Seite wird jedes Mal komplett auf dem Server neu "gerendert".

Eine lupenreine Web-2.0-Anwendung würde jetzt bedeuten, dass man viel JavaScript programmiert, um per AJAX nur die Daten vom Webservice BuchungsService zu beziehen und dann per JavaScript die Oberfläche zu verändern. Das ist im Vergleich zur .NET-Programmierung aufwendig. ASP.NET bietet daher eine Kompromisslösung, eine Art "AJAX light". Dabei definiert der Entwickler mit einem <asp:UpdatePanel>-Element einen Bereich in einer ASPX-Seite, der sich einzeln aktualisieren lässt.

Zugehörige Trigger geben an, wann es zur Aktualisierung kommt. Der Server rendert dann nur die auszutauschenden Seitenfragmente, die per JavaScript (das bei ASP.NET mitgeliefert wird) in die Seiten eingesetzt werden. Im konkreten Fall bietet es sich an, ein UpdatePanel um die beiden GridView-Steuerelemente sowie den unteren Bereich mit der Buchen-Schaltfläche und das danebenstehende Label-Steuerelement zu setzen.

Listing 4 zeigt die veränderte Buchung.aspx mit den UpdatePanel-Steuerelementen, Triggern und dem obligatorischen ScriptManager zu Beginn. Wichtig ist, dass der Entwickler nicht vergisst, dass das SelectedIndexChanged-Ereignis der GridView-Steuerelemente Trigger für das C_Buchen_UpdatePanel sein müssen, denn durch die Auswahl in den Tabellen wird ja gegebenenfalls die Schaltfläche "Buchen" aktiviert. Alternativ zu den statischen Triggern kann man die Aktualisierung einzelner UpdatePanel-Steuerelemente in der Hintergrund-Code-Datei auslösen. Abbildung 3 zeigt die komplette Seite im Visual-Studio-Webdesigner einschließlich der Möglichkeit, sich die Trigger zusammenzuklicken.

Buchung.aspx im Visual-Studio-Webdesigner (Abb. 3)

Benutzeroberfläche "advanced"

Eingabemaske für neue Passagiere

Nun soll die Benutzeroberfläche noch etwas weiter getrieben werden, um zu zeigen, wie man Daten zwischen Webseiten austauscht und die Operation SavePassagiere() verwendet. Abbildung 4 zeigt die Eingabemaske für neue Passagiere, die man über die Schaltfläche "Neuer Passagier" aus der Buchung.aspx erreicht.

NeuerPassagier.aspx (Abb. 4)

Der Entwickler muss zunächst in WWWings_Web ein weiteres Element vom Typ "Web Form" mit Namen NeuerPassagier.aspx und Bezug auf die Vorlagenseite WWWings.master anlegen. Den Inhalt dieser .ASPX-Seite zeigt Listing 5. Einiges, aber nicht alles davon kann der Programmierer in Visual Studio auch im Designer gestalten. Die Eingabemaske für die neuen Passagiere könnte er manuell aus einzelnen TextBox-Steuerelementen aufbauen. Hier sei aber als Grundgerüst das DetailsView-Steuerelement verwendet. Mit dem Zusatz DefaultMode="Insert" präsentiert es sich direkt beim Aufruf als leeres Eingabeformular. Die einzelnen darzustellenden Eingabefelder sind in <Fields> definiert. Zur einfachen Darstellung eines Texteingabefeldes würde <asp:BoundField> reichen. Da hier aber die Eingabe mit Validatoren geprüft werden soll, sind <asp:TemplateField>-Elemente notwendig, die etwas mehr Arbeit machen, weil man in ihnen das Aussehen der Zeilen sowohl für die Darstellung als auch die Eingabe explizit definieren muss.

Name und Vorname werden durch <asp:RequiredFieldValidator> zum Pflichtfeld. Die Prüfung dessen erfolgt clientseitig durch JavaScript und dann zur Sicherheit noch einmal serverseitig. Das JavaScript muss man aber nicht selbst schreiben, da ASP.NET es erzeugt. Das Geburtsdatum ist im Objektmodell kein Pflichtfeld (vgl. Teil 1 des Tutorials), aber es soll hier mit <asp:RegularExpressionValidator> ein gültiges Datum geprüft werden. Der hinterlegte reguläre Ausdruck kontrolliert aber nur, ob der grundsätzliche Aufbau korrekt ist. Eine Datumseingabe wie 99.12.2012 würde nicht bemängelt. Auch <asp:RegularExpressionValidator> verrichtet die Arbeit via JavaScript und serverseitig. Beim Passagierstatus gibt es ein Auswahlfeld; hier ist eine Fehleingabe also ausgeschlossen. Die letzte Zeile aktiviert mit <asp:CommandField> die Links zum Speichern und Abbrechen.

Es fehlt nun noch die komplette Zusammenarbeit mit dem Webservice. Diese hinterlegt der Entwickler im einfachsten Fall nicht in der Hintergrund-Code-, sondern in einer getrennten C#-Klassendatei im Webprojekt. Wenn er ein Element vom Typ "Class" einfügt, weist Visual Studio darauf hin, dass in einer ASP.NET-Website alle freien Klassendateien im Unterordner App_Code liegen müssen, und legt diesen dann an. Die Klasse NeuerPassagierManager realisiert zwei Methoden: Laden() erzeugt eine Instanz von Passagier und Einfuegen() speichert diese über den Webservice. Das <asp:ObjectDataSource>-Steuerelement vor dem DetailsView-Steuerelement in NeuerPassagier.aspx (siehe Listing 5) sorgt dafür, dass das DetailsView-Steuerelement diese Methoden aufruft. Letzteres ist an ObjectDataSource gebunden, und diese verweist auf die Klasse NeuerPassagierManager mit den Methoden Laden() und Einfuegen().

using System;
using System.Collections.ObjectModel;
using System.Web;
using WWWings_Dienstproxies.WWWingsServer;
using WWWings_GO;

public class NeuerPassagierManager
{
public static Passagier Laden()
{
return new Passagier();
}

public void Einfuegen(Passagier pneu)
{
// Neuen Passagier speichern
ObservableCollection<Passagier> GeändertePassagiere
= new ObservableCollection<Passagier>() { pneu };
string Statistik;

BuchungsServiceClient client = new BuchungsServiceClient();
var antwort = client.SavePassagierSet(GeändertePassagiere, out Statistik);
client.Close();

Console.WriteLine("Statistik von SavePassagierSet: " + Statistik);

if (antwort.Count == 1)
{
// Der erste neue Passagier muss der angelegte sein,
// der nun auch die ID enthält!
pneu = antwort[0];
// Den Passagier merken zur Übergabe an Buchung.aspx
HttpContext.Current.Session["Passagier"] = pneu;
}
}
}

Was nun noch fehlt, ist die Navigation zwischen Buchung.aspx und NeuerPassagier.aspx. Dabei gibt es die Anforderung, dass ein beim Verlassen der Buchung.aspx gewählter Flug bei der Rückkehr weiterhin ausgewählt ist und ein in NeuerPassagier.aspx erfasster Flug dann ebenfalls. Für die Kommunikation zwischen zwei Seiten lässt sich der Viewstate nicht verwenden, da er immer seitenbezogen ist. Hier benötigt man serverseitig gespeicherte Sitzungsvariablen. Der Browser erhält zur Wiederkennung des Nutzers einen Cookie oder eine lange ID in der URL. Um beides muss sich ein ASP.NET-Entwickler nicht kümmern.

Der Entwickler muss sich nur die beim Klick auf "Neuer Passagier" in Buchung.aspx die aktuelle Flugauswahl merken und beim Page_Load() der Buchung.aspx.cs prüfen, ob ein gemerkter Flug und/oder Passagier vorhanden ist (beides siehe Listing 6). Dabei sieht man eine kleine, leider nicht umgehbare Inkonsistenz: Beim "Passagier" kann man sich in der Sitzungsvariablen das ganze Passagierobjekt merken. Bei "Flug" kann man sich leider nur die ID des Flugs merken, da das GridView-Steuerelement nur die ID des gewählten Flugs hergibt, nicht aber das ganze Objekt. Zur Vervollständigung fehlt dann nur noch eine Zeile in der Hintergrundcode-Datei von NeuerPassagier.aspx (siehe folgenden Code): Egal, ob der Benutzer "Speichern" oder "Abbrechen" klickt, es geht zurück zu Buchung.aspx.

using System.Web.UI.WebControls;

public partial class NeuerPassagier : System.Web.UI.Page
{
protected void C_Passagier_ModeChanging(object sender,
DetailsViewModeEventArgs e)
{
if (e.CancelingEdit || e.NewMode == DetailsViewMode.Insert)
Response.Redirect("Buchung.aspx");
}

}

Fazit

Mit wenig Aufwand ist eine überzeugende Webanwendung zur Flugbuchung auf Basis der bestehenden Webservices entstanden. ASP.NET kann einige Stärken hinsichtlich der Abstraktion von HTTP, HTML und JavaScript in diesem Beispiel ausspielen, sodass der Webentwickler stark von Infrastrukturcode entlastet wird. Nur ansatzweise erwähnt wurde, welche Möglichkeiten der Webdesigner bietet, um sich die ASPX-Tags zusammenzuklicken.

Im nächsten Teil soll eine XAML-Oberfläche für die Windows Presentation Foundation (WPF) entstehen. Einige Male wird der Entwickler sich dann via Copy & Paste beim Webprojekt bedienen können, aber vieles ist auch wieder ganz anders. (ane)

Dr. Holger Schwichtenberg
leitet das Expertenteam von www.IT-Visions.de, das Beratung und Schulungen im .NET-Umfeld anbietet. Er hält Vorträge auf Fachkonferenzen und ist Autor zahlreicher Fachbücher.

Literatur

Holger Schwichtenberg; ASP.NET 4.0 Entwicklerhandbuch; Microsoft Press 2011

Listing 1

Listing 1: WWWings.master

<%@ Master Language="C#" AutoEventWireup="true" 
CodeFile="WWWings.master.cs" Inherits="WWWings" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>World Wide Wings</title>
<style type="text/css">

</style>
<link href="WWWings.css" rel="stylesheet" type="text/css" />
</head>
<body>
<form id="form1" runat="server">
<div>
<img alt="" src="WWWingsLogo.jpg" style="float: right" /><br />
<span class="Einleitung">Die beispielhafte Fluggesesellschaft<br />
www.word-wide-wings.de</span>
<br />
<br />
<span class="Ueberschrift">
<asp:ContentPlaceHolder ID="C_Ueberschrift" runat="server">
</asp:ContentPlaceHolder>
</span>
<br />
<br />
<br />
<asp:ContentPlaceHolder ID="C_Inhalt" runat="server">
</asp:ContentPlaceHolder>
</div>
</form>
</body>
</html>

Listing 2

Listing 2: Buchung.aspx

<%@ Page Language="C#" MasterPageFile="~/WWWings.master" 
AutoEventWireup="true" CodeFile="Buchung.aspx.cs"
Inherits="Buchung" Title="Flugbuchung" %>

<asp:Content ID="Content1" ContentPlaceHolderID="C_Ueberschrift"
runat="server">
Flugbuchung
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="C_Inhalt" runat="Server">
<!-- #####################################################
Sektion 1: Flugsuche -->
<asp:Panel ID="Panel1" runat="server" GroupingText="Flug">
<table class="Tabelle">
<tr>
<td class="hell" align="center" Width="80" >
Flug-ID
</td>
<td class="dunkel" align="center" Width="60">
<asp:TextBox ID="C_Flugnummer" runat="server" Width="50"
AutoPostBack="True" OnTextChanged="C_FlugSuchen_Click"
Text=""></asp:TextBox>
</td>
<td class="hell" align="center">
Abflugort
</td>
<td class="dunkel" align="center">
<asp:DropDownList ID="C_Abflugort" runat="server" Width="160"
AppendDataBoundItems="true" AutoPostBack="True"
OnSelectedIndexChanged="C_FlugSuchen_Click">
<asp:ListItem Text="Alle" Value=""></asp:ListItem>
</asp:DropDownList>
</td>
<td class="hell" align="center">
Zielort
</td>
<td class="dunkel" align="center">
<asp:DropDownList ID="C_Zielort" runat="server" Width="160"
AppendDataBoundItems="true" AutoPostBack="True"
OnSelectedIndexChanged="C_FlugSuchen_Click">
<asp:ListItem Text="Alle" Value=""></asp:ListItem>
</asp:DropDownList>
</td>
<td width="100" align="center" class="hell">
<asp:Button ID="C_FlugSuchen" runat="server" Text="Suchen"
OnClick="C_FlugSuchen_Click" />
</td>
</tr>
</table>
<asp:GridView ID="C_GefundeneFluege" runat="server"
AutoGenerateColumns="False" DataKeyNames="ID"
CellPadding="4" GridLines="None" EnablePersistedSelection="true">
<Columns>
<asp:CommandField ButtonType="Link" HeaderText="Auswahl"
SelectText="Wählen" ShowSelectButton="true" />
<asp:BoundField DataField="ID" HeaderText="Flug-ID" />
<asp:BoundField DataField="Route" HeaderText="Flugroute" />
<asp:BoundField DataField="Datum" HeaderText="Datum"
DataFormatString="{0:d}" />
<asp:BoundField DataField="Datum" HeaderText="Uhrzeit"
DataFormatString="{0:t}" />
<asp:BoundField DataField="FreiePlaetze" HeaderText
="Anzahl freier Plätze" />
</Columns>
<HeaderStyle CssClass="hell" />
<SelectedRowStyle CssClass="dunkel" />
</asp:GridView>
</asp:Panel>
<!-- #####################################################
Sektion 2: Passagiersuche -->
<asp:Panel ID="Panel2" runat="server" BorderStyle="None"
BorderWidth="1px" GroupingText="Passagier">
<table class="Tabelle">
<tr>
<td class="hell" align="center" Width="80">
Passagier-ID
</td>
<td class="dunkel" align="center" Width="60" >
<asp:TextBox ID="C_PassagierID" runat="server" Width="50"
AutoPostBack="True" OnTextChanged="C_PassagierSuchen_Click">
</asp:TextBox>
</td>
<td class="hell" align="center">
Namensbestandteil
</td>
<td class="dunkel" align="center" Width="200">
<asp:TextBox ID="C_Passagiername" runat="server" Width="200"
AutoPostBack="True" OnTextChanged="C_PassagierSuchen_Click"
Text="Müller"></asp:TextBox>
</td>
<td width="100" class="hell" align="center">
<asp:Button ID="C_PassagierSuche" runat="server" Text="Suchen"
OnClick="C_PassagierSuchen_Click" />
</td>
<td width="100" class="hell" align="center">
<asp:Button ID="C_NeuerPassagier" runat="server" Text="Neuer
Passagier" OnClick="C_NeuerPassagier_Click" />
</td>
</tr>
</table>
<asp:GridView ID="C_GefundenePassagiere" runat="server"
AutoGenerateColumns="False"
DataKeyNames="ID" CellPadding="4" GridLines="None"
EnablePersistedSelection="true">
<Columns>
<asp:CommandField ButtonType="Link" HeaderText="Auswahl"
SelectText="Wählen" ShowSelectButton="true" />
<asp:BoundField DataField="ID" HeaderText="Passagier-ID" />
<asp:BoundField DataField="GanzerName" HeaderText="GanzerName" />
<asp:BoundField DataField="Geburtsdatum" HeaderText="Geburtsdatum"
DataFormatString="{0:d}" />
</Columns>
<HeaderStyle CssClass="hell" />
<SelectedRowStyle CssClass="dunkel" />
</asp:GridView>
</asp:Panel>
<!-- #####################################################
Sektion 3: Buchen-Aktion -->
<asp:Panel ID="Panel3" runat="server" BorderStyle="None"
BorderWidth="1px" GroupingText="Aktionen">
<asp:Button ID="C_Buchen" runat="server" OnClick="C_Buchen_Click"
Text="Buchen" Enabled="False" />
<asp:Label ID="C_BuchenErgebnis" runat="server" Font-Bold="true"></asp:Label>
</asp:Panel>
</asp:Content>

Listing 3

Listing 3: Buchung.aspx.cs

using System;
using System.Collections.ObjectModel;
using System.Linq;
using WWWings_Dienstproxies.WWWingsServer;
using WWWings_GO;

public partial class Buchung : System.Web.UI.Page
{

Passagier Passagier;
Flug Flug;
ObservableCollection<Flug> Fluege;
ObservableCollection<Passagier> Passagiere;
BuchungsServiceClient client;
// -----------------------------------------

protected void Page_Load(object sender, EventArgs e)
{
client = new BuchungsServiceClient();

if (!Page.IsPostBack)
{ // Auswahlfelder befüllen, das aber nur beim ersten Laden der Seite!
var Flughaefen = client.GetFlughaefen();
this.C_Abflugort.DataSource = Flughaefen;
this.C_Zielort.DataSource = Flughaefen;
this.DataBind();
}

// Hier wird noch was eingefügt werden
}

protected void Page_Unload(object sender, EventArgs e)
{
client.Close();
}


// ------------------------------------------
protected void C_FlugSuchen_Click(object sender, EventArgs e)
{
this.C_BuchenErgebnis.Text = "";
int FNr;
if (Int32.TryParse(this.C_Flugnummer.Text, out FNr))
{
Flug = client.GetFlug(FNr);
Fluege = new ObservableCollection<Flug> { Flug };
}
else
{
Fluege = client.GetFluege(this.C_Abflugort.Text, this.C_Zielort.Text);
}

// Datenbindung an Tabelle
this.C_GefundeneFluege.DataSource = Fluege;
this.C_GefundeneFluege.DataBind();
}

// -------------------------------------------
protected void C_PassagierSuchen_Click(object sender, EventArgs e)
{
this.C_BuchenErgebnis.Text = "";
int PID;
if (Int32.TryParse(this.C_PassagierID.Text, out PID))
{
Passagier = client.GetPassagier(PID);
if (Passagier != null) Passagiere = new ObservableCollection<Passagier>
{ Passagier };
}
else
{
Passagiere = client.GetPassagiere(this.C_Passagiername.Text);
}

if (Passagiere != null) Passagiere =
new ObservableCollection<WWWings_GO.Passagier>
(Passagiere.OrderBy(x => x.GanzerName));

// Datenbindung an Tabelle
this.C_GefundenePassagiere.DataSource = Passagiere;
this.C_GefundenePassagiere.DataBind();

}

// --------------------------------------------
protected void C_Buchen_Click(object sender, EventArgs e)
{
// Wurde etwas ausgewählt?
if (this.C_GefundeneFluege.SelectedIndex < 0 ||
this.C_GefundenePassagiere.SelectedIndex < 0)
{
this.C_BuchenErgebnis.Text = "Auswahl nicht korrekt";
return;
}

// Ermittele Schlüsselwerte der gewählten Zeilen
int? FlugID = (int)this.C_GefundeneFluege.SelectedDataKey[0];
int? PassagierID = (int)this.C_GefundenePassagiere.SelectedDataKey[0];

try
{
string ergebnis = client.CreateBuchung(FlugID.Value, PassagierID.Value);
if (ergebnis == "OK")
{
this.C_BuchenErgebnis.Text = "Buchung erfolgreich!";
this.C_BuchenErgebnis.ForeColor = System.Drawing.Color.Green;
}
else
{
this.C_BuchenErgebnis.Text = ergebnis;
this.C_BuchenErgebnis.ForeColor = System.Drawing.Color.Red;
}
}
catch (Exception ex)
{
this.C_BuchenErgebnis.Text = "Fehler beim Aufruf der Buchungsfunktion:
" + ex.Message;
this.C_BuchenErgebnis.ForeColor = System.Drawing.Color.Red;
}

}
// ---------------------------------------------
protected void Page_PreRender(object sender, EventArgs e)
{
// Schaltfläche aktivieren, wenn Buchung möglich!
this.C_Buchen.Enabled = (this.C_GefundeneFluege.SelectedIndex
>= 0 && this.C_GefundenePassagiere.SelectedIndex >= 0);
}
// ---------------------------------------------
protected void C_NeuerPassagier_Click(object sender, EventArgs e)
{
// Folgt später!
}
}

Listing 4

Listing 4: Buchung.aspx mit "AJAX light"

<%@ Page Language="C#" MasterPageFile="~/WWWings.master" 
AutoEventWireup="true" CodeFile="Buchung.aspx.cs"
Inherits="Buchung" Title="Flugbuchung" %>

<asp:Content ID="Content1" ContentPlaceHolderID="C_Ueberschrift"
runat="server">
Flugbuchung
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="C_Inhalt" runat="Server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
<!-- #####################################################
Sektion 1: Flugsuche -->
<asp:Panel ID="Panel1" runat="server" GroupingText="Flug">
<table class="Tabelle">
<tr>
<td class="hell" align="center" width="80">
Flug-ID
</td>
<td class="dunkel" align="center" width="60">
<asp:TextBox ID="C_Flugnummer" runat="server" Width="50"
AutoPostBack="True" OnTextChanged="C_FlugSuchen_Click"
Text=""></asp:TextBox>
</td>
<td class="hell" align="center">
Abflugort
</td>
<td class="dunkel" align="center">
<asp:DropDownList ID="C_Abflugort" runat="server" Width="160"
AppendDataBoundItems="true"
AutoPostBack="True" OnSelectedIndexChanged="C_FlugSuchen_Click">
<asp:ListItem Text="Alle" Value=""></asp:ListItem>
</asp:DropDownList>
</td>
<td class="hell" align="center">
Zielort
</td>
<td class="dunkel" align="center">
<asp:DropDownList ID="C_Zielort" runat="server" Width="160"
AppendDataBoundItems="true"
AutoPostBack="True" OnSelectedIndexChanged="C_FlugSuchen_Click">
<asp:ListItem Text="Alle" Value=""></asp:ListItem>
</asp:DropDownList>
</td>
<td width="100" align="center" class="hell">
<asp:Button ID="C_FlugSuchen" runat="server" Text="Suchen"
OnClick="C_FlugSuchen_Click" />
</td>
</tr>
</table>
<asp:UpdatePanel ID="C_GefundeneFluege_UpdatePanel" runat="server">
<ContentTemplate>
<asp:GridView ID="C_GefundeneFluege" runat="server"
AutoGenerateColumns="False" DataKeyNames="ID"
CellPadding="4" GridLines="None" EnablePersistedSelection="true">
<Columns>
<asp:CommandField ButtonType="Link" HeaderText="Auswahl"
SelectText="Wählen" ShowSelectButton="true" />
<asp:BoundField DataField="ID" HeaderText="Flug-ID" />
<asp:BoundField DataField="Route" HeaderText="Flugroute" />
<asp:BoundField DataField="Datum" HeaderText="Datum"
DataFormatString="{0:d}" />
<asp:BoundField DataField="Datum" HeaderText="Uhrzeit"
DataFormatString="{0:t}" />
<asp:BoundField DataField="FreiePlaetze" HeaderText="Anzahl
freier Plätze" />
</Columns>
<HeaderStyle CssClass="hell" />
<SelectedRowStyle CssClass="dunkel" />
</asp:GridView>
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="C_Flugnummer"
EventName="TextChanged" />
<asp:AsyncPostBackTrigger ControlID="C_Abflugort"
EventName="SelectedIndexChanged" />
<asp:AsyncPostBackTrigger ControlID="C_Zielort"
EventName="SelectedIndexChanged" />
<asp:AsyncPostBackTrigger ControlID="C_FlugSuchen"
EventName="Click" />
</Triggers>
</asp:UpdatePanel>
</asp:Panel>
<!-- #####################################################
Sektion 2: Passagiersuche -->
<asp:Panel ID="Panel2" runat="server" BorderStyle="None"
BorderWidth="1px" GroupingText="Passagier">
<table class="Tabelle">
<tr>
<td class="hell" align="center" width="80">
Passagier-ID
</td>
<td class="dunkel" align="center" width="60">
<asp:TextBox ID="C_PassagierID" runat="server" Width="50"
AutoPostBack="True" OnTextChanged="C_PassagierSuchen_Click">
</asp:TextBox>
</td>
<td class="hell" align="center">
Namensbestandteil
</td>
<td class="dunkel" align="center" width="200">
<asp:TextBox ID="C_Passagiername" runat="server"
Width="200" AutoPostBack="True"
OnTextChanged="C_PassagierSuchen_Click" Text="Müller"></asp:TextBox>
</td>
<td width="100" class="hell" align="center">
<asp:Button ID="C_PassagierSuche" runat="server" Text="Suchen"
OnClick="C_PassagierSuchen_Click" />
</td>
<td width="100" class="hell" align="center">
<asp:Button ID="C_NeuerPassagier" runat="server" Text="Neuer
Passagier" OnClick="C_NeuerPassagier_Click" />
</td>
</tr>
</table>
<asp:UpdatePanel ID="C_GefundenePassagiere_UpdatePanel" runat="server">
<ContentTemplate>
<asp:GridView ID="C_GefundenePassagiere" runat="server"
AutoGenerateColumns="False"
DataKeyNames="ID" CellPadding="4" GridLines="None"
EnablePersistedSelection="true">
<Columns>
<asp:CommandField ButtonType="Link" HeaderText="Auswahl"
SelectText="Wählen" ShowSelectButton="true" />
<asp:BoundField DataField="ID" HeaderText="Passagier-ID" />
<asp:BoundField DataField="GanzerName" HeaderText="GanzerName" />
<asp:BoundField DataField="Geburtsdatum" HeaderText="Geburtsdatum"
DataFormatString="{0:d}" />
<asp:BoundField DataField="Passagierstatus" HeaderText="Status" />
</Columns>
<HeaderStyle CssClass="hell" />
<SelectedRowStyle CssClass="dunkel" />
</asp:GridView>
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="C_Passagiername"
EventName="TextChanged" />
<asp:AsyncPostBackTrigger ControlID="C_PassagierID"
EventName="TextChanged" />
<asp:AsyncPostBackTrigger ControlID="C_PassagierSuche"
EventName="Click" />
</Triggers>
</asp:UpdatePanel>
</asp:Panel>
<!-- #####################################################
Sektion 3: Buchen-Aktion -->
<asp:Panel ID="Panel3" runat="server" BorderStyle="None"
BorderWidth="1px" GroupingText="Aktionen">

<asp:UpdatePanel ID="C_Buchen_UpdatePanel" runat="server">
<ContentTemplate>
<asp:Button ID="C_Buchen" runat="server" OnClick="C_Buchen_Click"
Text="Buchen" Enabled="False" />
<asp:Label ID="C_BuchenErgebnis" runat="server" Font-Bold="true">
</asp:Label>
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="C_Buchen" EventName="Click" />
<asp:AsyncPostBackTrigger ControlID="C_GefundeneFluege"
EventName="SelectedIndexChanged" />
<asp:AsyncPostBackTrigger ControlID="C_GefundenePassagiere"
EventName="SelectedIndexChanged" />
</Triggers>
</asp:UpdatePanel>
</asp:Panel>
</asp:Content>

Listing 5

Listing 5: NeuerPassagier.aspx

<%@ Page Title="Neuer Passagier" Language="C#" MasterPageFile=
"~/WWWings.master" AutoEventWireup="true" CodeFile=
"NeuerPassagier.aspx.cs" Inherits="NeuerPassagier" %>

<asp:Content ContentPlaceHolderID="C_Ueberschrift" runat="server">
Neuer Passagier
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="C_Inhalt" runat="Server">
<!-- #################### Datenquelle ########################### -->

<asp:ObjectDataSource ID="ODS" runat="server"
DataObjectTypeName="WWWings_GO.Passagier"
TypeName="NeuerPassagierManager" SelectMethod="Laden"
InsertMethod="Einfuegen">
</asp:ObjectDataSource>
<!-- #################### DetailsView ########################### -->
<asp:DetailsView ID="C_Passagier" runat="server" AutoGenerateRows="False"
DefaultMode="Insert"
DataKeyNames="ID" DataSourceID="ODS" Width="300px"
OnModeChanging="C_Passagier_ModeChanging">
<CommandRowStyle CssClass="dunkel" />
<RowStyle CssClass="hell" />
<Fields>
<!-- #################### ID ########################### -->
<asp:BoundField DataField="ID" HeaderText="ID" ReadOnly="true"
InsertVisible="false" />
<!-- #################### Vorname ########################### -->
<asp:TemplateField HeaderText="Vorname">
<ItemTemplate>
<asp:Label ID="C_VornameAusgabe" runat="server" Text='<%#
Bind("Vorname") %>'>
</asp:Label>
</ItemTemplate>
<EditItemTemplate>
<asp:TextBox ID="C_VornameEingabe" runat="server" Text='<%#
Bind("Vorname") %>'>
</asp:TextBox>
<asp:RequiredFieldValidator ID="RequiredFieldValidator_Vorname"
runat="server" ControlToValidate="C_VornameEingabe"
ErrorMessage="*"></asp:RequiredFieldValidator>
</EditItemTemplate>
</asp:TemplateField>
<!-- #################### Name ########################### -->
<asp:TemplateField HeaderText="Name">
<ItemTemplate>
<asp:Label ID="C_NameAusgabe" runat="server" Text='<%#
Bind("Name") %>'>
</asp:Label>
</ItemTemplate>
<EditItemTemplate>
<asp:TextBox ID="C_NameEingabe" runat="server" Text='<%#
Bind("Name") %>'>
</asp:TextBox>
<asp:RequiredFieldValidator ID="RequiredFieldValidator_Name"
runat="server" ControlToValidate="C_NameEingabe"
ErrorMessage="*"></asp:RequiredFieldValidator>
</EditItemTemplate>
</asp:TemplateField>
<!-- #################### Geburtsdatum ########################### -->
<asp:TemplateField HeaderText="Geburtsdatum">
<ItemTemplate>
<asp:Label ID="C_GeburtsdatumAusgabe" runat="server" Text='<%#
Bind("Geburtsdatum") %>'>
</asp:Label>
</ItemTemplate>
<EditItemTemplate>
<asp:TextBox ID="C_GeburtsdatumEingabe" runat="server" Text='<%#
Bind("Geburtsdatum") %>'>
</asp:TextBox>
<asp:RegularExpressionValidator ID="RequiredFieldValidator_Geburtsdatum"
runat="server"
ControlToValidate="C_GeburtsdatumEingabe" ValidationExpression="
[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{4,4}"
ErrorMessage="*"></asp:RegularExpressionValidator>
</EditItemTemplate>
</asp:TemplateField>
<!-- #################### Passagier-Status ########################### -->
<asp:TemplateField HeaderText="Passagier-Status">
<ItemTemplate>
<asp:Label ID="StatusLabel" runat="server" Text='<%#
Bind("PassagierStatus") %>'>
</asp:Label>
</ItemTemplate>
<EditItemTemplate>
<asp:DropDownList ID="StatusAuswahl" runat="server"
SelectedValue='<%# Bind("PassagierStatus") %>'>
<asp:ListItem Value="A"></asp:ListItem>
<asp:ListItem Value="B"></asp:ListItem>
<asp:ListItem Value="C"></asp:ListItem>
</asp:DropDownList>
</EditItemTemplate>
</asp:TemplateField>
<!-- #################### Befehle ########################### -->
<asp:CommandField CancelText="Abbrechen" InsertText="Speichern"
ShowInsertButton="True" />
</Fields>
</asp:DetailsView>
</asp:Content>

Listing 6

Listing 6: Erweiterungen für Buchung.aspx.cs

protected void Page_Load(object sender, EventArgs e)
{
...

// Wenn ein Passagier neu angelegt wurde, zeige diesen an
if (Session["Passagier"] != null && Session["Passagier"] is Passagier)
{
Passagiere = new ObservableCollection<Passagier> { Session["Passagier"]
as Passagier };

// Datenbindung an Tabelle
this.C_GefundenePassagiere.DataSource = Passagiere;
this.C_GefundenePassagiere.DataBind();
// wähle diesen Passagier aus
this.C_GefundenePassagiere.SelectedIndex = 0;
// setze die Merk-Variable zurück
Session["Passagier"] = null;

}

// Wenn ein Flug vor dem Anlegen des Passagiers ausgewählt war,
// zeige diesen an
if (Session["GewaehlterFlug"] != null)
{
Flug = client.GetFlug((int)Session["GewaehlterFlug"]);
Fluege = new ObservableCollection<Flug> { Flug };
// Datenbindung an Tabelle
this.C_GefundeneFluege.DataSource = Fluege;
this.C_GefundeneFluege.DataBind();
// wähle diesen Flug aus
this.C_GefundeneFluege.SelectedIndex = 0;
// setze die Merk-Variable zurück
Session["GewaehlterFlug"] = null;
}
}

protected void C_NeuerPassagier_Click(object sender, EventArgs e)
{
// gewählten Flug merken
if (this.C_GefundeneFluege.SelectedIndex > 0) Session["GewaehlterFlug"]
= (int)this.C_GefundeneFluege.SelectedDataKey[0];
// Weitergabe an neue Seite
Response.Redirect("NeuerPassagier.aspx");
}