Posts Console Event Handling

Console Event Handling


At one point or another, most software developers have written a console application.  It might have been while you were learning how to write software or, even more recently, as a way to create a simple tool that doesn't require the extra effort involved in creating a robust user interface.  At Tech-Ed 2007, I actually saw a presentation where they demonstrated the "Acropolis" framework using a console application as the UI; just to show that "Acropolis" isn't tied to just Windows Presentation Foundation (WPF).  There are many uses for console applications and, with the release of .NET 2.0, there are many features built into the Microsoft .NET Framework for spicing up the what is normally a dreary monotone "UI".  You can change the foreground and background color, control cursor location, clear the screen, change the title of the window, etc.  However, what is missing is the ability to monitor and handle "window" events such as when the user presses the close button on the title bar or the user logging off of the system.

Taking Control

The first step in handling console application events is to setup an event handler to capture the various events that a console window can generate.  First, let's look at what the framework provides.  If all you'd like to do is capture the CTRL+C "break" event, .NET 2.0 provides an event you can register to do so.

Sub Main()
    AddHandler Console.CancelKeyPress, AddressOf Console_CancelKeyPress
End Sub

Private Sub Console_CancelKeyPress(ByVal sender As Object, ByVal e As ConsoleCancelEventArgs)
    ' Handles the CTRL+C "break" event.
    e.Cancel = True ' Return "true" to let other handlers know you are handling this event.
End Sub

As you can see this pretty straight forward.  However, this doesn't allow us to determine if the user is logging off of the machine or if they pressed the close button on the console window.  To do this, we will need to step outside of the framework using P/Invoke to utilize the Windows API.

The API method in particular that we will be utilizing is (defined in the documentation as a C/C++ signature):

BOOL SetConsoleCtrlHandler(
  PHANDLER_ROUTINE HandlerRoutine,
  BOOL Add

This method resides in the Kernel32 system DLL.  Knowing this, we can now build a P/Invoke signature that we can utilize in our VB code.

Private Declare Function SetConsoleCtrlHandler Lib "kernel32" (ByVal handlerRoutine As ConsoleEventDelegate, ByVal add As Boolean) As Boolean

In this case, we are saying that we want the method to be:

  • Private to the source file that it resides.
  • That it resides in the "Kernel32" system file.
  • Has two parameters that will be used.
  • Has a return value represented as a boolean.

The two parameter values will control how the event is handled.  The "add" parameter allows us to add the handler and remove the handler by passing in "true" and "false" respectively.  I'm sure there is some reason why you may want to remove the handler under some circumstance; however, for the sake of argument, let's just say that the event handler will exist for the lifetime of the application.  The first parameter is where things get tricky.  As you can see from the C/C++ signature that the first parameter is a PHANDLER_ROUTINE variable type.  Essentially this just means that we need to pass in a pointer to the method that will be handling the message (assuming we set "add" to "true").  In order to do this, .NET requires that we mark the method signature in such a way to represent that we will be passing in a method signature since we don't really interact with "pointers" in a managed world.  In VB speak, this just means we need to create a "signature" that will represent what a method should look like that we will pass in as the callback method.  To do this, we will create a Delegate.  If you don't understand what a Delegate is, just think of it as a blueprint of what a method should look like.  It doesn't really do anything on it's own; rather, it is used in tangent with the event handling mechanism in .NET.

Public Delegate Function ConsoleEventDelegate(ByVal [event] As ConsoleEvent) As Boolean

We now have our P/Invoke signature that allows us to call the Windows API SetConsoleCtrlHandler method and we've defined a method signature that defines our contract for the handlerRoutine parameter using the ConsoleEventDelegate Delegate definition.  The next step is to create the actual method that will be utilized as the callback (the "event handler") from the Windows kernel for our console application.

Let's define the "event handler":

Public Function Application_ConsoleEvent(ByVal [event] As ConsoleEvent) As Boolean

    ' Be sure to look up the SetConsoleCtrlHandler and HandlerRoutine documentation for any "gotchas" you
    ' may encounter when using this code.

    Select Case [event]

      Case Module1.ConsoleEvent.CtrlC
        MsgBox("CTRL+C received!")
        ' Place any cleanup code here...
      Case Module1.ConsoleEvent.CtrlBreak
        MsgBox("CTRL+BREAK received!")
      Case Module1.ConsoleEvent.CtrlClose
        MsgBox("Program being closed!")
      Case Module1.ConsoleEvent.CtrlLogoff
        MsgBox("User is logging off!")
      Case Module1.ConsoleEvent.CtrlShutdown
        MsgBox("User is logging off!")
      Case Else
        ' Unknown event type.
    End Select

    ' The return value probably isn't really too important under "normal" conditions; however, if you'd like to make
    ' sure that no other registered console events run code when the even occurs, you can return "true".

    Return False ' Not handling the event.

End Function

Now that we've defined the method that will actually handle the event messaging, all that is left is actually wiring everything together.  To do this, we just need to make a call to the SetConsoleCtrlHandler method specifying that we want the Application_ConsoleEvent method to handle the callback.

Sub Main()

    If Not SetConsoleCtrlHandler(AddressOf Application_ConsoleEvent, True) Then
      Console.Write("Unable to install console event handler.")
    End If

    ' Wait for the user to press the ENTER key.

End Sub


Be sure to read the documentation regarding SetConsoleCtrlHandler and HandlerRoutine for specifics.  There are several things to keep in mind regarding how much time you have and various operating system limitations (particularly older versions).

Also, I could find no way to "cancel" the close event.  It appears that console application just can't negate the closing events; so this pretty much provides you with the ability to clean up or store application state in the event of a close.

Wrap Up

As you can see, it isn't too difficult to utilize P/Invoke to extend the functionality of a console application.  Play around with this an let me know what interesting ways you find to take advantage of this.

This post is licensed under CC BY 4.0 by the author.