Eingestimmt
Zu einer Softwareinfrastruktur gehört mehr als nur eine Programmiersprache. Teil von .NET sind daher diverse Bibliotheken, die unter anderem Datenbankzugriffe und die Bereitstellung von Web Services ermöglichen.
- Michael Stal
Die zwei ersten Teile des Tutorials haben sich den Grundkonzepten und fortgeschrittenen Sprachmerkmalen von C# gewidmet. Mit Vorkenntnissen in Java oder C++ gestaltet sich das Erlernen dieser neuen Programmiersprache nicht allzu schwierig. Innovatives wie erweiterbare Metainformation, Delegates und Events oder Nebenläufigkeitsmechanismen erlauben es dem Entwickler, selbst komplizierte Entwurfsanforderungen elegant umzusetzen. Daher gilt C# als die Systemsprache für die .NET-Programmierung.
C# zu lernen ist eine (leichte) Sache, der Umgang mit den umfangreichen Framework-Klassen von .NET eine andere. Diese erst stellen Funktionen zum Bau von Windows-Programmen, XML-Anwendungen, ASP.NET-Webseiten, Web Services und Datenbankzugriffen zur Verfügung. Der dritte und letzte Teil des Tutorials unternimmt deshalb einen Streifzug durch die Bibliotheken des .NET Framework. Dieser kann zwar keine tiefen Einblicke vermitteln, aber zumindest einen Vorgeschmack geben.
Windows ist tot - es lebe Windows
Trotz Internet und XML - die wichtigste Aufgabe eines Programmierers unter Microsoft Windows bleibt die Entwicklung von visuellen, interaktiven Programmen. Bisher war die Auswahl von Werkzeugen zur Windows-Programmierung heterogen. Die Palette reichte vom systemnahen Aufruf der Win32-Schnittstellen über MFC, ATL bis hin zu Produktivitätswerkzeugen à la Delphi und Visual Basic. Selbst der eingefleischte Visual-Studio-Programmierer hatte sich allerdings mit unterschiedlichen Sprachen, Bibliotheken und Konzepten herumzuschlagen. Microsoft.NET bereitet dieser eher produktivitätshemmenden Vielfalt ein Ende.
Im Namensraum System.Windows.Forms bietet .NET Funktionen für die Erstellung von Windows-Oberflächen. Die Konstruktion eines Programms gestaltet sich denkbar einfach. Die Programmiererin leitet eine eigene Unterklasse von der Basisklasse Form ab, legt die benötigten Controls (siehe Abb. 1) als geschützte Instanzvariablen an, die sie im Konstruktor instanziiert. Idealerweise übernimmt eine separate Initialisierungsroutine die Konfiguration der Control-Eigenschaften. Alle Controls müssen anschließend der Control-Menge der Form hinzugefügt weden, die übrigens selbst eine (Container-)Control repräsentiert. Zu guter Letzt sind die Ereignisbehandlungsmethoden für Benutzerinteraktionen zu definieren und anzumelden sowie die Hauptereignisschleife zu starten.
Ein einfaches Programm soll dieses Vorgehen demonstrieren (siehe Abb. 2 und Listing 1). Es gibt lediglich einen Button auf einer Form aus, dessen Betätigung zum Ändern des Titels des Hauptfensters führt. Die Anwendung ist weitgehend selbsterklärend. Die private Instanzvariable button1 speichert das Kontrollelement. Instanziierung und Initialisierung ihrer Eigenschaften erfolgen am Anfang der Methode InitializeComponent. Dort wird auch die Ereignisbehandlungsmethode button1_Click beim Kontrollelement button1 angemeldet. Am Schluss der Initialisierung macht das Programm über AddRange das einzige Kontrollelement button1 dem übergeordneten Container bekannt. Im Hauptprogramm Main wird eine Instanz der Form angelegt und mittels Application.Run() die Hauptereignisschleife betreten, in der die kontrollierte Abarbeitung und Verteilung von Windows-und Anwendungsereignissen erfolgen. Business as usual sozusagen.
Listing 1
using System.Windows.Forms;
...
namespace SimpleSample {
public class Form1 : System.Windows.Forms.Form
{
private System.Windows.Forms.Button button1;
private System.ComponentModel.Container components = null;
public Form1() { // Konstruktor des Hauptfensters
this.Text = "Start"; // Anfangstext im Titel
InitializeComponent();
}
private void InitializeComponent() {
this.button1 = new System.Windows.Forms.Button(); // neuer Knopf
this.button1.Location = new System.Drawing.Point(104, 8);
this.button1.Size = new System.Drawing.Size(88, 16);
this.button1.Text = "press and see!"; // das steht drauf!
// Der Eventhandler wird angemeldet:
this.button1.Click += new System.EventHandler(this.button1_Click);
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(292, 37);
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.button1});
}
static void Main() {
Application.Run(new Form1()); // Hauptereignisschleife starten
}
private void button1_Click(object sender, System.EventArgs e) {
this.Text = "Button Pressed"; // Titel des Hauptfensters ändern
}
}
}
Aus den Basisingredienzen Forms, Controls, Eventhandler und Ereignisschleife lassen sich leistungsfähige GUIs komponieren, die in nichts ihren MFC-Vettern nachstehen. Nach oben gibt es keine Grenze. Zusätzliche eigene Controls kann der Entwickler über Vererbung von der Basisklasse UserControl erstellen. Für ausgefeilte Grafikausgaben stehen weitere Pakete wie GDI+ zur Verfügung.
Ran an die Daten mit ADO.NET
ADO.NET (ADO = ActiveX Data Objects) enthält Klassen, um von Client-Anwendungen auf Datenquellen (Data Source) zuzugreifen. Wer bereits Erfahrungen mit ADO sammeln konnte, speziell mit Disconnected Recordsets, dürfte das Web- und XML-orientierte ADO.NET schnell durchdringen. Zunächst zu den einzelnen architekturellen Konzepten (siehe Abb. 3).
Ein DataSet definiert eine fundamentale Struktur, um Datenbanktabellen im Speicher der Client-Anwendung zu halten. Sogar ganze Datenbanken lassen sich dort in den Cache laden. Im Gegensatz zu ADO-Recordsets ist auch die Verwaltung von Relationen zwischen den gespeicherten Tabellen möglich. DataSets merken sich ihren jeweiligen Zustand und erlauben die Synchronisation (Update) mit der physikalischen Datenquelle. Die produktabhängigen Managed Providers (siehe unten) sind dafür verantwortlich, die Kommunikation zwischen Client und Datenquelle zu implementieren. Eine Connection sorgt für die Verbindung zur Datenquelle. Mittels eines Command lassen sich Befehle an die Datenquelle senden, etwa SQL-Befehle oder Aufrufe von Stored Procedures. Aufgabe des DataSetCommand ist der Datentransfer zwischen DataSet und Datenquelle. Alternativ zur Nutzung von DataSets können Daten über eine Art Read-Only-, Forward-Only-Cursor gelesen werden, dem so genannten DataReader.
Im Augenblick gibt es je einen Managed Provider für den SQL Server ab Version 7 sowie einen für den Zugriff auf beliebige OLE-DB-Quellen. Auf ADO- und OLE-DB-Experten muss dies zunächst verwunderlich wirken, war Microsoft doch dereinst mit dem Vorsatz angetreten, alle Datenquellen hinter dem Mantel der Abstraktion zu verbergen. Ganz weggefallen ist dieser Gedanke nicht: Wer eigene Managed Provider zur Verfügung stellen möchte, muss die abstrakten Basisklassen System.Data.Internal.{DataSetCommand, DBConnection, DBCommand, DBDataReader} konkretisieren. Dadurch ist bis auf Präfixe in den Komponentenbezeichnungen der Umgang mit Managed Providern weitgehend identisch.
Folgendes Beispiel soll die Nutzung des SQL-Server-basierten Managed Provider veranschaulichen. Zunächst spezifiziert das Programm die benötigten Namensräume:
using System;
using System.Data;
using System.Data.SqlClient;
Mittels eines DataSetCommand-Objekts baut die Anwendung implizit eine Verbindung zum Server auf, der eine Datenbank mit Aktienwerten enthält, und führt dort ein SELECT-Kommando aus. Alternativ wäre es möglich, explizit mit Objekten wie SqlCommand und SqlConnection zu arbeiten:
string myConnection = "server=myserver;uid=sa;pwd=; database=StockTickerDB";
string myCommand = "SELECT * from StockTable";
SqlDataSetCommand datasetCommand = new SqlDataSetCommand(myCommand, myConnection);
Anschließend erfolgt die Instanziierung eines DataSets, in das sich mit FillData die komplette Datentabelle einlesen lässt:
DataSet myDataSet = new DataSet();
datasetCommand.FillDataSet(myDataSet, "StockTable");
Nun greift die Anwendung auf die erzeugte Tabelle über Indexerzugriff auf den DataSet zu (es können sich dort auch mehrere Tabellen befinden):
DataTable myTable = ds.Tables["StockTable"];
Im letzten Schritt durchläuft das Programm die eingelesenen Datensätze der Tabelle in einer foreach-Schleife und gibt sie am Bildschirm aus:
foreach (DataRow row in myTable.Rows) {
Console.WriteLine("Value of {0} is {1}",
row["LongName"], row["Value"]);
}
Wer statt des DataSet einen DataReader benutzen möchte, geht folgendermaßen vor (Belegung von myConnection und myCommand, siehe oben):
SqlConnection conn = new SqlConnection(myConnection);
conn.Open();
SqlCommand cmd = new SqlCommand(myCommand, conn);
SqlDataReader rdr = cmd.ExecuteReader();
while (rdr.Read()) {
Console.WriteLine("Value of {0} is {1}",
rdr["longName"], rdr["Value"]);
}
Unter Verwendung von DataSets ist sogar das dynamische Erzeugen eines Datenbankschemas möglich:
DataSet myDataSet = new DataSet();
DataTable persons = new DataTable("persons");
persons.Columns.Add(new DataColumn("last_name", typeof(string)));
persons.Columns.Add(new DataColumn("first_name", typeof(string)));
persons.Columns.Add(new DataColumn("age", typeof(short)));
myDataSet.Tables.Add(persons);
Alle Daten eines DataSet lassen sich mit XML in eine Datei schreiben:
myDataSet.WriteXml("MyFile.XML");
oder von dort wieder einlesen:
myDataSet.ReadXml("MyFile.XML");
Das waren nur einige der Möglichkeiten, die ADO.NET dem Programmierer bietet. Insgesamt erweist sich der Umgang mit dieser Bibliothek als einfach. Zum Schluss ein Hinweis für Performanzverfechter: Mit dem SQL-Server-Provider ist eine deutlich bessere Performanz zu erzielen als beim Zugriff über die entsprechenden ADO-Komponenten.
C# goes XML
Dass Microsoft auf XML setzt, zeigt sich deutlich am .NET Framework. Fast alle Pakete stützen sich in irgendeiner Form auf die Extensible Markup Language und sei es nur zu Konfigurationszwecken. Der Entwicklerin stehen diverse Klassen im Namensraum System.Xml zur Verfügung, mit der sie XML-Sprachen verarbeiten kann. Hier eine Zusammenfassung:
Die Unterklassen von XmlReader und XmlWriter implementieren eine Art Pull-Variante der SAX-API. Im Gegensatz zu SAX triggert bei XmlReader die Anwendung die Verarbeitung der XML-Instanz, nicht der Parser. Das kommt vielen Programmierern mehr entgegen als ereignisgesteuerte Push-Ansätze. Ein triviales Beispiel ist die Ausgabe aller Textknoten:
XmlTextReader rdr = new XmlTextReader(myFile);
while (rdr.Read()) {
if (rdr.NodeType == XmlNodeType.Text)
Console.WriteLine(rdr.Value);
}
Der XmlReader implementiert damit einen schnellen Read-Only-Cursor zum Navigieren über XML-Strukturen. Neben XmlReader und XmlWriter unterstützt .NET das reine DOM (Document Object Model), das Programmierer über die Klasse XmlNode nutzen können.
Zum Zugriff auf XML-Dokumente über XPath-Ausdrücke gibt es XPathDocument in System.Xml.XPath. Die Navigationsfähigkeit verbirgt sich speziell in der Klasse XPathNavigator.
Die Transformation von Dokumenten über XSLT ist dank der Klasse XslTransform möglich, die dazu eine Instanz des Typs IXPathNavigable erwartet, wie ihn ein XPathNavigator liefern kann.
Auch hier soll ein Beispiel Licht ins Dunkel des Zusammenspiels all dieser Klassen bringen. Aufgabe ist es, ein XML-Dokument als HTML-Tabelle aufzubereiten. Das Quelldokument enthält eine Auflistung diverser Aktienkurse. Auf das XML-Dokument in Listing 2 wird die XSLT-Transformation in Listing 3 angewendet.
Listing 2, Listing 3, Listing 4
Die XSLT-Transformation (Listing 3) bereitet das XML-Dokument (Listing 2) als HTML-Tabelle auf. Listing 4 übernimmt die Steuerung dieses Vorgangs.
Listing 2
<?xml version="1.0" encoding="utf-8" ?>
<stocklist>
<stock id="MSFT">
<company> Microsoft </company>
<value> 63 </value>
</stock>
<stock id="CSCO">
<company> Cisco </company>
<value> 22 </value>
</stock>
<stock id="INTL">
<company> Intel </company>
<value> 27 </value>
</stock>
</stocklist>
Listing 3
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
<head><title> List of Stocks </title></head>
<body> <h1> <center> List of different stocks </center> </h1> <br/>
<table width="200pt" border="1pt" align="center">
<tr><td>ID</td><td>Company</td><td>Value</td></tr>
<xsl:for-each select="stocklist/stock">
<xsl:sort select="@id" />
<tr>
<td><xsl:value-of select="@id" /></td>
<td><xsl:value-of select="company" /></td>
<td><xsl:value-of select="value" /></td>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
Listing 4
using System.IO;
using System.Xml.XPath;
using System.Xml.Xsl;
namespace XMLExample {
class TestXML {
static void Main(string[] args) {
XPathDocument myDoc = new XPathDocument("stocklist.xml");
XslTransform myXslt = new XslTransform();
myXslt.Load("stockview.xslt");
FileStream fs = new FileStream("result.html", FileMode.Create);
XPathNavigator myNav = ((IXPathNavigable)myDoc).CreateNavigator();
myXslt.Transform(myNav, null, fs);
}
}
}
Um dies programmtechnisch in .NET zu bewerkstelligen, genügt ein einfaches Programm. Zunächst bedarf es des Zugriffs auf das XML-Dokument durch eine Instanz vom Typ XPathDocument. Die XSLT-Instanz wird in ein XslTransform-Objekt geladen. Ein Navigationsobjekt vom Typ XpathNavigator dient zum navigierenden Zugriff auf das XML-Dokument. Mittels myXslt.Transform() erfolgt die eigentliche Transformation. Das Ergebnis ist eine nach der Kurzbezeichnung der Aktie sortierte HTML-Tabelle.
Nützlich sind die XML-Klassen insbesondere im Zusammenspiel mit ADO.NET, wo sich beispielsweise DataSets aus XML-Dateien einlesen oder in solche schreiben lassen. Die Menge der Möglichkeiten scheint schier unerschöpflich. Eine andere potenzielle Anwendung von XML besteht in der Serialisierung von Objekten, für die der Namensraum System.Xml.Serialization diverse Klassen anbietet. So lassen sich die Eigenschaften und öffentliche Felder einer Instanz in XML-Elemente oder -Attribute auslesen und umgekehrt (Listing 5).
Listing 5
Eigenschaften von Objektinstanzen lassen sich als XML-Elemente speichern und bei Bedarf wieder auslesen.
MyClass obj = new MyClass(...)
// modify the object obj according to your needs
TextWriter txt = new StreamWriter("persistdata.xml");
XmlSerializer serial = new XmlSerializer(typeof(MyClass));
Serial.Serialize(txt, obj);
txt.Close();
// ... Tage spaeter:
FileStream fs_read = new FileStream("persistdata.xml", FileMode.Open);
XmlSerializer serial_read = new XmlSerializer(typeof(MyClass));
MyClass read_obj = (MyClass) serial_read.Deserialize(fs_read);
fs_read.Close();
Es ist ersichtlich, dass XML im .NET Framework auf breiter Basis Unterstützung findet. Wer die Markup-Sprache in seinen Programmen verwenden möchte, kann in .NET eine ganze Reihe von Bibliotheksklassen und diverse Werkzeuge nutzen.
Interaktive Webseiten mit ASP.NET
Längst vorbei die Zeiten, als Entwickler ihre HTML-Seiten manuell editieren und Benutzerinteraktionen mühsam mit HTML-Forms und CGI-Scripts realisieren mussten. In Zeiten des E-Business bedarf es produktiverer Ansätze. Heutzutage gehören daher Microsofts ASP (Active Server Pages) und dessen Java-Pendant JSPs (Java Server Pages) zum Standardrüstzeug der Webdesigner und -entwickler. Leider hat ASP diverse Schönheitsfehler. So leidet der interpretative Ansatz unter Performanzproblemen, und nur wenige Sprachen wie VBScript und JScript sind einsetzbar. Diesen Einschränkungen bereitet ASP.NET ein Ende. ASP.NET-Seiten (Endung .aspx) bestehen aus einer Mischung von HTML und Codefragmenten, die der Compiler in ausführbaren IL-Code übersetzt. Ein im IIS oder einem anderen Webserver integrierter CLR-Runtimehost sorgt für den Ablauf dieses Codes in einer .NET-Umgebung. Ergebnis der Ausführung sind HTTP-Ressourcen, die der Webserver direkt zum Client-Browser überträgt.
Im Normalfall erfolgt die Programmierung von ASP.NET-Seiten interaktiv in IDEs wie Visual Studio.NET. Die Designerin platziert zu diesem Zweck Kontrollelemente aus einer Palette auf einer Web-Form genauso, wie sie es in Visual Basic mit Windows-Forms tun würde. Die Web-Controls erzeugen zur Laufzeit Code für die äquivalenten Controls auf der Client-Plattform. Handelt es sich um einen Webbrowser, generiert ein Textbox-Control dynamisch eine dazu äquivalente HTML-basierte Textbox. Die Menge der vordefinierten Web-Controls kann der Entwickler nach Belieben erweitern. Durch Binding-Mechanismen lassen sich Eigenschaften von Controls auch an Datenstrukturen oder Datenbanktabellen koppeln.
Ein einfacher Anwendungsfall verdeutlicht das (siehe Abb. 4). In einer Login-Seite gibt der Benutzer Name und Passwort ein und drückt anschließend auf einen Knopf. Die Seite prüft daraufhin die Zugangsberechtigung und leitet den Anwender im Erfolgsfall in den geschützten Benutzerbereich weiter. Direktiven an den Übersetzer ermittelt der Entwickler über <%@ .. >-Sequenzen. Um Code vom Entwurf zu trennen, spezifiziert die vorliegende Seite mittels CodeBehind-Attribut, dass alle C#-Codefragmente - etwa die Ereignishandler - in der Datei WebForm1.aspcx.cs liegen. Kontrollelemente wie Textboxen definiert der Entwickler über Elemente der Art
<asp:TextBox id ="name" runat="server"</asp:TextBox>.
Im C#-Programm lässt sich über die Notation name.feature auf Eigenschaften und Methoden des Controls zugreifen.
Besondere Aufmerksamkeit verdient das Kontrollelement vom Typ RequiredFieldValidator. Mit diesen Kontrollelementen lässt sich prüfen, ob Benutzer sich an vordefinierte Eingabevorgaben halten. Im Beispiel stellt es sicher, dass der Anwender einen nicht leeren Namen im Login angibt. Zusätzlich stehen weitere und mächtigere Validator-Controls zur Verfügung.
Listing 6
<%@ Page language="c#" Codebehind="WebForm1.aspx.cs" AutoEventWireup="false" Inherits="LoginPage.WebForm1" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<HTML>
<body>
<form id="Form1" method="post" runat="server">
<asp:Label id="TitleLabel" runat="server">Please specify your name and password</asp:Label> <br>
<asp:Label id="LoginLabel" runat="server">Login</asp:Label> <br>
<asp:TextBox id="LoginText" runat="server"></asp:TextBox>
<asp:RequiredFieldValidator id="RequiredFieldValidator" runat="server" ErrorMessage="You need to specify your name"
ControlToValidate="LoginText"></asp:RequiredFieldValidator> <br>
<asp:Label id="PasswordLabel" runat="server">Password</asp:Label> <br>
<asp:TextBox id="PasswordText" runat="server" TextMode="Password"></asp:TextBox> <br>
<asp:Button id="EnterButton" runat="server" Text="Open the entrance"
ToolTip="Press this after you have specified login and password"></asp:Button> <br>
<asp:Label id="MessageText" runat="server"></asp:Label>
</form>
</body>
</HTML>
Listing 7
Die in der ersten Zeile von Listing 6 genannte Datei WebForm1.aspx.cs ist für die Ereignisbehandlung der HTML-Seite zuständig.
...
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
namespace LoginPage {
public class WebForm1 : System.Web.UI.Page {
protected System.Web.UI.WebControls.TextBox PasswordText;
protected System.Web.UI.WebControls.Button EnterButton;
protected System.Web.UI.WebControls.TextBox LoginText;
protected System.Web.UI.WebControls.Label MessageLabel;
... lot of details omitted ...
private void InitializeComponent() {
this.EnterButton.Click +=
new System.EventHandler(this.EnterButton_Click);
this.Load += new System.EventHandler(this.Page_Load);
}
private void EnterButton_Click(object sender, System.EventArgs e)
{
if (!(LoginText.Text.Equals("aladdin")
&& PasswordText.Text.Equals("sesam"))) {
MessageLabel.Text = " Wrong name or password!";
}
else {
Session["user"] = "aladdin";
Response.Redirect("UserArea.aspx");
}
}
}
}
Den zugehörigen C#-Code (Datei: WebForm1.aspx.cs) für die Ereignisbehandlung zeigt Listing 7. Nicht dargestellt sind aus Platzgründen Methoden wie Page_Load oder Page_Init, deren Aktivierung erfolgt, sobald die Seite angefordert beziehungsweise zum ersten Mal geladen wird. Die Methode InitializeComponent sorgt für komponentenspezifische Initialisierungen von WebForm1 (Forms sind selbst Controls).
Das Beispiel soll die Methode EnterButton_Click aufrufen, wenn der Benutzer den entsprechenden Button betätigt. Im vorliegenden naiven Fall vergleicht die Rückrufmethode, ob sich die Inhalte der entsprechenden Textfelder mit den vorgegebenen Daten decken und leitet den Benutzer im Erfolgsfall mit Response.Redirect an die Webseite UserArea.aspx weiter. Neben Response gibt es weitere vordefinierte Variablen wie Session, um Daten zwischen den verschiedenen Webseiten einer Session zu übertragen.
Web Services - The Web is the Computer
Laut Definition von Sun Microsystems ist ein Web Service ein Dienst mit XML-Schnittstelle, der sich entweder von einem menschlichen Anwender oder einem Programm nutzen lässt. Die Kommunikation erfolgt dabei auf Basis der XML-Sprache SOAP (Simple Object Access Protocol) über HTTP oder über andere Transportprotokolle. Web Services stellen die nächste Evolutionsstufe des Web-Computing dar, weil sie ein Zusammenspiel von Funktionen über das Internet ermöglichen. Die Stichworte lauten hier B2B (Business to Business), B2C (Business to Consumer) und P2P (Peer to Peer) - und das alles unabhängig von Programmiersprachen, Betriebssystemen oder Middlewareplattformen, zumindestens in der Theorie.
Neben Kommunikationsprotokollen wie SOAP bedarf es für das Zusammenspiel von Web Services und Anwendungen weiterer Ingredienzen. Die Beschreibungssprache WSDL (Web Service Description Language) erlaubt dem Anbieter von Diensten, deren Struktur und Konfigurationsdetails zu beschreiben. Zum Finden und Anbinden der Dienste erweist sich ein globaler Service-Broker als notwendig, der mit UDDI (Universal Discovery, Description and Integration) zur Verfügung steht. Da Bill Gates als Chefwissenschaftler Web Services als das fundamentale Paradigma der heutigen IT-Welt sieht, hat sich Microsoft stark auf dieses Thema eingeschworen und war an der Entwicklung aller genannten Standards (UDDI, WSDL, SOAP) beteiligt. Das spiegelt sich naturgemäß in Microsoft .NET und der Entwicklungsumgebung Visual Studio.NET wieder.
Argumente und Rückgabewerte von Webdiensten beschränken sich nicht auf einfache Datentypen, wie Listing 8 implizieren könnte, sondern können selbst komplizierte Typen wie Klassen, Felder, DataSets (siehe ADO.NET) oder XmlNodes sein. Bei Klasseninstanzen transportiert der Marshaler allerdings nur die als public gekennzeichneten Felder und Eigenschaften.
Listing 8
...
using System.Web;
using System.Web.Services;
namespace WebService1 {
public class Service1 : System.Web.Services.WebService {
// lot of stuff omitted
[WebMethod]
public double DM_to_Euro(double value) {
return value / 1.95583;
}
[WebMethod]
public double Euro_to_DM(double value) {
return value * 1.95583;
}
}
}
Um einen Web Service zu erstellen, ist die eigene Klasse von der Basisklasse System.Web.Services.WebService abzuleiten und jede nach außen angebotene öffentliche Methode mit dem Attribut [WebMethod] zu kennzeichnen. Das alles ist auch händisch möglich, aber die Programmierumgebung Visual Studio .NET bietet erheblich mehr Komfort aufgrund der automatischen Testinstallation des Dienstes, der als Datei Service1.asmx zur Verfügung steht. Ebenso erzeugt Visual Studio .NET eine HTML-Seite, um den Dienst Browser-gesteuert aufzurufen (siehe Abb. 5).
Der programmatische Aufruf dieses Dienstes aus einer Applikation kann etwa durch
localhost.Service1 s1 = new localhost.Service1();
double result = s1.Euro_to_DM(200);
erfolgen, nachdem in VS.NET über UDDI oder lokal die zugehörige Webreferenz eingelesen und aus deren WSDL-Beschreibung die entsprechenden Proxies erzeugt wurden. Für den umständlicheren Weg über den Kommandozeilencompiler existiert dafür das Werkzeug WSDL.EXE. Befindet sich ein Web Service Service1 global auf dem Rechner myserver.domain.com, lautet der notwendige Befehl:
WSDL http://myserver.domain.com/Service1.asmx?WSDL
Übrigens erzeugt .NET zusätzliche proprietäre DISCO-Dateien (Discovery), die alle verfügbaren Web Services auf einem Rechner beschreiben.
Zusammengefasst: Mittels Web Services lassen sich Dienste programmatisch über Protokolle wie HTTP nutzen. Dadurch ist in Zukunft die Entwicklung ausgefeilter Dienste denkbar. Speziell das Zusammenspiel in B2B-Systemen ist auf diese Weise wesentlich effizienter und leichter zu realisieren.
In die Ferne schweifen mit .NET Remoting
DCOM-Gegner haben es schon immer vermutet: Das Pinging-Modell und die Architektur von DCOM lassen den Einsatz des Distributed Common Object Model für hochskalierbare vernetzte Systeme gelinde gesagt problematisch erscheinen. Das bestätigt sich in .NET, dessen Architekten nicht ohne Grund mit .NET Remoting ein völlig neues Modell für entfernte Kommunikation eingeführt haben. In .NET Remoting kommunizieren Clients transparent über Methodenaufrufe mit Servants (Serverobjekte), wobei im Hintergrund eine umfangreiche Maschinerie ins Spiel kommt (siehe Abb. 6). Beim Zugriff auf ein Serverobjekt erzeugt das System aus den verfügbaren Metainformationen einen Transparent Proxy, der dieselben Schnittstellen wie der Servant anbietet. Der Transparent Proxy ruft bei einem so genannten Real Proxy eine generische Invoke-Methode auf, die für die weitere Kommunikation sorgt.
Da der Real Proxy sich programmatisch austauschen lässt, besteht die Möglichkeit, eigene Strategien wie Caching oder Load Balancing auf diesem Weg zu ‘injizieren’. Vom Real Proxy führt der Weg des Methodenaufrufs weiter zu den Envoy Sinks, die eine hintereinander geschaltete und erweiterbare Kette von Interzeptoren bilden. Ihre Aufgabe besteht darin, zusätzliche Eigenschaften wie Sicherheit zu implementieren. Der Letzte in der Kette legt die Nachricht auf den Kanal (Channel). Dabei stehen unterschiedliche Kanaltypen zur Verfügung, in Beta 2 sind es TCP-Sockets und HTTP. Diese wiederum benutzen Formatierkomponenten, um die Nachricht ins entsprechende Protokollformat zu transformieren.
Beta 2 verfügt über einen SOAP- und einen binären Formatierer. Selbstredend ist die Infrastruktur um eigene Formatter- und Channel-Typen erweiterbar (pluggable). Auf der Empfängerseite empfangen Kanalinstanzen den Methodenaufruf und Formatierer deserialisieren ihn. Bevor sie mit dem Servant ihr endgültiges Ziel erreichen, durchlaufen die Aufrufe noch zwei Ketten so genannter Context Sinks, ihrerseits Interzeptoren, die zusätzliche Funktionen implementieren.
In diesem Zusammenhang ist wichtig zu wissen, dass Objekte in Kontexten agieren. Kontexte realisieren genau die Anforderungen, die ein Objekt an seine Umgebung hat, beispielsweise in Bezug auf Multithreading, Transaktionen, Sicherheit oder Synchronisation. Selbst wenn ein Client und ein Servant in derselben Application Domain leben, aber unterschiedliche Anforderungen an ihre Umgebung besitzen, sorgt .NET Remoting bei ihrer Kooperation durch Kontextobjekte dafür, dass der Methodenaufruf den Servant im dafür benötigten Kontext erreicht, dass also zum Beispiel mehrere Client-Threads einen Single-Thread Servant nicht mehrmals durchlaufen. Genau das leistet die Architektur von .NET Remoting.
Bezüglich der Aktivierung von Servants unterscheidet man zwischen Server- und Client-Aktivierung von Objekten. Im ersteren Fall aktiviert und deaktiviert der Server automatisch entweder bei jedem Methodenaufruf (Modus: SingleCall) oder nur einmal für alle Clients (Modus: Singleton) die Zielobjekte. Im Gegensatz dazu erhält bei Client-aktivierten Objekten (CAO) der Client Zugriff auf seinen ‘persönlichen’ Servant. Um aus Client-Abstürzen resultierende Probleme zu vermeiden, bleibt ein vom Client aktivierter Servant nur für eine bestimmte Zeit am Leben (Leasing). Bei längerem Bedarf ist diese Lease zu erneuern.
Als Beispiel für .NET Remoting implementiert Listing 9 eine Servant-Klasse, die ein zweidimensionales Grid von Gleitkommazahlen definiert. Bis auf die Tatsache, dass die Klasse von System.MarshalByRefObject erbt, ergeben sich keine Unterschiede zu nicht verteilter Programmierung. Listing 9 wird in einer DLL-Assembly installiert.
Listing 9
.NET Remoting: Die Klasse Grid erbt von System.MarshalByRefObject - ansonsten unterscheidet sie sich nicht von einer entsprechenden lokalen Klasse.
using System;
namespace RemoteObject
public class Grid : System.MarshalByRefObject {
const int LENGTH = 10;
double [,] matrix = new double[LENGTH,LENGTH];
public Grid() { Console.WriteLine("Ready!"); }
~Grid() { Console.WriteLine("Destroyed!"); }
public int Length { get { return LENGTH; } }
public double this[int i1, int i2] {
set {
if (OutOfBounds(i1,i2)) throw new ArgumentException();
else matrix[i1,i2] = value;
}
get {
if (OutOfBounds(i1,i2)) throw new ArgumentException();
else return matrix[i1,i2];
}
}
bool OutOfBounds(int i1, int i2) {
return (i1 < 0) || (i1 >= LENGTH) ||
(i2 < 0) || (i2 >= LENGTH) ? true : false;
}
}
}
Ähnlich einfach ist das Treiberprogramm für den Server. Zunächst öffnet der Server einen TCP-Kanal und meldet ihn bei ChannelServices an. Dort lassen sich weitere Kanäle unterschiedlicher Priorität registrieren. Dem Konfigurationsmanager RemotingConfiguration teilt das Programm abschließend mit, dass der Servant-Objekttyp typeof(Grid) nach außen unter dem Namen ‘Grid’ bekannt sein soll und für alle Client-Anfragen ein einziges Objekt zur Verfügung steht (singleton). Anschließend wartet der Server in einer Endlosschleife auf eingehende Aufrufe (Listing 10).
Listing 10
Das Treiberprogramm für den Server: Nach außen stellt sich der Objekttyp des Servant als ´Grid´ dar.
using System;
using RemoteObject;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting;
namespace ServerDomain {
class Server {
static void Main(string[] args) {
TcpServerChannel ch = new TcpServerChannel(8888);
ChannelServices.RegisterChannel(ch);
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(Grid), "Grid", WellKnownObjectMode.Singleton);
Console.WriteLine("Waiting forever");
while(true);
}
}
}
Das ausführbare Serverprogramm benötigt eine Referenz auf die Assembly mit der Grid-Klasse. Das gilt auch für das Client-Programm. Dieses instanziiert einen beliebigen TCP-Kanal, wobei die Implementierung den ersten freien Port verwendet. Den Kanal meldet die Anwendung bei den ChannelServices an und holt sich über den Activator unter Angabe des gewünschten Typs und der URL tcp://localhost:8888/Grid eine Referenz auf den Servant. In Wirklichkeit handelt es sich dabei um den Transparent Proxy, den der Client zur transparenten Kommunikation mit dem eigentlichen Servant nutzt (Listing 11).
Listing 11
Über den Activator holt sich das Serverprogramm eine Referenz auf die DLL-Assembly, in der die Klasse Grid installiert ist.
using System;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using RemoteObject;
namespace GridClient {
class Client {
static void Main(string[] args) {
ChannelServices.RegisterChannel(new TcpClientChannel());
Grid myGrid = (Grid)Activator.GetObject
(typeof(Grid), "tcp://localhost:8888/Grid");
if (myGrid == null) {
Console.WriteLine("Could not connect!");
return;
}
for (int i = 0; i < myGrid.Length; i++) {
for (int j = 0; j < myGrid.Length; j++) {
myGrid[i,j] = (double)(i*10 + j);
Console.WriteLine(myGrid[i,j]);
}
}
}
}
}
Im Normalfall ist der Programmierer nicht mit der Komplexität und Mächtigkeit der .NET-Remoting-Architektur konfrontiert. Er verwendet die Bestandteile wie sie vorliegen. Andererseits erlaubt die ‘pluggable’ Architektur beliebige Ergänzungen und Erweiterungen. Gerade das dürfte .NET Remoting zum zukünftigen Objekt der Begierde für Verteilungsexperten machen.
Zur Zeit allerdings weist diese Technik noch ein gravierendes Problem auf. Möchte eine Client-Applikation auf ein Objekt in einer entfernten Assembly zugreifen, muss sie Zugriff auf deren Metadaten haben. Mit anderen Worten, auf der Client-Maschine muss eine lokale Kopie der Server-Assembly verfügbar sein. Das führt unweigerlich zu einem Alptraum für Entwickler, kann es doch potentiell Tausende von Clients geben.
Es existieren zwei Workarounds: Entweder die Programmiererin definiert die Schnittstellen des entfernten Objekts in einer separaten Assembly. Client-Entwickler müssen dann nur noch die Schnittstellen-Assembly lokal verfügbar haben. Eine Alternative bietet das Werkzeug SOAPSUDS, das aus Server-Assemblies transparente Proxies erzeugt, mit denen sich entfernte Objekte aufrufen lassen. Dies ist sogar direkt von einem laufenden Server möglich:
soapsuds -url://myserver:8888/MyObject -oa:MyObjectWrapper.dll
Trotzdem bleibt zu hoffen, dass Microsoft .NET Remoting bald entsprechend bereinigt.
Zukunftsmusik
Obwohl .NET sich noch in der Betaphase befindet, haben sich viele Softwarehersteller bereits darauf eingeschworen. Daher könnte sie eine ernsthafte Alternative zu Java bieten, und es scheint auf jeden Fall ratsam, die Entwicklung von .NET und C# genau zu verfolgen.
Im Dezember 2001 hat das Industriekonsortium ECMA (European Computer Manufacturers) C# und die Laufzeitumgebung CLI (Common Language Infrastructure) bereits als Industriestandard anerkannt. Zur CLI zählen unter anderem das CTS (Common Type System), die CLS (Common Language Specification), das Ablaufsystem VES (Virtual Execution System), die Zwischensprache CIL (Common Intermediate Language), Metadaten sowie Standardbibliotheken und Konzepte für deren Erweiterbarkeit beziehungsweise Anpassbarkeit über Profile. Im Januar 2002 soll der ECMA-Standard nach seiner Fertigstellung im Schnellverfahren (Fast Track Process) die ISO-Standardisierung durchlaufen. Mit einem existierenden Standard könnte .NET auch auf Nicht-Windows-Plattformen Rückenwind erhalten. Das ist im Übrigen ein Schuss vor dem Bug von Sun Microsystems, die sich bezüglich Java geweigert hatte, die Kontrolle Standardisierungsgremien wie ECMA oder ISO zu übertragen.
Microsoft selbst ist mittlerweile nicht untätig geblieben, sondern arbeitet an verschiedenen Erweiterungen. Das .NET Compact Framework soll beispielsweise die Bereitstellung von .NET auf Windows-CE-Plattformen ermöglichen, Mobile Forms gestatten den WML-basierten Zugriff über mobile Geräte. Zahlreiche weitere Produkte stehen in der Pipeline. Immerhin richtet Microsoft fast seine komplette Strategie auf .NET aus. Teilweise erhalten im Übereifer sogar Produkte das Attribut ‘.NET’, die nur bei allergrößter Fantasie einen tatsächlichen Bezug dazu erkennen lassen.
Die Leistungsfähigkeit dieser neuen Technologie muss der Branchenprimus natürlich erst unter Beweis stellen. Große Anwendungen kann es noch nicht geben, weil sich .NET erst in der Betaphase befindet. Bezüglich der technischen Reife hat Hauptkonkurrent Java außerdem einen jahrelangen Vorsprung. Dass dies nichts bedeuten muss, haben Beispiele wie Palm oder Netscape gezeigt. Daher sollte Sun die Konkurrenz in Redmond gut im Auge behalten und die eine oder andere Rosine in die eigene Java-Plattform integrieren. Momentan ist schwer abzusehen, wie sich die technische Entwicklung in der Zukunft fortsetzt, aber es ist immer gut, mehr als eine Option zu haben.
Dem Programmierer sei zum Schluss noch eine Buchempfehlung ans Herz gelegt: ‘Effective Java’ von Joshua Bloch. Dabei handelt es sich zwar um ein Buch mit Entwurfstipps für Java-Programmierer, kein Lehr-, sondern ein Entwurfsbuch aus erfahrener Hand. Die dort zusammengetragenen Ratschläge lassen sich aber zum großen Teil auch auf C# anwenden.
Michael Stal
ist Senior Principal Engineer bei der Corporate Technology der Siemens AG und leitet das Kompetenzfeld Middleware & Application Integration. Er ist Chefredakteur der Zeitschrift Java Spektrum sowie Koautor der Buchreihe ‘Pattern-Oriented Software Architecture’.
Literatur
[1] Robinson, Cornes, Glynn, Harvey, McQueen, Moemeka, Nagel, Skinner, Watson; Professional C#; Wrox Press, Birmingham 2001
[2] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides; Design Patterns - Elements of Reusable Object-Oriented Software; Addison-Wesley, Reading 1995
[3] Douglas Schmidt, Michael Stal, Hans Rohnert, Frank Buschmann; Pattern-Oriented Software Architecture; Volume 2; Patterns for Concurrent and Networked Objects; Wiley & Sons, Chichester 2000
[4] Michael Stal; Ton angebend; C#- und .NET-Tutorial: Teil 1; iX 12/2001, S. 122
[5] Michael Stal; Mehrstimmig; C#- und .NET-Tutorial: Teil 2; iX 01/2002, S. 130
[6] Spezifikationen der ECMA-Standardisierung zum Beispiel bei: http://www.dotnetexperts.com/ecma/
iX-TRACT
- Die Bibliotheken von Microsofts Infrastruktur .NET beinhalten Framework-Klassen für die Programmierung von Windows- und Weboberflächen, Web Services und verteilten Anwendungen sowie den Datenbankzugriff.
- Für den Datenaustausch unterstützen die Bibliotheken sowohl die Extensible Markup Language (XML) als auch das Document Object Model (DOM).
- Das Industriekonsortium ECMA hat die Programmiersprache C# sowie die Laufzeitumgebung CLI (Common Language Infrastructure) als Industriestandard akzeptiert; die ISO-Standardisierung soll im Schnellverfahren folgen.
Übungsaufgabe
Da jetzt das gesamte Rüstzeug zur Verfügung steht, kann es an eine anspruchsvollere Aufgabe gehen:
- In einer Datenbank sind Aktienwerte abzulegen.
- Eine .NET-Anwendung auf dem Server liest diese Daten über ADO.NET in Echtzeit aus der Datenbank und liefert sie bei Anfrage als XML-Strukturen über einen Web Service an den Client.
- Die Client-Anwendungen liegen in zweierlei Gestalt vor: Als Browser-Anwendung unter Nutzung von ASP.NET und als Windows-Anwendung.
- Der Anwender meldet sich auf einer Login-Seite an und landet nach Anmeldung im User-Bereich. Dort kann er Präferenzen spezifizieren, etwa die Aktien, an denen er interessiert ist.
- Ein separates Simulationsprogramm greift ebenfalls auf die Aktiendatenbank zu und ändert dessen Einträge zufällig. Alternativ können Sie auf existierende Webseiten zugreifen und die entsprechenden tatsächlichen Aktienwerte programmatisch abrufen.
Die Auflösung ist hier zu finden. (ka)