WPF - How to Veto a Windows Shutdown

Friday, July 17, 2009 / Posted by Luke Puplett /

...(even though you shouldn’t really).

The .NET Framework offers an event and a boolean argument property which is supposed to let you cancel the shutdown, but it often gets ignored and anyway, it doesn’t let you inform the user as to why your app is preventing them from rebooting. In this post I will cut and paste a few chunks of code from an application I’m working on which has, in the user’s interest, a need to stop the machine being switched off. Note that I am assuming some MVVM knowledge, but the principle is the same.

The System.Windows.Application class, from which a standard WPF application class or entrypoint derives, offers up a SessionEnding event to which you can add a handler and use the SessionEndingCancelEventArgs.Cancel property to veto a system shutdown.

Before I go any further, I must stress that Microsoft advises that you don’t do this unless its for a very important reason such as a CD-ROM being burned (which would ruin the disc, if interrupted). Guidance on this and on the timings of forced closures etc. and even the wording you should use in your reasons (which I’ll come to later) can be found here: http://msdn.microsoft.com/en-us/library/ms700677(VS.85).aspx

A bit hit and miss

So my point here is that Microsoft don’t want you to do this in the first place and that the wording on the Microsoft.Win32 version of effectively the same event is that it is not guaranteed. See here.

The way I look at it is if you feel you’ve good enough reason to stop a shutdown and potentially annoy your users (who will inevitably blame Windows and Microsoft), then the system to do it should be guaranteed. In fact, the system used by unmanaged coders is.

In the unmanaged world, session ending notification comes via a message sent to the window. As we have already a system to listen for this, we will just use the WPF SessionEnding event.

As I’m using the Model-View-ViewModel (MVVM) pattern, my code will need some extra explanations but the important stuff is clear.

My ViewModel knows not of the App class of my application, so this code goes outside of where the actual action takes place, in the App_Startup handler.

 

this.SessionEnding += new SessionEndingCancelEventHandler(App_SessionEnding);

void App_SessionEnding(object sender, SessionEndingCancelEventArgs e)
{
    int r = 1;
    if (e.ReasonSessionEnding == ReasonSessionEnding.Logoff)
        r = 0;

    bool cancel = false;
    this.MainWindowViewModel.OnShutdown(r, ref cancel);
    this.MainWindowViewModel.IsHandlingShutdownRequest = false;
    e.Cancel = cancel;
}

 

My ViewModel base class has an OnShutdown virtual method that can be implemented by any derived view model, I do this because a) the view model often knows of the application state better than the App class does (which has no real logic) and b) view models are usually tied to windows and the Win32 API needs the window for this thing to work.

I abstract the reason enums to make it more flexible. The ref cancel argument is used because I don’t want to block a shutdown and then have WPF unblock it by returning cancel = false

 

public override void OnShutdown(int shutdownType, ref bool cancel)
{
    if (this.IsHandlingShutdownRequest) // Subsequent calls allow time for first call.
    {
        int cycles = 0;
        while ((this.IsHandlingShutdownRequest) && cycles < 40) // 40*100 = 4000ms = 4sec
        {
            cycles++;
            System.Threading.Thread.Sleep(100);
        }
        System.Diagnostics.Trace.TraceInformation(
           @"The client waited and ignored a concurrent request by the OS to shutdown.");
        return;
    }
    this.IsHandlingShutdownRequest = true;

    System.Diagnostics.Trace.TraceInformation(
        @"The client handled a request by the OS to shutdown.");

    if (shutdownType > -1) // Change to whatever.
    {                
        this.DispatchMessage(@"Windows is shutting down.");

        if (this.IsBurningCD)
        {
            string msg = @"A CD is being burned.";            
            this.DispatchMessage(msg, true); // Uses dispatcher to put a
                     // message on screen, true make the window pop up.
            this.IsShutdownBlock = true; // Trigger for WPF animation/alert.

            System.Diagnostics.Trace.TraceWarning(msg);

            this.TryBlockShutdown(msg);
            cancel = true;

            this.IsHandlingShutdownRequest = false;
            return;
        }
    }

    this.DispatchMessage(@"Client is closing down.");

    // Some clean up tasks.

   
    System.Diagnostics.Trace.TraceInformation(
        @"The client closed itself in response to an OS shutdown.");

    this.TryUnblockShutdown();
    this.ShutdownApplication(); // Calls a delegate on the ViewModel (code not shown).
}

 

A few interesting points in the code above. The first section is experimental/stupid: I don’t want to have a subsequent/concurrent caller dismissed and give the wrong message back to Windows so I sit on them for 4s. I should use a wait handle but that would require complex explaining – plus it works as is.

The second bit reacts to the shutdown type and starts doing some UI work. This being MVVM, a storyboard triggered by a property here could flag up the impending doom to the user and add to their awareness of the problem.

Then a call is made to TryBlockShutdown() which does the dirty deed before cancel is set appropriately and the call returns.

Where execution determines that the shutdown is fine, a message is sent to the UI and a call is made to TryUnblockShutdown() just in case it was left ‘switched on’ somewhere.

Note that you may be able to use these methods outside of a handler and not call them from within – block and unblock in response to Before/AfterCDBurn events for example, however I prefer to handle the session ending event so I can make sure to unblock the shutdown.

Microsoft urges you to pre-empt rather than react on-the-fly to the session ending event. Registering a reason to block a shutdown seems to live in memory until the window is torn down, so you must always call the unblock API.

Important: When the window closes, your block will be lost. My own application, which is a background app, actually hides the main window upon closure to make sure that this doesn’t happen.

The certain way

Behind the TryBlockShutdown method is a call to ShutdownBlockReasonCreate within user32.dll. Calling managed code requires the UnmanagedCode permission and so this is demanded and gracefully dealt with here.

 

private bool TryBlockShutdown(string reason)
{
    bool rv = false;
    IntPtr hWnd;
    if (this.TryGetWindowHandle(out hWnd))
    {
        try
        {
            var p = new System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityPermissionFlag.UnmanagedCode);
            p.Demand();
            ((System.Windows.Threading.Dispatcher)this.Dispatcher).Invoke(new Action(
                delegate
                {
                    rv = ShutdownBlockReasonCreate(hWnd, reason);
                }));
        }
        catch { }
    }
    return rv;
}

[DllImport("user32.dll")]
private static extern bool ShutdownBlockReasonCreate(IntPtr HWnd,
    [MarshalAs(UnmanagedType.LPWStr)] string reason);

private bool TryUnblockShutdown()
{
    bool rv = false;
    IntPtr hWnd;
    if (this.TryGetWindowHandle(out hWnd))
    {
        try
        {
            var p = new System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityPermissionFlag.UnmanagedCode);
            p.Demand();
            ((System.Windows.Threading.Dispatcher)this.Dispatcher).Invoke(new Action(
                delegate
                {
                    rv = ShutdownBlockReasonDestroy(hWnd);
                }));
        }
        catch { }
    }
    return rv;
}

[DllImport("user32.dll")]
private static extern bool ShutdownBlockReasonDestroy(IntPtr HWnd);

 

So the main points to note here are the static extern bool methods, decorated with DllImportAttribute attributes and the MarshalAs attribute which appears before the string type definition for the reason argument.

Secondly and importantly, there is a call to this.Dispatcher which on my view model is the Dispatcher for the window so I can perform some work on the UI thread if I need to, which I do here.

Important: Calls to ShutdownBlockReasonXXX must be performed on the thread that owns/created the window to which the block applies and in WPF, this is the UI thread which is available via the Dispatcher.

Finally, there is TryGetWindowHandle which for me is defined in the base ViewModel class and skipping some of the logic for clarity, eventually ends up calling this code:

 

public static IntPtr GetWindowHandle(System.Windows.Window window)
{
    return (new System.Windows.Interop.WindowInteropHelper(window)).Handle;
}

 

And that’s that. More information about the Model-View-ViewModel can found at these places:

http://msdn.microsoft.com/en-us/magazine/dd419663.aspx

http://jonas.follesoe.no/

And more on calling unmanaged code, here:

http://msdn.microsoft.com/en-us/library/aa288468(VS.71).aspx

Labels: , ,

0 comments:

Post a Comment