Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Vorwort zur 6. Auflage
1 Allgemeine Einführung in .NET
2 Grundlagen der Sprache C#
3 Das Klassendesign
4 Vererbung, Polymorphie und Interfaces
5 Delegates und Ereignisse
6 Strukturen und Enumerationen
7 Fehlerbehandlung und Debugging
8 Auflistungsklassen (Collections)
9 Generics – Generische Datentypen
10 Weitere C#-Sprachfeatures
11 LINQ
12 Arbeiten mit Dateien und Streams
13 Binäre Serialisierung
14 XML
15 Multithreading und die Task Parallel Library (TPL)
16 Einige wichtige .NET-Klassen
17 Projektmanagement und Visual Studio 2012
18 Einführung in die WPF und XAML
19 WPF-Layout-Container
20 Fenster in der WPF
21 WPF-Steuerelemente
22 Elementbindungen
23 Konzepte von WPF
24 Datenbindung
25 Weitere Möglichkeiten der Datenbindung
26 Dependency Properties
27 Ereignisse in der WPF
28 WPF-Commands
29 Benutzerdefinierte Controls
30 2D-Grafik
31 ADO.NET – Verbindungsorientierte Objekte
32 ADO.NET – Das Command-Objekt
33 ADO.NET – Der SqlDataAdapter
34 ADO.NET – Daten im lokalen Speicher
35 ADO.NET – Aktualisieren der Datenbank
36 Stark typisierte DataSets
37 Einführung in das ADO.NET Entity Framework
38 Datenabfragen des Entity Data Models (EDM)
39 Entitätsaktualisierung und Zustandsverwaltung
40 Konflikte behandeln
41 Plain Old CLR Objects (POCOs)
Stichwort

Download:
- Beispiele, ca. 62,4 MB

Buch bestellen
Ihre Meinung?

Spacer
Visual C# 2012 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual C# 2012

Visual C# 2012
Rheinwerk Computing
1402 S., 6., aktualisierte und erweiterte Auflage 2013, geb., mit DVD
49,90 Euro, ISBN 978-3-8362-1997-6
Pfeil 12 Arbeiten mit Dateien und Streams
Pfeil 12.1 Einführung
Pfeil 12.2 Namespaces der Ein- bzw. Ausgabe
Pfeil 12.2.1 Das Behandeln von Ausnahmen bei E/A-Operationen
Pfeil 12.3 Laufwerke, Verzeichnisse und Dateien
Pfeil 12.3.1 Die Klasse »File«
Pfeil 12.3.2 Die Klasse »FileInfo«
Pfeil 12.3.3 Die Klassen »Directory« und »DirectoryInfo«
Pfeil 12.3.4 Die Klasse »Path«
Pfeil 12.3.5 Die Klasse »DriveInfo«
Pfeil 12.4 Die »Stream«-Klassen
Pfeil 12.4.1 Die abstrakte Klasse »Stream«
Pfeil 12.4.2 Die von »Stream« abgeleiteten Klassen im Überblick
Pfeil 12.4.3 Die Klasse »FileStream«
Pfeil 12.5 Die Klassen »TextReader« und »TextWriter«
Pfeil 12.5.1 Die Klasse »StreamWriter«
Pfeil 12.5.2 Die Klasse »StreamReader«
Pfeil 12.6 Die Klassen »BinaryReader« und »BinaryWriter«
Pfeil 12.6.1 Komplexe binäre Dateien

Galileo Computing - Zum Seitenanfang

12.6 Die Klassen »BinaryReader« und »BinaryWriter«Zur nächsten Überschrift

Daten werden in einer Datei byteweise gespeichert. Dieses Grundprinzip macht sich die Klasse FileStream zunutze, indem sie Daten Byte für Byte in den Datenstrom schreibt oder aus einem solchen liest. Dazu werden Methoden angeboten, die entweder nur ein einzelnes Byte behandeln oder auf Basis eines Byte-Arrays operieren. Eine spezialisiertere Form der einfachen, byteweisen Vorgänge bieten uns die Klassen BinaryReader bzw. BinaryWriter. Mit BinaryReader lesen Sie aus dem Datenstrom, mit BinaryWriter schreiben Sie in einen solchen hinein. Das Besondere an den beiden Klassen ist die Behandlung der übergebenen oder ausgewerteten Daten.

Die Methoden der Klassen »BinaryReader« und »BinaryWriter«

Fast schon erwartungsgemäß veröffentlicht die Klasse BinaryWriter eine Write-Methode, die vielfach überladen ist. Sie können dieser Methode einen beliebigen primitiven Typ als Argument übergeben, der mit der ihm eigenen Anzahl von Bytes in der Datei gespeichert wird. Ein int schreibt sich demnach mit vier Bytes in eine Datei, ein long mit acht.

Ähnliches gilt auch für die Methode Read, der noch der Typ als Suffix angehängt wird, der gelesen wird, z. B. ReadByte, ReadInt32, ReadSingle usw.

Die Konstruktoren der Klassen »BinaryReader« und »BinaryWriter«

In den beiden Klassen BinaryReader und BinaryWriter stehen Ihnen nur jeweils zwei Konstruktoren zur Verfügung. Dem ersten können Sie die Referenz auf ein Objekt vom Typ Stream übergeben, dem zweiten zusätzlich noch eine Encoding-Referenz.

Binäre Datenströme auswerten

Aus einem Strom die Bytes auszulesen, ist kein Problem. Halten Sie aber nur die rohen Bytes in den Händen, werden diese in den meisten Fällen nur von geringem Nutzen sein. Das Problem ist, eine bestimmte Sequenz von Bytes in richtiger Weise zu interpretieren. Kennen Sie den Typ der Dateninformationen nicht, sind die Bytes praktisch wertlos. Betrachten Sie dazu das folgende Beispiel:

// Beispiel: ..\Kapitel 12\BinaryReaderSample_1

class Program {
static void Main(string[] args) {

// eine Datei erzeugen und einen Integer-Wert in die Datei schreiben

FileStream fileStr = new FileStream(@"D:\Binfile.mic", FileMode.Create);
BinaryWriter binWriter = new BinaryWriter(fileStr);
int intArr = 500;
binWriter.Write(intArr);
binWriter.Close();

// Datei öffnen und den Inhalt byteweise auslesen

FileInfo fi = new FileInfo(@"D:\Binfile.mic");
FileStream fs = new FileStream(@"D:\Binfile.mic", FileMode.Open);
byte[] byteArr = new byte[fi.Length];

// Datenstrom in ein Byte-Array einlesen

fs.Read(byteArr, 0, (int)fi.Length);

// Konsolenausgabe

Console.Write("Interpretation als Byte-Array: ");
for (int i = 0; i < fi.Length; i++)
Console.Write(byteArr[i] + " ");
Console.Write("\n\n");
fs.Close();

// Dateiinhalt textuell auswerten

StreamReader strReader = new StreamReader(@"D:\Binfile.mic");
Console.Write("Interpretation als Text: ");
Console.WriteLine(strReader.ReadToEnd());
strReader.Close();
Console.ReadLine();
}
}

Listing 12.10 Auswerten binärer Datenströme

Zuerst wird ein Objekt vom Typ FileStream erzeugt, um eine neue Datei anzulegen bzw. eine gleichnamige Datei zu überschreiben. Die Objektreferenz wird einem Konstruktor der Klasse BinaryWriter übergeben. Die Methode Write schreibt anschließend einen Integer mit dem Inhalt 500 in die Datei. Anschließend wird die Datei ausgelesen. Wir stellen uns dabei dumm und tun so, als wüssten wir nicht, von welchem Datentyp die in der Datei D:\Binfile.mic gespeicherte Zahl ist. Also testen wir den Dateiinhalt, zuerst byteweise und danach auch noch zeichenorientiert, in der Hoffnung, ein sinnvolles Ergebnis zu erhalten.

Zum byteweisen Lesen greifen wir auf die Klasse FileStream zurück, lesen den Datenstrom aus der Datei in das Byte-Array byteArr ein und geben dann die Elemente des Arrays an der Konsole aus:

Interpretation als Byte-Array: 244 1 0 0

Ein Unbedarfter wird vielleicht wegen der fehlerfreien Ausgabe in Verzückung geraten, wir wissen aber, dass es nicht das ist, was wir ursprünglich in die Datei geschrieben haben. Wie aber ist die Ausgabe zu interpretieren, die mit Sicherheit auf jedem Rechner genauso lauten wird?

Die vier Zahlen repräsentieren die vier Bytes aus der Datei. Dabei erfolgt die Anzeige vom Lower-Byte bis zum Higher-Byte. In die »richtige«, besser gesagt, gewohnte Reihenfolge gebracht müssten wir demnach

0 0 1 244

schreiben. Wir wissen, dass diese Bytes einen Integer beschreiben – und sie tun es auch, wenn wir uns nur die Bitfolge ansehen:

0000 0000 0000 0000 0000 0001 1111 0100

Die Kombination aller Bits ergibt tatsächlich die Dezimalzahl 500. Ein Benutzer, der nicht weiß, wie die vier Bytes zu interpretieren sind, hat die Qual der Wahl: Handelt es sich um vier einzelne Bytes oder um zwei Integer oder vielleicht um eine Zeichenfolge? Letzteres testet unser Code ebenfalls, das Ergebnis der Ausgabe ist ernüchternd: Uns grinst ein Smiley mit ausgestreckter Zunge an.

Ändern wir nun den Lesevorgang der Daten so ab, dass wir den Dateiinhalt tatsächlich als int auswerten:

FileStream fs = new FileStream(@"D:\Binfile.mic", FileMode.Open);
BinaryReader br = new BinaryReader(fs);
Console.WriteLine(br.ReadInt32());

Das Ergebnis wird diesmal mit der korrekten Ausgabe an der Konsole enden.


Galileo Computing - Zum Seitenanfang

12.6.1 Komplexe binäre DateienZur vorigen Überschrift

Der Informationsgehalt binärer Dateien kann nur dann korrekt ausgewertet werden, wenn der Typ, den die Daten repräsentieren, bekannt ist. Im vorhergehenden Abschnitt haben Sie dazu ein kleines Beispiel gesehen. Binäre Dateien können aber mehr als nur einen einzigen Typ speichern, es können durchaus unterschiedliche Typen in beliebiger Reihenfolge sein. Um zu einem späteren Zeitpunkt auf die Daten zugreifen zu können, muss nur der strukturelle Aufbau der Datei – das sogenannte Dateiformat – der gespeicherten Informationen bekannt sein, ansonsten ist die Datei praktisch wertlos.

Dateien unterscheiden sich im Dateiformat: Eine Bitmap-Datei wird die Informationen zu den einzelnen Pixeln anders speichern, als Word den Inhalt eines Dokuments speichert; eine JPEG-Datei unterscheidet sich wiederum von einer MPEG-Datei. Die Dateierweiterung ist als Kennzeichnung einer bestimmten Spezifikation anzusehen, nämlich als Spezifikation der Datenstruktur in der Datei. Praktisch alle Binärdateien werden sich in ihrem Dateiformat unterscheiden.

Wir wollen uns nun in einem etwas aufwendigeren Beispiel dem Thema komplexer Binärdateien nähern, um das Arbeiten mit solchen Dateien zu verstehen, ohne zugleich in zu viel Programmcode die Übersicht zu verlieren. Sie können das Konzept, das sich hinter diesem Beispiel verbirgt, in ähnlicher Weise auch auf andere bekannte Dateiformate anwenden.

Dazu geben wir uns zunächst eine Struktur vor, die ein Point-Objekt beschreibt:

public struct Point {
public int XPos {get; set;}
public int YPos {get; set;}
public long Color {get; set;}
}

Listing 12.11 Definition der Struktur »Point« für das folgende Beispiel

Der Typ Point veröffentlicht drei Daten-Member: XPos und YPos jeweils vom Typ int sowie Color vom Typ long. Nun wollen wir eine Klasse entwickeln, die in der Lage ist, die Daten vieler Point-Objekte in einer Datei zu speichern und später auch wieder auszulesen. Außerdem soll eine Möglichkeit geschaffen werden, um auf die Daten eines beliebigen Point-Objekts in der Datei zugreifen zu können.

Die erste Überlegung ist, wie das Format einer Datei aussehen muss, um den gestellten Anforderungen zu entsprechen. Die Daten mehrerer Point-Objekte hintereinander zu speichern, ist kein Problem. Stellen Sie sich aber nun vor, Sie würden versuchen, die Informationen des zehnten Punkts aus einer Datei zu lesen, in der nur die Daten für fünf Punkte enthalten sind. Das kann zu keinem erfolgreichen Ergebnis führen.

Wir wollen daher eine Information in die Datei schreiben, der wir die gespeicherte Point-Anzahl entnehmen können. Der Typ dieser Information muss klar definiert sein, damit jedes Byte in der Datei eine klare Zuordnung erhält. Im Folgenden wird diese Information in einem int gespeichert, und zwar am Anfang der Datei.

Abbildung

Abbildung 12.5 Datei mit drei gespeicherten Point-Objekten

Damit haben wir die Spezifikation der binären Datei festgelegt. Die Auswertung der ersten vier Bytes liefert die Anzahl der gespeicherten Point-Objekte, und die folgenden insgesamt 16 Byte großen Blöcke beschreiben jeweils einen Punkt. Wir könnten jetzt noch festlegen, dass Dateien dieses Typs beispielsweise die Dateierweiterung .pot erhalten, aber eine solche Festlegung wird der Code des folgenden Beispiels nicht berücksichtigen.

Da wir uns nun auf ein Dateiformat geeinigt haben, wollen wir uns das weitere Vorgehen überlegen. Wir könnten die gesamte Programmlogik in Main implementieren mit dem Nachteil, dass etwaige spätere Änderungen zu Komplikationen führen könnten. Besser ist es, sich das objektorientierte Konzept der Modularisierung in Erinnerung zu rufen. Deshalb wird eine Klasse definiert, deren Methoden die Dienste zur Initialisierung der Point-Objekte, zum Speichern in einer Datei, zum Lesen der Datei und zur Ausgabe der Daten eines beliebigen Point-Objekts zur Verfügung stellen. Der Name der Klasse sei PointReader, die Bezeichner der Methoden lauten WriteToFile, GetFromFile und GetPoint.

Grundsätzlich können Methoden als Instanz- oder Klassenmethoden veröffentlicht werden. Instanzmethoden würden voraussetzen, dass die Klasse PointReader instanziiert wird. Das Objekt wäre dann an eine bestimmte Datei gebunden, die Point-Objekte enthält. Statische Methoden sind flexibler einsetzbar, verlangen allerdings auch bei jedem Aufruf die Pfadangabe zu der Datei. In diesem Beispiel sollen die Methoden statisch sein.

Widmen wir uns der Methode WriteToFile. Sie hat die Aufgabe, eine Datei zu generieren, die die Anforderungen unserer Spezifikation zur Speicherung von Point-Objekten erfüllt. Die Pfadangabe muss der Methode als Argument übergeben werden.

Wie wird der Code in dieser Methode arbeiten? Zunächst muss eine int-Zahl in die Datei geschrieben werden, die der Anzahl der Point-Objektdaten entspricht. Danach werden Point für Point alle Objektdaten übergeben, bis das Array durchlaufen ist.

public static void WriteToFile(string path, Point[] array) {
FileStream fileStr = new FileStream(path, FileMode.Create);
BinaryWriter binWriter = new BinaryWriter(fileStr);

// Anzahl der Punkte in die Datei schreiben

binWriter.Write(array.Length);

// die Point-Daten in die Datei schreiben

for(int i = 0; i < array.Length; i++) {
binWriter.Write(array[i].XPos);
binWriter.Write(array[i].YPos);
binWriter.Write(array[i].Color);
}
binWriter.Close();
}

Listing 12.12 »Point«-Daten in eine Datei schreiben

Die Daten der Point-Objekte sollen mit einer Instanz der Klasse BinaryWriter in die Datei geschrieben werden. Dazu benötigen wir auch ein Objekt vom Typ FileStream. Da alle Daten hintereinander in eine neue Datei geschrieben werden sollen, müssen wir FileStream im Modus Create öffnen.

Nachdem wir die Referenz auf den FileStream an den Konstruktor der BinaryWriter-Klasse übergeben haben, wird die Anzahl der Points in die Datei geschrieben. In einer Schleife greifen wir danach jedes Point-Objekt im Array ab und schreiben die Daten nacheinander in die Datei. Zum Schluss muss der Writer ordnungsgemäß geschlossen werden.

Unsere Datei ist erzeugt, und nur mit dem Kenntnisstand der Spezifikation, wie die einzelnen Bytes zu interpretieren sind, liefern die Daten die richtigen Werte. Die Methode GetFromFile zum Auswerten des Dateiinhalts muss sich an unsere Festlegung halten. Daher wird auch zuerst der Integer aus der Datei gelesen und daran anschließend die Daten der Point-Objekte. Der Rückgabewert der Methode ist die Referenz auf ein intern erzeugtes Point-Array.

public static Point[] GetFromFile(string path) {
FileStream fs = new FileStream(path, FileMode.Open);
BinaryReader br = new BinaryReader(fs);

// liest die ersten 4 Bytes aus der Datei, die die Anzahl der

// Point-Objekte enthält

int anzahl = br.ReadInt32();

// Lesen der Daten aus der Datei

Point[] arrPoint = new Point[anzahl];
for (int i = 0; i < anzahl; i++) {
arrPoint[i].XPos = br.ReadInt32();
arrPoint[i].YPos = br.ReadInt32();
arrPoint[i].Color = br.ReadInt64();
}
br.Close();
return arrPoint;
}

Listing 12.13 Lesen der »Point«-Daten aus einer Datei

Da wir die Kontrolle über jedes einzelne gespeicherte Byte der Datei haben und dieses richtig zuordnen können, muss es auch möglich sein, die Daten eines beliebigen Point-Objekts einzulesen. Dazu dient die Methode GetPoint. Bei deren Aufruf wird zunächst die Pfadangabe übergeben und als zweites Argument die Position des Point-Objekts in der Datei. Der Rückgabewert ist die Referenz auf das gefundene Objekt.

public static Point GetPoint(string path, int pointNo) {
FileStream fs = new FileStream(path, FileMode.Open);
int pos = 4 + (pointNo - 1) * 16;
BinaryReader br = new BinaryReader(fs);

// Prüfen, ob der user eine gültige Position angegeben hat

if (pointNo > br.ReadInt32() || pointNo == 0) {
string message = "Unter der angegebenen Position ist";
message += " kein \nPoint-Objekt gespeichert.";
throw new PositionException(message);
}

// den Zeiger positionieren

fs.Seek(pos, SeekOrigin.Begin);

// Daten des gewünschten Points einlesen

Point savedPoint = new Point();
savedPoint.XPos = br.ReadInt32();
savedPoint.YPos = br.ReadInt32();
savedPoint.Color = br.ReadInt64();
br.Close();
return savedPoint;
}

Listing 12.14 Einlesen eines bestimmten »Point«-Objekts

Die wesentliche Funktionalität der Methode steckt in der richtigen Positionierung der Zeigers, die aus der Angabe des Benutzers berechnet wird. Dabei muss berücksichtigt werden, dass am Dateianfang vier Bytes die Gesamtanzahl der Objekte in der Datei beschreiben und dass die Länge eines einzelnen Point-Objekts 16 Byte beträgt.

int pos = 4 + (pointNo - 1) * 16; 

Die so ermittelte Position wird der Seek-Methode des BinaryReader-Objekts übergeben. Die Positionsnummer des ersten Bytes in der Datei ist 0, daher verweist der Zeiger mit der Übergabe der Zahl 4 auf das fünfte Byte. Wir setzen in diesem Fall natürlich den Ursprung origin des Zeigers auf den Anfang des Datenstroms.

fs.Seek(pos, SeekOrigin.Begin)

Da damit gerechnet werden muss, dass der Anwender eine Position angibt, die keinem Objekt in der Datei entspricht, sollte eine Ausnahme ausgelöst werden. Diese ist benutzerdefiniert und heißt PositionException.

public class PositionException : Exception {
public PositionException() {}
public PositionException(string message) : base(message) {}
public PositionException(string message, Exception inner):base(message, inner){}
}

Listing 12.15 Anwendungsspezifische Exception

Damit ist unsere Klassendefinition fertig, und wir können abschließend die Implementierung testen. Dazu schreiben wir entsprechenden Testcode in die Methode Main:

// Beispiel: ..\Kapitel 12\BinaryReaderSample_2

public class Program {
static void Main(string[] args) {

// Point-Array erzeugen

Point[] pArr = new Point[2];
pArr[0].XPos = 10;
pArr[0].YPos = 20;
pArr[0].Color = 310;
pArr[1].XPos = 40;
pArr[1].YPos = 50;
pArr[1].Color = 110;

// Point-Array speichern

PointReader.WriteToFile(@"D:\Test.pot",pArr);

// gespeicherte Informationen aus der Datei einlesen

Point[] x = PointReader.GetFromFile(@"D:\Test.pot");

// alle eingelesenen Point-Daten ausgeben

for(int i = 0; i < 2; i++) {
Console.WriteLine("Point-Objekt-Nr.{0}", i + 1);
Console.WriteLine();
Console.WriteLine("p[{0}].XPos = {1}", i, x[i].XPos);
Console.WriteLine("p[{0}].YPos = {1}", i, x[i].YPos);
Console.WriteLine("p[{0}].Color = {1}", i, x[i].Color);
Console.WriteLine(new string('=',30));
}

// einen bestimmten Point einlesen

Console.Write("\nWelchen Punkt möchten Sie einlesen? ");
int position = Convert.ToInt32(Console.ReadLine());
try {
Point myPoint = PointReader.GetPoint(@"D:\Test.pot", position);
Console.WriteLine("p.XPos = {0}", myPoint.XPos);
Console.WriteLine("p.YPos = {0}", myPoint.YPos);
Console.WriteLine("p.Color = {0}", myPoint.Color);
}
catch(PositionException e) {
Console.WriteLine(e.Message);
}
Console.ReadLine();
}
}


Listing 12.16 Komplettes Beispielprogramm

Weil die Main-Methode nur zum Testen der zuvor entwickelten Klasse dient, werden auch nur zwei Point-Objekte erzeugt, die uns als Testgrundlage für die weiteren Operationen dienen. Außerdem ist die Datei, in die gespeichert wird, immer dieselbe. Für unsere Zwecke ist das völlig ausreichend. Nach dem Speichern mit

PointReader.WriteToFile(@"D:\Test.pot", pArr);

wird die Datei sofort wieder eingelesen und die zurückgegebene Referenz einem neuen Array zugewiesen:

Point[] x = PointReader.GetFromFile(@"D:\Test.pot");

In einer Schleife werden danach alle eingelesenen Objektdaten an der Konsole ausgegeben.

Aufregender ist es hingegen, die Daten eines bestimmten Punktes zu erfahren. Dem Aufruf von GetPoint wird neben der Pfadangabe auch noch die Position des Point-Objekts in der Datei übergeben. Die Übergabe einer unzulässigen Position führt dazu, dass die spezifische Ausnahme PositionException mit einer entsprechenden Fehlermeldung ausgelöst wird, andernfalls werden die korrekten Werte angezeigt.



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.

>> Zum Feedback-Formular
<< zurück
  Zum Katalog
Zum Katalog: Visual C# 2012

Visual C# 2012
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Professionell entwickeln mit Visual C# 2012






 Professionell
 entwickeln mit
 Visual C# 2012


Zum Katalog: Windows Presentation Foundation






 Windows Presentation
 Foundation


Zum Katalog: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Katalog: C++ Handbuch






 C++ Handbuch


Zum Katalog: C/C++






 C/C++


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo





Copyright © Rheinwerk Verlag GmbH 2013
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de