## 9.2 Debuggen mit Programmcode ### 9.2.1 Einführung In Abschnitt 9.1, »Die Behandlung von Laufzeitfehlern«, haben wir uns mit Fehlern beschäftigt, die nach der erfolgreichen Kompilierung zur Laufzeit auftreten können und, falls sie nicht behandelt werden, unweigerlich zum Absturz des Programms führen. Vielleicht noch schlimmer sind Fehler, die weder vom Compiler erkannt werden noch einen Laufzeitfehler verursachen. Es sind die logischen Fehler, die ein falsches oder zumindest unerwartetes Ergebnis zur Folge haben. Um logische Fehler aufzuspüren, muss die Anwendung unter Zuhilfenahme des integrierten Debuggers untersucht werden. Das .NET Framework stellt Ihnen eine Reihe von Hilfsmitteln zur Verfügung, um den Programmcode zu debuggen. Die Spanne reicht von der einfachen Ausgabe von Meldungen im Ausgabe-Fenster bis zur Umleitung der Meldungen in eine Datei oder das Windows-Ereignisprotokoll. Dabei können Sie das Laufzeitverhalten einer Anwendung sowohl mit Programmcode als auch mit der Unterstützung von Visual Studio 2010 überprüfen. Wir werden in den nächsten Abschnitten auf alle Debugging-Techniken eingehen. ### 9.2.2 Die Klasse »Debug« In den vorangegangenen Beispielen haben wir uns sehr häufig eines Kommandos bedient, um beispielsweise den Inhalt von Variablen zu überprüfen. Es war die Methode WriteLine der Klasse Console:
    int value = 4711;
    Console.WriteLine(value);
Diese Technik hat zur Folge, dass die Ausgabe an der Konsole unübersichtlich wird und zwischen den erforderlichen Programminformationen immer wieder Informationen zu finden sind, die nur dazu dienen, die Entwicklung zu unterstützen. Bevor ein solches Programm an den Kunden ausgeliefert wird, müssen die Testausgaben aus dem Programmcode gelöscht werden. Die Entwicklungsumgebung bietet uns eine bessere Alternative an. Dazu wird die Ausgabe nicht in das Konsolenfenster geschrieben, sondern in das Ausgabe-Fenster von Visual Studio 2010. Standardmäßig wird dieses Fenster am unteren Rand der Entwicklungsumgebung angezeigt. Sie können es sich anzeigen lassen, indem Sie im Menü Ansicht den Menüpunkt Ausgabe wählen. Hier klicken, um das Bild zu vergrößern Abbildung 9.2 Das Fenster »Ausgabe« Sie haben dieses Fenster möglicherweise schon häufig gesehen und aufmerksam seinen Inhalt gelesen, denn bei jeder Kompilierung werden hier Informationen ausgegeben, beispielsweise ob die Kompilierung fehlerfrei war. Das Ausgabe-Fenster zeigt uns aber nicht nur Informationen an, die der Compiler hineinschreibt; wir können auch eigene Meldungen in dieses Fenster umleiten. Eine Debug-Information in das Ausgabe-Fenster zu schreiben, ist genauso einfach wie die Ausgabe an der Konsole. Wir müssen nur die Anweisung
    Console.WriteLine("...");
durch
    Debug.WriteLine("...");
ersetzen. Debug ist eine nicht ableitbare Klasse des Namespace System.Diagnostics, die ausschließlich statische Member bereitstellt. Sie sollten den Namespace mit using bekannt geben. Die Methode Debug.WriteLine unterscheidet sich von der Methode Console.WriteLine dahingehend, dass sie keine Formatierungsmöglichkeiten erlaubt. Um mehrere Informationen in einer gemeinsamen Zeichenfolge unterzubringen, müssen Sie daher den Verknüpfungsoperator + benutzen:
    Debug.WriteLine("Inhalt von value = " +  value);
#### Programmablaufinformationen anzeigen Debug.WriteLine ist mehrfach überladen und kann ein Argument vom Typ string oder object entgegennehmen. Eine parameterlose Überladung gibt es nicht.
    public static void WriteLine(object value);
    public static void WriteLine(string message);
Optional können wir auch ein zweites string-Argument übergeben, das eine detaillierte Beschreibung bereitstellt, die vor der eigentlichen Debug-Information ausgegeben wird.
    public static void WriteLine(object value, string category);
    public static void WriteLine(string message, string category);
Sehen wir uns das an einem Beispiel an. Die Anweisung
    Debug.WriteLine("Inhalt von value = " + value, "Variable value");
wird in das Ausgabe-Fenster
    Variable value: Inhalt von value = 34
schreiben – vorausgesetzt, der Inhalt von value ist 34. Neben WriteLine sind in der Klasse Debug noch weitere Methoden zur Ausgabe von Informationen definiert. Tabelle 9.2 gibt darüber Auskunft.

Tabelle 9.2 Ausgabemethoden der Klasse »Debug«

Methode | Beschreibung
Write | Schreibt Debug-Informationen ohne Zeilenumbruch.
WriteLine | Schreibt Debug-Informationen mit Zeilenumbruch.
WriteIf | Schreibt Debug-Informationen ohne Zeilenumbruch, wenn eine bestimmte Bedingung erfüllt ist.
WriteLineIf | Schreibt Debug-Informationen mit Zeilenumbruch, wenn eine bestimmte Bedingung erfüllt ist.
Die beiden zuletzt aufgeführten Methoden WriteIf und WriteLineIf schreiben nur dann Debug-Informationen, wenn eine vordefinierte Randbedingung erfüllt ist. Damit lässt sich der Programmcode übersichtlicher gestalten. Beide Methoden sind genauso überladen wie Write bzw. WriteLine, erwarten jedoch im ersten Parameter zusätzlich einen booleschen Wert, zum Beispiel:
    public static void WriteIf(bool condition, string message);
Verdeutlichen wir uns den Einsatz an einem Beispiel. Um den Inhalt des Feldes value zu testen, könnten wir in herkömmlicher Weise Folgendes schreiben:
    if (value == 77)
      Debug.WriteLine("Inhalt von value ist 77");
Mit WriteLineIf wird daraus eine Codezeile:
    Debug.WriteLineIf(value == 77, "Inhalt von value ist 77");
Wie Sie später noch sehen, bieten uns die Methoden WriteLineIf und WriteIf sehr bequem einzusetzende Möglichkeiten, eine Ablaufprotokollierung zu steuern. #### Einrücken der Ausgabeinformation Die Klasse Debug stellt uns Eigenschaften und Methoden zur Verfügung, um die Debug-Ausgaben einzurücken. Mit der Methode Indent wird die Einzugsebene um eins erhöht, mit Unindent um eins verringert. Standardmäßig beschreibt eine Einzugsebene vier Leerzeichen. Mit der Eigenschaft IndentSize kann ein anderer Wert bestimmt werden. IndentLevel erlaubt, eine bestimmte Einzugsebene festzulegen, ohne Indent mehrfach aufrufen zu müssen. An einem Beispiel wollen wir uns noch die Auswirkungen ansehen.
    Debug.WriteLine("Ausgabe 1");
    Debug.Indent();
    Debug.WriteLine("Ausgabe 2");
    Debug.IndentLevel = 3;
    Debug.WriteLine("Ausgabe 3");
    Debug.Unindent();
    Debug.WriteLine("Ausgabe 4");
    Debug.IndentSize = 2;
    Debug.IndentLevel = 1;
    Debug.WriteLine("Ausgabe 5");
Der Code führt zu folgender Ausgabe:
    Ausgabe 1
        Ausgabe 2
                Ausgabe 3
            Ausgabe 4
      Ausgabe 5
#### Die Methode »Assert« Mit der Methode Assert können Sie eine Annahme prüfen, um beispielsweise unzulässige Zustände festzustellen. Die Methode zeigt eine Fehlermeldung an, wenn ein Ausdruck mit false ausgewertet wird.
    Debug.Assert(value >= 0, "value ist negativ");
Hat die Eigenschaft value einen Wert, der kleiner 0 ist, erscheint auf dem Bildschirm die in Abbildung 9.3 gezeigte Nachricht. Hier klicken, um das Bild zu vergrößern Abbildung 9.3 Die Meldung der Methode »Debug.Assert« Das Dialogfenster enthält neben der dem zweiten Parameter übergebenen Zeichenfolge auch Informationen darüber, in welcher Klasse und welcher Methode der Assertionsfehler aufgetreten ist. ### 9.2.3 Die Klasse »Trace« Die Klasse Trace unterscheidet sich in der Liste ihrer Eigenschaften und Methoden nicht von Debug. Dennoch gibt es einen Unterschied, der sich nur bei einem Wechsel der Build-Konfiguration zwischen Release und Debug bemerkbar macht (siehe Abbildung 9.4). Hier klicken, um das Bild zu vergrößern Abbildung 9.4 Die Einstellung der Debug/Release-Build-Konfiguration #### Die Debug/Release-Konfiguration Standardmäßig ist bei jedem Projekt die Konfiguration Debug ausgewählt. Anweisungen, die auf den Klassen Debug oder Trace basieren, werden dann grundsätzlich immer bearbeitet. Wird jedoch die Konfiguration Release gewählt, ignoriert der C#-Compiler Aufrufe auf der Klasse Debug, während Aufrufe auf Trace weiterhin bearbeitet werden. Das ist aber noch nicht das Wesentlichste. Viel wichtiger ist die Tatsache, dass Aufrufe auf Trace kompiliert werden – unabhängig davon, ob Sie die Konfiguration Debug oder Release eingestellt haben. Viele Trace-Anweisungen vergrößern deshalb auch das DLL- bzw. EXE-Kompilat. Andererseits hat der Entwickler hier auch eine einfache Möglichkeit, bestimmte Zustände zu protokollieren, die sich zur Laufzeit einstellen und geprüft werden müssen. Unterhalb des Verzeichnisses, in dem sich die Quellcodedateien befinden, legt die Entwicklungsumgebung das Verzeichnis \bin an, dem selbst je nach eingestellter Build-Konfiguration die beiden Verzeichnisse \Debug und \Release untergeordnet sind. Abhängig von der Konfigurationseinstellung wird das Kompilat der ausführbaren Datei in eines dieser beiden Unterverzeichnisse gespeichert. Debug-Informationen, die beim Kompilieren generiert werden, sind in einer Datei mit der Dateierweiterung .PDB im Verzeichnis gespeichert. Der Debugger nutzt die darin enthaltenen Informationen, um Variablennamen und andere Informationen während des Debuggens in einem sinnvollen Format anzuzeigen. ### 9.2.4 Ablaufverfolgung mit »TraceListener«-Objekten Standardmäßig erfolgt die Ausgabe der Ablaufverfolgungsmeldungen im Fenster Ausgabe. Mit dem Schließen des Ausgabe-Fensters gehen auch die Meldungen verloren. Das .NET Framework stellt Ihnen aber auch Konzepte zur Verfügung, um die Meldungen beispielsweise in eine Datei oder das Ereignisprotokoll von Windows umzuleiten. Verantwortlich dafür sind Listener-Objekte. Im Namespace System.Diagnostics sind fünf Listener vordefiniert, die alle aus der abstrakten Klasse TraceListener abgeleitet sind: ConsoleTraceListener DefaultTraceListener DelimitedListTraceListener TextWriterTraceListener EventLogTraceListener Eine Beschreibung der genannten Klassen können Sie Tabelle 9.3 entnehmen.

Tabelle 9.3 Die Listener-Klassen des Namespace »System.Diagnostics«

Listener-Klasse | Beschreibung
ConsoleTraceListener | Die ConsoleTraceListener-Klasse schreibt Ablaufverfolgungs- und Debug-Meldungen in die Konsole.
DefaultTraceListener | Die Klasse DefaultTraceListener sendet die Ausgabe an die Entwicklungsumgebung. Das ist auch gleichzeitig die Standardeinstellung.
DelimitedListTraceListener | Leitet die Ablaufverfolgungs- oder Debug-Ausgabe an einen Textwriter, z. B. einen Streamwriter, oder in einen Stream.
EventLogTraceListener | Die Klasse EventLogTraceListener leitet die Ausgabe an das Windows-Ereignisprotokoll weiter.
TextWriterTraceListener | Der Empfänger von TextWriterTraceListener ist ein Stream, beispielsweise um die Informationen in eine Datei zu schreiben.
Listener-Objekte sind an die Klassen Debug und Trace gebunden. Beide Klassen stellen mit Listeners eine Eigenschaft vom Typ TraceListenerCollection bereit. Diese Auflistung weist die bekannten Eigenschaften und Methoden auf, also z. B. Add, Remove, Clear. Beachten Sie, dass die Listener-Objekte sowohl von Trace als auch von Debug gemeinsam benutzt werden. Beide Klassen bedienen sich also derselben Auflistung. Sehen wir uns nun an einem Beispiel an, wie die Listener eingesetzt werden:
    Debug.Listeners.Clear();
    ConsoleTraceListener console = new ConsoleTraceListener(Console.Out);
    Debug.Listeners.Add(console);
    Debug.WriteLine("Debug");
    Trace.WriteLine("Trace");
In der ersten Anweisung wird die Listeners-Auflistung mit Clear geleert. Weil damit der standardmäßige Eintrag des DefaultTraceListeners-Objekts gelöscht wird, würde ein Debug.Write- oder Trace.Write-Methodenaufruf keinen Abnehmer mehr finden und im Nirwana verpuffen. In der folgenden Codezeile wird daher die Klasse ConsoleTraceListener instanziiert. Dieses Objekt leitet die Ausgabemeldungen an die Konsole um und wird mit der darauf folgenden Anweisung der TraceListenerCollection übergeben. Da sich die Klassen Trace und Debug die installierten Listener gleichberechtigt teilen, werden in der Build-Konfiguration Debug beide Meldungen an der Konsole angezeigt. #### Die Klasse »TextWriterTraceListener« Eine Liste von insgesamt sieben Konstruktoren zeugt davon, dass die Klasse TextWriterTraceListener die flexibelste der Listener ist. Neben dem parameterlosen Konstruktor kann dieser Listener auch mit einem TextWriter- oder einem Stream-Objekt verbunden werden. Weitergehende Informationen zu den Writer- und Stream-Klassen erhalten Sie in Kapitel 12, »Arbeiten mit Dateien und Streams«. Nehmen wir an, dass Sie Ihre Debug-Informationen in einer Datei speichern möchten. Im einfachsten Fall brauchen Sie dem Konstruktor nur eine Zeichenfolge zu übergeben, die den Pfad zu der Datei beschreibt. Anschließend muss der neue Listener registriert werden.
    TextWriterTraceListener listener =
         new TextWriterTraceListener(@"C:\DebugProtocol.txt");
    Trace.Listeners.Add(listener);
    Debug.WriteLine("Debug.WriteLine-Anweisung");
    Trace.WriteLine("Trace.WriteLine-Anweisung");
    listener.Flush();
    listener.Close();
TextWriterTraceListener schreibt alle Ausgabeinformationen zunächst in einen Puffer. Mit dem Aufruf der Methode Flush wird der Puffer geleert und der Inhalt in die Datei geschrieben. Alternativ dazu können Sie auch die Eigenschaft AutoFlush=true setzen. Mit Close wird das TextWriterTraceListener-Objekt geschlossen. Soll danach die Ablaufverfolgung wieder aufgenommen werden, muss der Listener neu initialisiert und registriert werden. Die Klassen Debug und Trace bedienen sich derselben Listener. Daher werden beide Write-Anweisungen in der Datei einen Abnehmer finden – vorausgesetzt natürlich, dass die Anwendung im Debug- und nicht im Release-Modus ausgeführt wird. Ist die Datei bereits vorhanden, werden die Debug-Informationen an den alten Dateiinhalt angehängt. Vielleicht wünschen Sie aber, dass die alten Einträge überschrieben werden. Hier hilft die Überladung des TextWriterTraceListener-Konstruktors weiter, die die Referenz auf ein Stream-Objekt erwartet. Im folgenden Beispiel wird dem Konstruktor eine Referenz vom Typ FileStream übergeben. Das FileStream-Objekt leitet die Ausgabe in eine Datei um. Ist die Datei bereits vorhanden, wird die alte überschrieben, ansonsten wird eine neue erzeugt. Dafür sorgt die Angabe FileMode.Create.
    // --------------------------------------------------------
    // Beispiel: ...\Kapitel 9\TextWriterTraceListenerSample
    // --------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        Debug.Listeners.Clear();
        FileStream fs = new FileStream(@"C:\ErrorProtocol.txt", FileMode.Create);
        TextWriterTraceListener listener = new TextWriterTraceListener(fs);
        Debug.Listeners.Add(listener);
        try {
          Operation();
        }
        catch(Exception e) {
          string strError = DateTime.Now.ToString();
          Trace.WriteLine("Fehler: " + strError + " - " + e.Message);
        }
        finally {
          listener.Flush();
          listener.Close();}
        }
      static void Operation() {
        throw new Exception("Unbekannter Fehler");
      }
    }
Sehr häufig werden Listener dazu benutzt, aufgetretene Laufzeitfehler im catch-Zweig der Fehlerbehandlung zu protokollieren. In diesem Beispiel wird in der Methode Operation eine Exception mit einer spezifischen Fehlermeldung erzeugt. Neben der Fehlermeldung werden in der Protokolldatei auch noch das Datum und die Uhrzeit des Auftretens der Ausnahme festgehalten. Die entsprechende Information liefert uns die statische Methode Now der Klasse DateTime. #### Mehrere Listener verwalten Ein TextWriterTraceListener beschreibt immer genau einen Informationsabnehmer und eignet sich nicht nur dazu, Fehler zu protokollieren. Sie können einen Listener auch genauso gut dazu benutzen, allgemeine Informationen in einen Stream zu schreiben. Es sind deshalb auch Szenarien vorstellbar, in denen mehrere Listener mit unterschiedlichen Aufgaben benötigt werden. Grundsätzlich werden Meldungen von jedem registrierten Listener gepuffert. Soll eine Meldung nur von einem bestimmten Listener verarbeitet werden, muss die Registrierung der anderen Listener zumindest zeitweise aufgehoben werden. Mit den Methoden Add, Remove sowie dem Indexer ist das grundsätzlich möglich. Allerdings ist es einfacher, ein Listenelement über einen Namen als über den Index in der Auflistung anzusprechen. Daher stellt die Klasse TextWriterTraceListener Konstruktoren mit einem zweiten Parameter bereit, um den Listener zu benennen. Der Name des Listeners wird in der Eigenschaft Name eingetragen. Im folgenden Beispiel werden zwei Listener registriert, und beiden wird eine Zeichenfolge übergeben. Anschließend wird ein Listener aus der TraceListenerCollection gelöscht. Die anschließende Meldung wird nur vom verbleibenden Listener weitergeleitet.
    // ersten Listener registrieren
    TextWriterTraceListener listenerA =
       new TextWriterTraceListener(@"C:\A.txt", "ListenerA");
    Trace.Listeners.Add(listenerA);
    // zweiten Listener registrieren
    TextWriterTraceListener listenerB =
       new TextWriterTraceListener(@"C:\B.txt", "ListenerB");
    Trace.Listeners.Add(listenerB);
    Trace.WriteLine("Erste Information");
    // in beide Listener schreiben
    listenerA.Flush();
    listenerB.Flush();
    // listenerB deregistrieren
    Trace.Listeners.Remove("ListenerB");
    Trace.WriteLine("Zweite Information");
    listenerA.Flush();
Sollten Sie versuchen, den Informationsfluss nur über den Aufruf der Flush-Methode auf die einzelnen Listener zu steuern, werden Sie damit wenig Erfolg haben. Denn solange ein Listener registriert ist, werden alle eingehenden Informationen in seinen Puffer geschrieben und beim nächsten Aufruf von Flush an den Abnehmer weitergeleitet. #### Die Klasse »EventLogTraceListener« Alternativ zum TextWriterTraceListener können Sie mit einer Instanz der Klasse EventLogTraceListener Meldungen in das Windows-Ereignisprotokoll schreiben. Auch dieser Listener muss in der Listeners-Auflistung mit der Add-Methode registriert werden. Das Verhalten hinsichtlich der Pufferung der Meldungen ist identisch mit dem, das wir im letzten Abschnitt erörtert haben. Die Klasse EventLogTraceListener stellt drei Konstruktoren bereit:
    public EventLogTraceListener();
    public EventLogTraceListener(string);
    public EventLogTraceListener(EventLog);
Wenn Sie den parameterlosen Konstruktor aufrufen, müssen alle Eigenschaften konfiguriert werden, bevor Nachrichten an ein Ereignisprotokoll gesendet werden können. Geeigneter erscheint daher der Konstruktor, der eine Zeichenfolge erwartet, die das Ereignisprotokoll namentlich beschreibt. Diagnosemeldungen, die von den Klassen Debug oder Trace geschrieben werden, werden Sie anschließend im Anwendungsprotokoll von Windows wiederfinden.
    EventLogTraceListener listener = new EventLogTraceListener("TestLog");
    Debug.Listeners.Add(listener);
    Debug.WriteLine("Ein Eintrag im Ereignisprotokoll");
    listener.Flush();
Mit der dritten Überladung besteht die Möglichkeit, eine Ablaufverfolgungs- oder Debug-Meldung an ein beliebiges Ereignisprotokoll auf der lokalen oder einer anderen Maschine weiterzuleiten. Darüber hinaus können Sie auch ein benutzerdefiniertes Ereignisprotokoll erstellen. Der Konstruktor erwartet dann die Referenz auf ein EventLog-Objekt, mit dem ein Ereignisprotokoll von Windows beschrieben wird. Im folgenden Codefragment wird ein Ereignisprotokoll namens MyProtocol auf der lokalen Maschine eingerichtet. Die lokale Maschine wird durch eine Zeichenfolge beschrieben, die nur einen Punkt enthält. Sie könnten aber auch den Namen eines entfernten Rechners angeben.
    EventLog log = new EventLog("MyProtocol", ".");
    log.Source = "MyApplication";
    EventLogTraceListener listener = new EventLogTraceListener(log);
    Debug.Listeners.Add(listener);
    Debug.WriteLine("Ein Eintrag im Ereignisprotokoll");
    listener.Flush();
Mit der Eigenschaft Source geben Sie einen Bezeichner für die Ereignisquelle an. Innerhalb der Ereignisprotokolle von Windows muss dieser eindeutig sein. Wollen Sie in Erfahrung bringen, welche Ereignisprotokolle auf der lokalen Maschine eingerichtet sind, können Sie sich über den Aufruf der statischen Methode GetEventLogs der Klasse EventLog ein Array gleichen Typs besorgen und auf jedem Element die Log-Eigenschaft auswerten, die den Namen des Ereignisprotokolls als Zeichenfolge liefert.
    EventLog[] myEventLogs = EventLog.GetEventLogs(".");
    foreach(EventLog log in myEventLogs) {
      Console.WriteLine("Protokollname: " + log.Log);
    }
Sind keine benutzerdefinierten Ereignisprotokolle in der Registrierungsdatenbank eingetragen, sollte die Ausgabe Application, System und Security lauten. Grundsätzlich sollten Sie sich darüber im Klaren sein, dass das Ereignisprotokoll nicht dazu dient, eine große Anzahl allgemeiner Meldungen vorzuhalten. Beschränken Sie daher den Einsatz des EventLogTraceListeners auf wirklich wichtige Meldungen. ### 9.2.5 Steuerung der Protokollierung mit Schaltern Ausgaben, die mit den Write-Methoden der Klassen Debug und Trace erzeugt werden, suchen sich grundsätzlich immer ihre Abnehmer in den registrierten Listenern. Das .NET Framework stellt Ihnen darüber hinaus mit zwei Schalterklassen einen weiteren effektiven Mechanismus zur Verfügung, mit dem Ablaufverfolgungs- und Debug-Ausgaben dynamisch gesteuert werden können. Bei den Schalterklassen handelt es sich um BooleanSwitch und TraceSwitch, die beide in Switch eine gemeinsame Basisklasse haben. #### Die Klasse »BooleanSwitch« Die einfachere Schalterklasse ist BooleanSwitch, die einem An-/Ausschalter ähnelt. Mit einer Instanz dieser Klasse lassen sich alle Ablaufverfolgungs- und Debug-Ausgaben gemeinsam aktivieren oder deaktivieren. Die Klasse hat nur einen Konstruktor, dem Sie den Namen des Schalters und eine Beschreibung übergeben müssen:
    BooleanSwitch mySwitch = new BooleanSwitch("MeinSchalter",
                             "Ablaufverfolgung in MyApplication");
Standardmäßig sind alle Ausgaben zunächst deaktiviert. Sollen die Ausgaben an die Listener weitergeleitet werden, müssen Sie die Eigenschaft Enabled des BooleanSwitch-Objekts auf true setzen:
    mySwitch.Enabled = true;
Um die aktuelle Schalterstellung auszuwerten und den weiteren Protokollierungsablauf zu steuern, bieten sich die beiden Methoden WriteIf und WriteLineIf der Klassen Debug und Trace an, z. B.:
    Trace.WriteLineIf(mySwitch.Enabled,
                "Ablaufverfolgung:" + DateTime.Now.ToString());
Nur dann, wenn der Schalter mit Enabled=true aktiviert ist, wird die Meldung mit genauer Datums- und Zeitangabe an alle registrierten Listener geschickt. #### Schalterstellung in einer Konfigurationsdatei festlegen Damit sind aber noch nicht alle Möglichkeiten erschöpft. .NET-Anwendungen können um eine optionale Anwendungskonfigurationsdatei erweitert werden, in der nach Bedarf Einstellungen verändert werden können, ohne dass die Anwendung neu kompiliert werden muss.
Anmerkung
Anwendungskonfigurationsdateien sind bisher noch nicht erwähnt worden. Mehr darüber erfahren Sie in Kapitel 15, »Projektmanagement und Visual Studio 2010«.
Anstatt ein BooleanSwitch-Objekt mit einer Anweisung im Code zu aktivieren bzw. zu deaktivieren, lässt sich das Verhalten des Schalters in der Anwendungskonfigurationsdatei festlegen. Dazu müssen Sie diese folgendermaßen ergänzen:
    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
       <system.diagnostics>
          <switches>
             <add name="MeinSchalter" value="1" />
          </switches>
       </system.diagnostics>
    </configuration>
Innerhalb des switches-Knotens wird mit dem add-Element der entsprechende Schalter festgelegt. Eine Konfigurationsdatei ist nicht nur auf die Steuerung eines Schalters beschränkt. Wollen Sie die Ausgabe über mehrere Schalter steuern, fügen Sie einfach für jeden Schalter ein weiteres add-Element hinzu. Das Attribut name gibt den Namen des Schalters an, das Attribut value die Schalterstellung. Dabei steht 1 für die Aktivierung und 0 für die Deaktivierung des Schalters. In unserem Beispiel ist der Schalter aktiviert. Deshalb wird die Anwendung auch alle Ausgabemeldungen an die registrierten Listener weiterleiten. Wollen wir die Ausgabemeldungen nicht mehr protokollieren, brauchen wir die Konfigurationsdatei nur mit dem Editor zu öffnen und value auf den Wert 0 zu setzen. Beim nächsten Start der Anwendung ist damit die Protokollierung abgeschaltet. Beachten Sie, dass die Angabe der Schalterstellung im Programmcode dazu führt, dass die Vorgaben in der Konfigurationsdatei verworfen werden. #### Die Klasse »TraceSwitch« Im Gegensatz zu einem BooleanSwitch-Objekt, das nur die Einstellungen »aktiviert« und »deaktiviert« zulässt, können die Ablaufverfolgungs- und Debug-Ausgaben mit einem TraceSwitch-Objekt über die Festlegung mehrerer Ebenen gesteuert werden. Verantwortlich für die Steuerung der Ablaufverfolgung ist die Eigenschaft Level, die vom Typ der Enumeration TraceLevel ist.
    public TraceLevel Level {get; set;}
In TraceLevel werden Ablaufverfolgungsebenen definiert, die Sie Tabelle 9.4 entnehmen können.

Tabelle 9.4 Die Konstanten der Enumeration »TraceLevel«

Member | Wert | Beschreibung
Off | 0 | Es werden keine Ausgaben an die Listener weitergegeben.
Error | 1 | Es werden Ausgaben von Fehlermeldungen an die Listener weitergeleitet.
Warning | 2 | Es werden Ausgaben von Fehler- und Warnmeldungen an die Listener weitergeleitet.
Info | 3 | Es werden Ausgaben von Fehler-, Warn- und Informationsmeldungen an die Listener weitergeleitet.
Verbose | 4 | Es werden alle Meldungen an die Listener weitergeleitet.
Wollen Sie ein TraceSwitch-Objekt erzeugen, müssen Sie dem Konstruktor den Namen des Schalters und eine Beschreibung übergeben. Damit gleicht die Instanziierung der von BooleanSwitch. Um die Ablaufverfolgungsebene festzulegen, weisen Sie der Eigenschaft Level einen Wert zu, zum Beispiel:
    TraceSwitch switch = new TraceSwitch("MeinSchalter",
                           "Ablaufverfolgung in MyApplication");
    switch.Level = TraceLevel.Warning;
Damit in Abhängigkeit von der Ablaufverfolgungsebene mit den Methoden WriteIf und WriteLineIf ein boolescher Wert ausgewertet werden kann, sind in der Klasse TraceSwitch die Eigenschaften TraceError TraceWarning TraceInfo TraceVerbose definiert, die je nach Schalterstellung true oder false zurückgeben. Um die Ausgabe der Meldungen zu steuern, müssen Sie in den Methoden WriteIf oder WriteLineIf nur eine der Eigenschaften überprüfen:
    Trace.WriteLineIf(switch.TraceWarning,
                "Ablaufverfolgung:" + DateTime.Now.ToString());
Die Einstellung der Eigenschaft Level umfasst gleichzeitig auch die niedriger bewerteten Einstellungen, mit Ausnahme von TraceLevel.Off. Wenn im Code beispielsweise die Ablaufverfolgungsebene auf TraceLevel.Info festgelegt ist, bedeutet dies, dass die Eigenschaften TraceError, TraceWarning und TraceInfo true liefern, während TraceVerbose false ist. Zur Steuerung des oder der TraceSwitch-Objekte bietet sich ebenfalls die Anwendungskonfigurationsdatei an. Dem Attribut value müssen Sie für eine bestimmte Ablaufverfolgungsebene einen zugeordneten Wert übergeben. Sie können diesen Tabelle 9.4 entnehmen. Die folgende Konfigurationsdatei beschreibt die Einstellung TraceLevel.Info.
    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
       <system.diagnostics>
          <switches>
             <add name="MeinSchalter" value="3" />
          </switches>
       </system.diagnostics>
    </configuration>
### 9.2.6 Bedingte Kompilierung Die bedingte Kompilierung ermöglicht es, Codeabschnitte oder Methoden nur dann zu kompilieren, wenn ein bestimmtes Symbol definiert ist. Üblicherweise werden bedingte Codeabschnitte dazu benutzt, während der Entwicklungsphase den Zustand der Anwendung zur Laufzeit zu testen. Bevor ein Release-Build der Anwendung erstellt wird, wird das Symbol entfernt. Die Abschnitte, deren Code als bedingt kompilierbar gekennzeichnet ist, werden dann nicht kompiliert. Der folgende Code zeigt ein Beispiel für bedingte Kompilierung:
    #define MYDEBUG
    using System;
    class Program {
      static void Main(string[] args) {
        #if(MYDEBUG)
          Console.WriteLine("In der #if-Anweisung");
        #elif(TEST)
          Console.WriteLine("In der #elif-Anweisung");
        #endif
      }
    }
Mit der Präprozessordirektive #define wird das Symbol MYDEBUG definiert. Symbole werden immer vor der ersten Anweisung festgelegt, die selbst keine #define-Präprozessordirektive ist. Werte können den Symbolen nicht zugewiesen werden. Die Präprozessordirektive gilt nur in der Quellcodedatei, in der sie definiert ist, und wird nicht mit einem Semikolon abgeschlossen. Mit #if oder #elif wird das Vorhandensein des angegebenen Symbols getestet. Ist das Symbol definiert, liefert die Prüfung das Ergebnis true, und der Code wird ausgeführt. #elif ist die Kurzschreibweise für die beiden Anweisungen #else und #if. Da im Beispielcode kein Symbol namens TEST definiert ist, wird die Ausgabe wie folgt lauten:
    In der #if-Anweisung
Standardmäßig sind in C#-Projekten die beiden Symbole DEBUG und TRACE vordefiniert. Diese Vorgabe ist im Projekteigenschaftsfenster eingetragen (siehe Abbildung 9.5) und hat anwendungsweite Gültigkeit. Das Projekteigenschaftsfenster öffnen Sie, indem Sie im Projektmappen-Explorer den Knoten Properties doppelt anklicken. Sie können die Symbole löschen oder auch weitere hinzufügen, die ihrerseits alle durch ein Semikolon voneinander getrennt werden müssen. Hier klicken, um das Bild zu vergrößern Abbildung 9.5 Festlegung der Symbole im Projekteigenschaftsfenster Das Projekteigenschaftsfenster bietet darüber hinaus den Vorteil, dass sich die Symbole einer bestimmten Build-Konfiguration zuordnen lassen. Wählen Sie in der Dropdown-Liste Konfiguration die Build-Konfiguration aus, für die die unter Bedingte Kompilierungskonstanten angegebenen Symbole gültig sein sollen. Wenn Sie beispielsweise keine #define-Präprozessordirektive im Code angeben, dafür aber der Debug-Konfiguration das Symbol DEBUG zugeordnet haben, wird der in #if(DEBUG) bis #endif eingeschlossene Code im Debug-Build mitkompiliert, im Release-Build jedoch nicht. Die im Projekteigenschaftsfenster definierten Konstanten gelten projektweit. Um in einer einzelnen Codedatei die Wirkung eines Symbols aufzuheben, müssen Sie das Symbol hinter der #undef-Direktive angeben. #### Bedingte Kompilierung mit dem Attribut »Conditional« Häufig ist es wünschenswert, eine komplette Methode als bedingt zu kompilierende Methode zu kennzeichnen. Hier hilft Ihnen .NET mit dem Attribut Conditional aus dem Namespace System.Diagnostics weiter. Damit eine komplette Methode als bedingt kompilierbar gekennzeichnet wird, muss das Conditional-Attribut (wie im folgenden Beispiel gezeigt) vor dem Methodenkopf in eckigen Klammern angegeben werden. In den runden Klammern wird das Symbol als Zeichenfolge genannt:
    [Conditional("DEBUG")]
    public void ConditionalTest() {
      ...
    }
Die Methode ConditionalTest wird jetzt nur dann kompiliert, wenn das Symbol DEBUG gesetzt ist. Sie können auch mehrere Attribute mit unterschiedlichen Symbolen angeben. Kann eines der Symbole ausgewertet werden, wird die Methode ausgeführt. Anders als bedingter Code, der durch #if - #endif eingeschlossen ist, wird eine Methode, der das Conditional-Attribut angeheftet ist, immer kompiliert.