MSDN Magazin > Home > Ausgaben > 2007 > June >  .NET-Themen: Verarbeiten von Meldungen in Konso...
.NET-Themen
Verarbeiten von Meldungen in Konsolenanwendungen
Stephen Toub

Codedownload verfügbar unter: NetMatters2007_06.exe (153 KB)
Browse the Code Online

F: Ich verwende eine Win32 ® API über P/Invoke. Die API unterstützt asynchrone Vorgänge und stellt einen Rückrufmechanismus zur Verfügung, der mich benachrichtigt, wenn der Vorgang abgeschlossen wurde. Allerdings akzeptiert diese API, im Gegensatz zu anderen APIs, mit denen ich gearbeitet habe und die eine ähnliche Funktionalität bieten, keinen Funktionszeiger und keinen Delegat für den Rückruf. Stattdessen akzeptiert sie ein Fensterhandle, dem sie eine bestimmte Meldung sendet, wenn der Vorgang abgeschlossen ist. Wenn ich das über eine Anwendung mit einer grafischen Benutzeroberfläche verwenden würde, wäre das alles kein Problem. Aber ich verwende das in einer Konsolenanwendung und daher habe ich kein Fenster und keine Meldungsschleife, um die Benachrichtigung zu empfangen oder zu verarbeiten. Was mache ich jetzt? Gibt es einen Ausweg?
F: Ich verwende eine Win32 ® API über P/Invoke. Die API unterstützt asynchrone Vorgänge und stellt einen Rückrufmechanismus zur Verfügung, der mich benachrichtigt, wenn der Vorgang abgeschlossen wurde. Allerdings akzeptiert diese API, im Gegensatz zu anderen APIs, mit denen ich gearbeitet habe und die eine ähnliche Funktionalität bieten, keinen Funktionszeiger und keinen Delegat für den Rückruf. Stattdessen akzeptiert sie ein Fensterhandle, dem sie eine bestimmte Meldung sendet, wenn der Vorgang abgeschlossen ist. Wenn ich das über eine Anwendung mit einer grafischen Benutzeroberfläche verwenden würde, wäre das alles kein Problem. Aber ich verwende das in einer Konsolenanwendung und daher habe ich kein Fenster und keine Meldungsschleife, um die Benachrichtigung zu empfangen oder zu verarbeiten. Was mache ich jetzt? Gibt es einen Ausweg?

A: Viele Entwickler, die Microsoft® .NET Framework verwenden, haben den Eindruck, dass der Anwendungstyp an die Bibliotheken gebunden ist, die in dieser Anwendung verwendet werden können, und dass zum Beispiel eine Konsolenanwendung nicht die Windows® Forms-Klassen nutzen kann. Dabei handelt es sich aber um unterschiedliche Konzepte. Wenn Sie eine neue Konsolenanwendung in Visual Studio® erstellen, setzt es den Ausgangstyp der Anwendung auf „Console Application“ (den Gegenwert des Befehlszeilenparameters /t:exe auf csc.exe). Wenn Sie eine neue Windows Forms-Anwendung in Visual Studio erstellen, setzt es den Ausgangstyp der Anwendung auf „Windows Application“ (den Gegenwert von /t: winexe) und fügt auch der System.Windows.Forms.dll einen Verweis hinzu. Was aber viele Programmierer nicht wissen, ist, dass der System.Windows.Forms.dll in einer Konsolenanwendung ein Verweis hinzugefügt werden kann und umgekehrt auch ein Windows Forms-Projekt geändert werden kann, damit es zu einer „Console Application“ kompiliert wird. Der Anwendungstyp diktiert eigentlich nur, ob ein Standardkonsolenfenster für den Prozess geöffnet wird.
A: Viele Entwickler, die Microsoft® .NET Framework verwenden, haben den Eindruck, dass der Anwendungstyp an die Bibliotheken gebunden ist, die in dieser Anwendung verwendet werden können, und dass zum Beispiel eine Konsolenanwendung nicht die Windows® Forms-Klassen nutzen kann. Dabei handelt es sich aber um unterschiedliche Konzepte. Wenn Sie eine neue Konsolenanwendung in Visual Studio® erstellen, setzt es den Ausgangstyp der Anwendung auf „Console Application“ (den Gegenwert des Befehlszeilenparameters /t:exe auf csc.exe). Wenn Sie eine neue Windows Forms-Anwendung in Visual Studio erstellen, setzt es den Ausgangstyp der Anwendung auf „Windows Application“ (den Gegenwert von /t: winexe) und fügt auch der System.Windows.Forms.dll einen Verweis hinzu. Was aber viele Programmierer nicht wissen, ist, dass der System.Windows.Forms.dll in einer Konsolenanwendung ein Verweis hinzugefügt werden kann und umgekehrt auch ein Windows Forms-Projekt geändert werden kann, damit es zu einer „Console Application“ kompiliert wird. Der Anwendungstyp diktiert eigentlich nur, ob ein Standardkonsolenfenster für den Prozess geöffnet wird.
Das heißt einfach, dass Sie in Ihrer Konsolenanwendung die Klassen von System.Windows.Forms verwenden können, um Ihr Ziel zu erreichen. Ich erstelle eine Klasse, die auf den Windows Forms-Klassen aufbaut, um mir die Arbeit zu vereinfachen, und ich verwende die von winmm.dll exportierte Funktion „mciSendString“ als meinen Testfall für diese Klasse, da sie eine ähnliche Funktionalität wie die API zur Verfügung stellt, die Probleme macht. Mit dieser Lösung wird dann die Konsolenanwendung in der Lage sein, alle Arten von Windows-Meldungen zu verarbeiten. Um zum Beispiel die neue Energieverwaltungsfunktionalität in Windows Vista™ auszunutzen, möchten Sie ggf. die Meldung WM_POWERBROADCAST behandeln. Mit der Klasse, die ich in diesem Artikel bereitstelle, können Sie das machen.
Zunächst werde ich mit dem Entwurf beginnen, auf den ich hinarbeite. Die Klasse „Microsoft.Win32.SystemEvents“, die von System.dll exportiert wird, stellt eine Fülle statischer Ereignisse bereit, die verwendet werden, um verschiedene Systemereignisse zu beobachten. Einige dieser Ereignisse werden aufgrund von Übertragungsmeldungen ausgelöst. Zum Beispiel werden die Ereignisse „DisplaySettingsChanging“ und „DisplaySettingsChanged“ als Antwort auf die Meldung WM_DISPLAYCHANGE ausgelöst.
Wenn SystemEvents in einer benutzerinteraktiven Anwendung initialisiert wird und der Thread der Anwendung, der die Initialisierung von SystemEvents verursacht, als STA (Single-Threaded Apartment, der Standard in einer Windows Forms-Anwendung) markiert ist, erwartet SystemEvents, dass dieser Thread eine Meldungsschleife hat, auf dem SystemEvents mit übertragen wird. (Wenn Sie also fälschlicherweise eine Konsolenanwendung als STA markieren und nicht manuell Meldungen senden, würden keine der meldungsbasierten Ereignisse auf SystemEvents ausgelöst werden). Wenn aber der initialisierende Thread auf MTA gesetzt ist (Multi-Threaded Apartment, der Standard in einer Konsolenanwendung), erzeugt SystemEvents seinen eigenen Thread. Dieser Thread führt eine Meldungsschleife aus. In beiden Fällen erstellt SystemEvents ein ausgeblendetes Fenster, das am Thread angebunden ist und die Meldungsschleife ausführt. Die Fensterprozedur von Windows ist für das Verarbeiten relevanter Meldungen und Auslösen dazugehöriger Ereignisse verantwortlich.
Obwohl meine Implementierung nicht so ausgereift wie SystemEvents ist, wird ein einfaches Design ähnlich wie das von SystemEvents für die ersten Schritte ausreichend sein. Meine MessageEvents-Klasse stellt drei öffentliche und statische Member zur Verfügung:
public static class MessageEvents
{
    public static void WatchMessage(int message);
    public static event EventHandler<MessageReceivedEventArgs> 
        MessageReceived;
    public static IntPtr WindowHandle;
    ...
}
Die WatchMessage-Methode wird verwendet, um die MessageEvents-Klasse über Meldungen zu informieren, die für mich als Empfänger wirklich wichtig sind. (Im Gegensatz dazu sind bei SystemEvents diese Meldungen fest in seiner Implementierung integriert.) Wenn eine dieser Meldungen empfangen wird, wird das Ereignis „MessageReceived“ ausgelöst. Dieses Ereignis bringt die Instanz „MessageReceivedEventArgs“ mit sich, die wiederum an der relevanten Struktur System.Windows.Forms.Message festhält, die die empfangene Meldung vertritt. Schließlich macht die MesssageEvents-Klasse bei Szenarios, bei denen eine API auf ein bestimmtes Fenster abzielt, dem es Meldungen sendet, das Handle für das zugrunde liegende Fenster verfügbar, das von MessageEvents zum Empfangen und Verarbeiten von Ereignisse verwendet wird.. (Normalerweise funktionieren APIs, die Windows-Meldungen für Benachrichtigung verwenden, auch mit HWND_BROADCAST oder 0xFFFF, die, wenn sie verwendet werden, auch dafür sorgen, dass die Benachrichtigung unser zugrunde liegendes Fenster erreicht.)
Dieses zugrunde liegende Fenster wird von MessageEvents erstellt. Im Gegensatz zu SystemEvents, das intelligent entscheidet, ob ein zusätzliches Fenster erstellt werden soll, und in einem solchen Fall keine Ressourcen gespart werden müssen, habe ich mich für die einfachere Lösung entschieden, immer einen neuen Thread und ein neues Fenster zu erstellen. Wenn Sie durch Messungen feststellen, dass dies die Leistung ihrer Anwendung beeinträchtigt, können Sie den Typ gern entsprechend mit der notwendigen zusätzlichen Logik vergrößern.
MessageWindow, in Abbildung 1 dargestellt, ist ein privater verschachtelter Typ innerhalb von MessageEvents. Es ist von Form abgeleitet und beruht deshalb auch auf der zugrunde liegenden NativeWindow-Funktionalität, die allen Control-Instanzen verfügbar ist (ich könnte MessageWindow neu schreiben, sodass es leichter ist, und direkt an NativeWindow anpassen, aber auch das überlasse ich Ihnen als eine Übung, falls Sie für angebracht halten). MessageWindow stellt ein öffentliches Member bereit, RegisterEventForMessage, das einfach die gelieferte messageID in einen Meldungssatz speichert, der vom Wörterbuch<int, bool> vertreten wird. Die Klasse setzt auch die WndProc-Methode außer Kraft, die als Antwort auf das Fenster ausgeführt wird, das eine Meldung empfängt. Dabei wird geprüft, ob die empfangene Meldung im Meldungssatz enthalten ist und folglich ob es eine zuvor registrierte Meldung für ein Ereignis ist, das ausgelöst werden soll. Wenn das der Fall ist, löst WndProc das Ereignis „MessageReceived“ aus. Alle Meldungen werden ausnahmslos der Implementierung der WndProc-Basisklasse übergeben.
public static class MessageEvents
{
    private static SynchronizationContext _context;
    public static event 
        EventHandler<MessageReceivedEventArgs> MessageReceived;

    ...

    private class MessageWindow : Form
    {
        private ReaderWriterLock _lock = new ReaderWriterLock();
        private Dictionary<int, bool> _messageSet = 
            new Dictionary<int, bool>();

        public void RegisterEventForMessage(int messageID)
        {
            _lock.AcquireWriterLock(Timeout.Infinite);
            _messageSet[messageID] = true;
            _lock.ReleaseWriterLock();
        }

        protected override void WndProc(ref Message m)
        {
            _lock.AcquireReaderLock(Timeout.Infinite);
            bool handleMessage = _messageSet.ContainsKey(m.Msg);
            _lock.ReleaseReaderLock();

            if (handleMessage)
            {
                MessageEvents._context.Post(delegate(object state)
                {
                        EventHandler<MessageReceivedEventArgs> 
                            handler = MessageEvents.MessageReceived;
                        if (handler != null) handler(null, 
                            new MessageReceivedEventArgs(
                                      (Message)state));
                }, m);
            }
            base.WndProc(ref m);
        }
    }
}
Für das Auslösen dieses Ereignisses gibt es mehrere interessante Dinge, die Sie beachten sollten. Erstens: MessageReceived ist ein Ereignis auf MessageEvents, und nicht auf MessageWindow. Normalerweise kann eine Klasse kein Ereignis auf einer anderen Klasse durch einen Direktzugriff auslösen. Das Grund dafür ist ein Trick, der vom C#-Compiler angewendet wird. Als ich das Ereignis „MessageReceived“ deklarierte, führte ich eigentlich die folgenden zwei Schritte aus: Ich deklarierte ein öffentliches Ereignis und einen privaten Delegaten. Das öffentliche Ereignis stellt einen Add- und Remove-Accessor bereit, die dafür sorgen, dass alle Klassen Handler mit dem Ereignis registrieren und die Registrierung aufheben können. Was tatsächlich aufgerufen wird, wenn „das Ereignis ausgelöst wird“, ist der private Delegat, auch wenn der C#-Quellcode so aussieht, als ob das Ereignis direkt aufgerufen wird (weitere Informationen dazu finden Sie unter msdn.microsoft.com/msdnmag/issues/06/11/NETMatters). Da der Delegat privat ist, darf eine andere Klasse keinen Zugriff auf ihn haben. Da aber MessageWindow eine verschachtelte Klasse innerhalb von MessageEvents ist, hat MessageWindow Zugriff auf die privaten Implementierungsdaten von MessageEvents. Deshalb hat MessageWindow Zugriff auf den privaten Delegaten MessageReceived, weshalb MessageReceived direkt durch MessageWindow aufgerufen werden kann.
Was auch noch beachtet werden sollte, ist, dass das Ereignis nicht auf dem gleichen Thread ausgelöst wird, der WndProc ausführt. Stattdessen verwendet WndProc eine Instanz von SynchronizationContext, das MessageEvents erfasst, wenn es initialisiert wird (mehr dazu weiter unten). SynchronizationContext wird verwendet, um das Ereignis durch einen asynchronen Post (anstatt eines synchronen Send) im Synchronisierungskontext der Anwendung auszulösen, was das auch immer sein mag. In einer Konsolenanwendung führt SynchronizationContext.Post einfach den Delegaten auf dem Thread ThreadPool aus. In einer Windows Forms-Anwendung führt WindowsFormsSynchronizationContext.Post den Delegaten aus, indem sein Aufruf zurück zum Benutzeroberflächenthread gemarshallt wird. Weitere Informationen zu SynchronizationContext finden Sie unter msdn.microsoft.com/msdnmag/issues/06/06/NETMatters.
Die MessageEvents-Klasse enthält nur statische Members, und die eigentliche Klasse wird tatsächlich als eine statische Klasse deklariert (der Compiler beschwert sich, wenn versucht wird, einen nichtstatischen Member hinzuzufügen). Sie speichert intern einen statischen Verweis zu einem einzelnen MessageWindow, das erstellt wird, wenn MessageEvents initialisiert wird:
private static MessageWindow _window;
private static IntPtr _windowHandle;
Die Implementierung der beiden öffentlichen Methoden von MessageEvents ist sehr einfach, da die meiste Arbeit in eine getrennte Initialisierungsmethode ausgelagert wird. Die WatchMessage-Methode ruft EnsureInitialized auf und übergibt dann die Meldungs-ID der RegisterEventForMessage-Methode von MessageWindow. Die WindowHandle-Eigenschaft ruft ebenfalls EnsureInitialized auf und gibt einfach das Fensterhandle zurück, das während der Initialisierung erstellt wurde:
public static void WatchMessage(int message) {
    EnsureInitialized();
    _window.RegisterEventForMessage(message);
}

public static IntPtr WindowHandle { 
    get {
        EnsureInitialized();
        return _windowHandle; 
    } 
}
Jetzt müssen wir nur noch EnsureInitialized implementieren. EnsureInitialized ist dafür verantwortlich, die MessageEvents-Klasse abzurufen und auszuführen, damit es alle relevanten Meldungen erfassen kann und auf diese antwortet. Folglich muss es einen neuen Thread erzeugen, das Übertragungsfenster erstellen, das Meldungen empfängt, und eine Meldungsschleife starten.
Wie in Abbildung 2 dargestellt führt EnsureInitialized nur die Initialisierungslogik einmal aus; wenn es erkennt, dass MessageWindow bereits erstellt worden ist, steigt es aus. Wenn eine Initialisierung durchgeführt werden muss, ergreift es den SynchronizationContext, der dem aktuellen Thread zugeordnet ist, und startet. Das könnte es auch mit SynchronizationContext.Current machen, doch SynchronizationContext.Current gibt Null zurück, wenn zuvor kein Kontext konfiguriert wurde. Stattdessen verwende ich AsyncOperationManager.SynchronizationContext, einen einfachen Wrapper für SynchronizationContext.Current, der zuerst einen Standard-SynchronizationContext erstellt, wenn noch keiner existiert. Auf diese Weise gibt es bei der Rückgabe des Werts von SynchronizationContext.Current nie den Wert Null zurück.
public static class MessageEvents
{
    private static object _lock = new object();
    private static MessageWindow _window;
    private static IntPtr _windowHandle;
    private static SynchronizationContext _context;

    ...

    private static void EnsureInitialized()
    {
        lock (_lock)
        {
            if (_window == null)
                   {
                   _context = AsyncOperationManager.
                         SynchronizationContext;
                   using (ManualResetEvent mre = 
                         new ManualResetEvent(false))
                {
                    Thread t = new Thread((ThreadStart)delegate
                    {
                        _window = new MessageWindow();
                        _windowHandle = _window.Handle;
                        mre.Set();
                        Application.Run();
                    });
                    t.Name = “MessageEvents”;
                    t.IsBackground = true;
                    t.Start();

                    mre.WaitOne();
                }
            }
        }
    }
}
Mit dem erzeugten Kontext erstellt EnsureInitialized einen neuen Thread. Dieser Thread erstellt MessageWindow und speichert es zusammen mit dem Handle des Fensters in einem separaten statischen Member, der, wie zuvor gezeigt, als Rückgabewert der WindowHandle-Eigenschaft verwendet wird. Jetzt wird das Fenster ausgeführt, und ich muss eine Meldungsschleife starten. Dafür könnte ich meine eigene Meldungsschleife schreiben, aber es ist viel einfacher, eine zu verwenden, die bereits in .NET Framework existiert. Die Application.Run-Methode führt eine Meldungsschleife für Standardanwendungen auf dem aktuellen Thread aus, deshalb muss ich nur diese Methode aufrufen, damit dieser Prozess in Gang kommt.
Das ist alles für meine MessageEvents-Klasse. Um sie in Aktion zu sehen, verwende ich die Funktion „mciSendString“ von winmm.dll, wie oben erwähnt. Die mciSendString-Funktion ist Teil von Media Control Interface (MCI) in Windows und kann verwendet werden, um mit einer Vielzahl von Multimediageräte zu interagieren:
[DllImport(“winmm.dll”, EntryPoint = “mciSendStringA”, 
           CharSet = CharSet.Ansi)]
private static extern int MciSendString(
    string lpszCommand, StringBuilder lpszReturnString, 
    int cchReturn, IntPtr hwndCallback);
Um eine WAV-Datei mit mciSendString wiederzugeben, würde eine typische Sequenz von Befehlen ungefähr so aussehen:
open “C:\Windows\Media\chimes.wav” type waveaudio alias 12345
play 12345 wait
close 12345
Der erste Befehl öffnet das entsprechende Gerät und weist ihm ein Alias zu (12345 in diesem Beispiel), über das es in Zukunft bei Aufrufen zu anderen Befehlen verwiesen wird. Der zweite Befehl weist das System an, das Gerät abzuspielen und erst dann wieder zurückzukehren, wenn der Befehl komplett ausgeführt wurde. Und der letzte Befehl weist das System an, das Gerät zu schließen. Beachten Sie jedoch, dass die Signatur P/Invoke, die ich für MciSendString erstellt habe, den Parameter „hwndCallback IntPtr“ hat. Dieser wird gemeinsam mit einem Benachrichtigungskennzeichen verwendet, das verschiedenen Befehlen hinzugefügt werden kann, einschließlich des Wiedergabebefehls:
play 12345 notify
Da kein Wartekennzeichen definiert wurde, startet dieser Befehl die Wiedergabe und kehrt sofort zurück. Da jedoch das Benachrichtigungskennzeichen beim asynchronen Abschluss des Befehls gesetzt ist, wird die Meldung MM_MCINOTIFY zum Fensterhandle gesendet, das durch das Verfahren hwndCallback angegeben wird:
MciSendString(“play 12345 notify”, null, 
              0, MessageEvents.WindowHandle);
Beachten Sie, wie ich MessageEvents.WindowHandle als das Zielfensterhandle bereitgestellt habe. Vor dem Ausführen dieses Befehls kann ich die MessageEvents-Klasse so konfigurieren, dass sie diese Rückrufmeldungen empfängt:
MessageEvents.WatchMessage(MM_MCINOTIFY);
MessageEvents.MessageReceived += delegate(
    object sender, MessageReceivedEventArgs e) 
{
    Console.WriteLine(“Message received: “ + e.Message.Msg);
};
Mit diesem Code werden, wenn ein Benachrichtigungsbefehl abgeschlossen wird, Daten der entstehenden Meldung in die Konsole geschrieben. Da die MessageEvents-Klasse das natürlich nicht unbedingt in einer Windows Forms-Anwendung machen muss, sollte sie ebenso gut in einer Anwendung funktionieren, wie sie das in einer Konsolenanwendung macht, die keine eigene Meldungsschleife hat.

Senden Sie Fragen und Kommentare für Stephen in englischer Sprache an  netqa@microsoft.com.


Stephen Toubist technischer Redakteur des MSDN Magazine.

Page view tracker