A recent project at work needed to detect when the user locked and unlocked their workstation. As I'd seen some great examples do this previously, I didn't think much of it. However, because WPF is a very different beast compared to Windows Forms, it's a slightly different approach.
Neat stuff like detecting operating system events isn't part of the .NET framework and it requires calling outside to native Win32 API using P/Invoke features of the CLR. If you've done any Win32 programming, you'd know that it's largely based on Handles, IntrPtr's and messages. And as a breath of fresh air, the WPF API is focused primarily on building rich user-interfaces and is completely devoid of legacy Win32 programming concepts. WPF is a huge leap, but worth it.
Fortunately, interoperability with Win32 is a breeze so if you want to tap into native Windows API, it's available if you're willing to write some code.
I should point out that most of this code has been adapted from this post over at the .NET Security Blog. As shawnfa's post goes over the API in detail, I won't cover it here, rather I'll focus on how to pull this off in WPF. Also, as I'm a big fan of composition in favour over inheritance, I've pulled all the session management stuff into an encapsulated class to make it easier to "mix in". I haven't found any issues with this approach, but I'd welcome feedback.
Tapping into the Windows API can be mixed into your app using the HwndSource class. It requires a handle to the calling window, and since the Handle property doesn't exist on the WPF Window class, you'll have to use the WindowInteropHelper to expose it. The only gotcha here is that the Handle isn't available until after the window has been loaded. This is analogous to the Form.OnHandleCreated method in Windows Form programming.
Here's my adapted sample:
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
namespace LockedWorkstationExample
{
public class SessionNotificationUtil : IDisposable
{
// from wtsapi32.h
private const int NotifyForThisSession = 0;
// from winuser.h
private const int SessionChangeMessage = 0x02B1;
private const int SessionLockParam = 0x7;
private const int SessionUnlockParam = 0x8;
[DllImport("wtsapi32.dll")]
private static extern bool WTSRegisterSessionNotification(IntPtr hWnd, int dwFlags);
[DllImport("wtsapi32.dll")]
private static extern bool WTSUnRegisterSessionNotification(IntPtr hWnd);
// flag to indicate if we've registered for notifications or not
private bool registered = false;
WindowInteropHelper interopHelper;
/// <summary>
/// Constructor
/// </summary>
/// <param name="window"></param>
public SessionNotificationUtil(Window window)
{
interopHelper = new WindowInteropHelper(window);
window.Loaded += new RoutedEventHandler(window_Loaded);
}
// deferred initialization logic
void window_Loaded(object sender, RoutedEventArgs e)
{
HwndSource source = HwndSource.FromHwnd(interopHelper.Handle);
source.AddHook(new HwndSourceHook(WndProc));
EnableRaisingEvents = true;
}
protected bool EnableRaisingEvents
{
get { return registered; }
set
{
// WtsRegisterSessionNotification requires Windows XP or higher
bool haveXp = Environment.OSVersion.Platform == PlatformID.Win32NT &&
(Environment.OSVersion.Version.Major > 5 ||
(Environment.OSVersion.Version.Major == 5 &&
Environment.OSVersion.Version.Minor >= 1));
if (!haveXp)
{
registered = false;
return;
}
if (value == true && !registered)
{
WTSRegisterSessionNotification(interopHelper.Handle, NotifyForThisSession);
registered = true;
}
else if (value == false && registered)
{
WTSUnRegisterSessionNotification(interopHelper.Handle);
registered = false;
}
}
}
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == SessionChangeMessage)
{
if (wParam.ToInt32() == SessionLockParam)
{
OnSessionLock();
}
else if (wParam.ToInt32() == SessionUnlockParam)
{
OnSessionUnLock();
}
}
return IntPtr.Zero;
}
private void OnSessionLock()
{
if (SessionChanged != null)
{
SessionChanged(this, new SessionNotificationEventArgs(SessionNotification.Lock));
}
}
private void OnSessionUnLock()
{
if (SessionChanged != null)
{
SessionChanged(this, new SessionNotificationEventArgs(SessionNotification.Unlock));
}
}
public event EventHandler<SessionNotificationEventArgs> SessionChanged;
#region IDisposable Members
public void Dispose()
{
// unhook from wtsapi
if (registered)
{
EnableRaisingEvents = false;
}
}
#endregion
}
public class SessionNotificationEventArgs : EventArgs
{
public SessionNotificationEventArgs(SessionNotification notification)
{
_notification = notification;
_timestamp = DateTime.Now;
}
public SessionNotification Notification
{
get { return _notification; }
}
public DateTime TimeStamp
{
get { return _timestamp; }
}
private SessionNotification _notification;
private DateTime _timestamp;
}
public enum SessionNotification
{
Lock = 0,
Unlock
}
}
At this point, I can mix in session notification wherever needed without having to introduce a base window class:
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
SessionNotificationUtil util = new SessionNotificationUtil(this);
util.SessionChanged += new EventHandler<SessionNotificationEventArgs>(util_SessionChanged);
}
void util_SessionChanged(object sender, SessionNotificationEventArgs e)
{
this.txtOutput.Text += String.Format("Recieved {0} notification at {1}\n", e.Notification, e.TimeStamp);
}
}
Update 10/23/2008: Source code now available for download.