Introduction
In some applications, we need to implement Auto logoff feature for security reasons. In auto log off feature, the application redirects the user into login window, if the user remains inactive for a certain amount of time. I have written this to describe how I came up with implementing auto logoff feature based on user inactivity in WPF application. In web application, session times out and redirects to login page if the user remains inactive for a certain amount of time (set by the application). However, this is not the case in desktop application. In desktop application, you have to implement log off feature explicitly. There are many ways to implement log off feature in a WPF application, which I will describe in this article. From my point of view, implementing auto logoff feature using hooking technique is the best way in terms of performance. Here I have described the implementation of auto logoff using hooking approach and provided the source code for it. This is a simple task, but I am posting it here because I believe it can save some of your valuable time.
Basic Idea of Implementation
Here auto logoff feature has been implemented like the following: system logs out an operator automatically, when the operator remains inactive for his auto logoff time. This feature has been implemented as sliding, i.e., the timer is restarted when the operator activities such as mouse clicks and keyboard entries occur. The approach of implementation is in the following:
- A timer is started with interval equal to user logoff time and on tick event of this timer, the operator is redirected to login window.
- Each window listens for the OS(Operation System) messages per window basis by hooking technique (If you hook per window basis, Windows operating system only sends to the window the messages, which is that window specific.)
- If an OS message is received, the OS message is checked to test whether it is User activity or not. If the message is user activity, the timer is reset (it will start over the timer). Otherwise nothing is done.
How Can An Application Listen to Operating System (OS) Messages?
An application can listen to OS messages using hooking. According to MSDN, A hook
is a point in the system message-handling mechanism where an application can install a subroutine to monitor the message traffic in the system and process certain types of messages before they reach the target window procedure. Windows hooks are implemented using callback functions. Examples of hooking: intercepting keyboard or mouse event messages before they reach an application. Windows OS supports many different types of hooks; each type provides access to a different aspect of its message-handling mechanism. For example, an application can use the WH_MOUSE
Hook to monitor the message traffic for mouse messages. Here each window has used hook to monitor the message traffic for the particular window.
How Does Application Test Whether the OS Message is User Activity or Not?
Each OS message corresponds to a constant integer value, which indicates the type of message. For example, OS message value of MOUSEHOVER
is 0x02A1
(WM_MOUSEHOVER = 0x02A1
). As every windows message has a predefined integer value, the application keeps a list of integer values, which represent user activity type OS messages. On receiving an OS message, the application tests whether there is a match between the received OS message integer value and list integer value. The complete list for the integer value of each message (In Hexadecimal) is in the following:
WM_NULL = 0x0000, WM_CREATE = 0x0001, WM_DESTROY = 0x0002
WM_MOVE = 0x0003, WM_SIZE = 0x0005, WM_ACTIVATE = 0x0006
WM_SETFOCUS = 0x0007, WM_KILLFOCUS = 0x0008, WM_ENABLE = 0x000A
WM_SETREDRAW = 0x000B, WM_SETTEXT = 0x000C, WM_GETTEXT = 0x000D
WM_GETTEXTLENGTH = 0x000E, WM_PAINT = 0x000F, WM_CLOSE = 0x0010
WM_QUERYENDSESSION = 0x0011, WM_QUIT = 0x0012, WM_QUERYOPEN = 0x0013
WM_ERASEBKGND = 0x0014, WM_SYSCOLORCHANGE = 0x0015, WM_ENDSESSION = 0x0016
WM_SHOWWINDOW = 0x0018, WM_CTLCOLOR = 0x0019, WM_WININICHANGE = 0x001A
WM_SETTINGCHANGE = 0x001A, WM_DEVMODECHANGE = 0x001B, WM_ACTIVATEAPP = 0x001C
WM_FONTCHANGE = 0x001D, WM_TIMECHANGE = 0x001E, WM_CANCELMODE = 0x001F
WM_SETCURSOR = 0x0020, WM_MOUSEACTIVATE = 0x0021, WM_CHILDACTIVATE = 0x0022
WM_QUEUESYNC = 0x0023, WM_GETMINMAXINFO = 0x0024, WM_PAINTICON = 0x0026
WM_ICONERASEBKGND = 0x0027, WM_NEXTDLGCTL = 0x0028, WM_SPOOLERSTATUS = 0x002A
WM_DRAWITEM = 0x002B, WM_MEASUREITEM = 0x002C, WM_DELETEITEM = 0x002D
WM_VKEYTOITEM = 0x002E, WM_CHARTOITEM = 0x002F, WM_SETFONT = 0x0030
WM_GETFONT = 0x0031, WM_SETHOTKEY = 0x0032, WM_GETHOTKEY = 0x0033
WM_QUERYDRAGICON = 0x0037, WM_COMPAREITEM = 0x0039, WM_GETOBJECT = 0x003D
WM_COMPACTING = 0x0041, WM_COMMNOTIFY = 0x0044 , WM_WINDOWPOSCHANGING = 0x0046
WM_WINDOWPOSCHANGED = 0x0047, WM_POWER = 0x0048, WM_COPYDATA = 0x004A
WM_CANCELJOURNAL = 0x004B, WM_NOTIFY = 0x004E, WM_INPUTLANGCHANGEREQUEST = 0x0050
WM_INPUTLANGCHANGE = 0x0051, WM_TCARD = 0x0052, WM_HELP = 0x0053
WM_USERCHANGED = 0x0054, WM_NOTIFYFORMAT = 0x0055, WM_CONTEXTMENU = 0x007B
WM_STYLECHANGING = 0x007C, WM_STYLECHANGED = 0x007D, WM_DISPLAYCHANGE = 0x007E
WM_GETICON = 0x007F, WM_SETICON = 0x0080, WM_NCCREATE = 0x0081
WM_NCDESTROY = 0x0082, WM_NCCALCSIZE = 0x0083, WM_NCHITTEST = 0x0084
WM_NCPAINT = 0x0085, WM_NCACTIVATE = 0x0086, WM_GETDLGCODE = 0x0087
WM_SYNCPAINT = 0x0088, WM_NCMOUSEMOVE = 0x00A0, WM_NCLBUTTONDOWN = 0x00A1
WM_NCLBUTTONUP = 0x00A2, WM_NCLBUTTONDBLCLK = 0x00A3, WM_NCRBUTTONDOWN = 0x00A4
WM_NCRBUTTONUP = 0x00A5, WM_NCRBUTTONDBLCLK = 0x00A6, WM_NCMBUTTONDOWN = 0x00A7
WM_NCMBUTTONUP = 0x00A8, WM_NCMBUTTONDBLCLK = 0x00A9, WM_KEYDOWN = 0x0100
WM_KEYUP = 0x0101, WM_CHAR = 0x0102, WM_DEADCHAR = 0x0103
WM_SYSKEYDOWN = 0x0104, WM_SYSKEYUP = 0x0105, WM_SYSCHAR = 0x0106
WM_SYSDEADCHAR = 0x0107, WM_KEYLAST = 0x0108, WM_IME_STARTCOMPOSITION = 0x010D
WM_IME_ENDCOMPOSITION = 0x010E, WM_IME_COMPOSITION = 0x010F, WM_IME_KEYLAST = 0x010F
WM_INITDIALOG = 0x0110, WM_COMMAND = 0x0111, WM_SYSCOMMAND = 0x0112
WM_TIMER = 0x0113, WM_HSCROLL = 0x0114, WM_VSCROLL = 0x0115
WM_INITMENU = 0x0116, WM_INITMENUPOPUP = 0x0117, WM_MENUSELECT = 0x011F
WM_MENUCHAR = 0x0120, WM_ENTERIDLE = 0x0121, WM_MENURBUTTONUP = 0x0122
WM_MENUDRAG = 0x0123, WM_MENUGETOBJECT = 0x0124, WM_UNINITMENUPOPUP = 0x0125
WM_MENUCOMMAND = 0x0126, WM_CTLCOLORMSGBOX = 0x0132, WM_CTLCOLOREDIT = 0x0133
WM_CTLCOLORLISTBOX = 0x0134, WM_CTLCOLORBTN = 0x0135, WM_CTLCOLORDLG = 0x0136
WM_CTLCOLORSCROLLBAR = 0x0137, WM_CTLCOLORSTATIC = 0x0138, WM_MOUSEMOVE = 0x0200
WM_LBUTTONDOWN = 0x0201, WM_LBUTTONUP = 0x0202, WM_LBUTTONDBLCLK = 0x0203
WM_RBUTTONDOWN = 0x0204, WM_RBUTTONUP = 0x0205, WM_RBUTTONDBLCLK = 0x0206
WM_MBUTTONDOWN = 0x0207, WM_MBUTTONUP = 0x0208, WM_MBUTTONDBLCLK = 0x0209
WM_MOUSEWHEEL = 0x020A, WM_PARENTNOTIFY = 0x0210, WM_ENTERMENULOOP = 0x0211
WM_EXITMENULOOP = 0x0212, WM_NEXTMENU = 0x0213, WM_SIZING = 0x0214
WM_CAPTURECHANGED = 0x0215, WM_MOVING = 0x0216, WM_DEVICECHANGE = 0x0219
WM_MDICREATE = 0x0220, WM_MDIDESTROY = 0x0221, WM_MDIACTIVATE = 0x0222
WM_MDIRESTORE = 0x0223, WM_MDINEXT = 0x0224, WM_MDIMAXIMIZE = 0x0225
WM_MDITILE = 0x0226, WM_MDICASCADE = 0x0227, WM_MDIICONARRANGE = 0x0228
WM_MDIGETACTIVE = 0x0229, WM_MDISETMENU = 0x0230, WM_ENTERSIZEMOVE = 0x0231
WM_EXITSIZEMOVE = 0x0232, WM_DROPFILES = 0x0233, WM_MDIREFRESHMENU = 0x0234
WM_IME_SETCONTEXT = 0x0281, WM_IME_NOTIFY = 0x0282, WM_IME_CONTROL = 0x0283
WM_IME_COMPOSITIONFULL = 0x0284, WM_IME_SELECT = 0x0285, WM_IME_CHAR = 0x0286
WM_IME_REQUEST = 0x0288, WM_IME_KEYDOWN = 0x0290, WM_IME_KEYUP = 0x0291
WM_MOUSEHOVER = 0x02A1, WM_MOUSELEAVE = 0x02A3, WM_CUT = 0x0300
WM_COPY = 0x0301, WM_PASTE = 0x0302, WM_CLEAR = 0x0303
WM_UNDO = 0x0304, WM_RENDERFORMAT = 0x0305, WM_RENDERALLFORMATS = 0x0306
WM_DESTROYCLIPBOARD = 0x0307, WM_DRAWCLIPBOARD = 0x0308, WM_PAINTCLIPBOARD = 0x0309
WM_VSCROLLCLIPBOARD = 0x030A, WM_SIZECLIPBOARD = 0x030B, WM_ASKCBFORMATNAME = 0x030C
WM_CHANGECBCHAIN = 0x030D, WM_HSCROLLCLIPBOARD = 0x030E, WM_QUERYNEWPALETTE = 0x030F
WM_PALETTEISCHANGING = 0x0310, WM_PALETTECHANGED = 0x0311, WM_HOTKEY = 0x0312
WM_PRINT = 0x0317, WM_PRINTCLIENT = 0x0318, WM_HANDHELDFIRST = 0x0358
WM_HANDHELDLAST = 0x035F, WM_AFXFIRST = 0x0360, WM_AFXLAST = 0x037F
WM_PENWINFIRST = 0x0380, WM_PENWINLAST = 0x038F, WM_APP = 0x8000
WM_USER = 0x0400, WM_REFLECT = WM_USER + 0x1c00
Why Has System.windows.forms/System.Timers.timer Been Used Instead of Dispatcher Timer?
System.windows.forms timer
/ System.Timers.timer
runs on a different thread than the user interface (UI) thread whereas DispatcherTimer
runs in the user interface (UI) thread. For this reason, DispatcherTimer
is independent on UI updating but System.windows.forms
timer is not. According to MSDN, DispatcherTimer
is a timer that is integrated into the dispatcher queue which is processed at a specified interval of time and at a specified priority. Dispatcher timers
are not guaranteed to execute exactly when the time interval occurs, but they are guaranteed to not execute before the time interval occurs. This is because DispatcherTimer
operations are placed on the dispatcher queue like other UI operations. The execution of tick event of DispatcherTimer
is dependent on the other UI jobs and their priorities in the dispatcher queue. System.windows.forms/System.Timers.timer
is guaranteed to execute exactly when the time interval occurs.
Why Does System.Windows.Threading.Dispatcher.CurrentDispatcher.
Hooks.OperationPosted Event Not Work For Me?
It may seem that System.Windows.Threading.Dispatcher.CurrentDispatcher.
Hooks.OperationPostedevent
can be used to implement logoff feature in a WPF application. This event is fired when something is added to the WPF dispatcher queue
. When any user activity happens, then a work item is posted in dispatcher queue
as well as if the result of any background processing tries to Update the UI like a clock in the window, then a work item is posted in dispatcher queue
. This situation is also true when the application is getting something from the network and tries to update in UI. So the bottom line is that this event is fired not only on User activity but also on UI update from code. So detecting OperationPosted
does not work when the application has some background work which updates UI. If an application has no background work which updates UI, then OperationPosted
event will work for that application to implement autologoff feature.
Why Is GetLastInputInfo Function Not Used to Determine User Inactivity?
Using GetLastInputInfo
function, someone can implement autologoff feature as this function retrieves the time of the last input event. However, you have to call this function periodically to detect input idle for a certain time. In this way, the application has to use polling strategy to implement autologoff and the application will make CPU unnecessarily busy. Therefore, if someone does not care about CPU usage (performance), he can go in this way.
What Will You Do If You Have Multiple Windows in Your Application?
As we know, here application is listening for window specific OS messages, OS will not send one window's message to a different window. For that reason, in each window, the application has to listen for the OS messages. So the application has to hook for the windows message in each window and when the message of type user activity is received, the application has to reset the timer. Therefore, you have to write some repetitive code in each window.
Description of Code
An Autologoff
timer class named AutoLogOffHelper
has been implemented to provide timer functionality according to autologoff
feature. AutoLogOffHelper
class exposes functionality to start auto logoff timer and to reset auto logoff time. To start auto logoff timer, the method named StartAutoLogoffOption()
of AutoLogOffHelper
class has to be called. System.Windows.Interop.ComponentDispatcher.ThreadIdle
is fired when the thread is idle. Here Thread Idle means there is no message in the dispatcher queue
. Here, the application starts the timer at the Thread Idle event handler. In the StartAutoLogoffOption()
method, System.Windows.Interop.ComponentDispatcher.ThreadIdle
event is set to event handler named DispatcherQueueEmptyHandler
. The used code for AutoLogOffHelper
class is in the following:
class AutoLogOffHelper
{
static System.Windows.Forms.Timer _timer = null;
static private int _logOffTime;
static public int LogOffTime
{
get { return _logOffTime; }
set { _logOffTime = value; }
}
public delegate void MakeAutoLogOff();
static public event MakeAutoLogOff MakeAutoLogOffEvent;
static public void StartAutoLogoffOption()
{
System.Windows.Interop.ComponentDispatcher.ThreadIdle += new
EventHandler(DispatcherQueueEmptyHandler);
}
static void _timer_Tick(object sender, EventArgs e)
{
if (_timer != null)
{
System.Windows.Interop.ComponentDispatcher.ThreadIdle -= new
EventHandler(DispatcherQueueEmptyHandler);
_timer.Stop();
_timer = null;
if (MakeAutoLogOffEvent != null)
{
MakeAutoLogOffEvent();
}
}
}
static void DispatcherQueueEmptyHandler(object sender, EventArgs e)
{
if (_timer == null)
{
_timer = new System.Windows.Forms.Timer();
_timer.Interval = LogOffTime * 60 * 1000;
_timer.Tick += new EventHandler(_timer_Tick);
_timer.Enabled = true;
}
else if (_timer.Enabled == false)
{
_timer.Enabled = true;
}
}
static public void ResetLogoffTimer()
{
if (_timer != null)
{
_timer.Enabled = false;
_timer.Enabled = true;
}
}
}
This class exposes a property named LogOffTime
by which application can set logoff time. In the DispatcherQueueEmptyHandler
, a timer is started with setting interval equal to LogOffTime
time. The tick event of timer
class is set to the event handler named _timer_Tick
. The tick
event of timer is fired means that logoff time has elapsed without any user activity and user has to be redirected to Login window. For that reason, in the _timer_Tick
method, MakeAutoLogOffEvent
is fired to notify the application window to redirect the operator to login window. The method ResetLogoffTimer
of this class is exposed to reset the timer when a user activity occurs.
To track user activity on a window, the Win32 handler of the window is retrieved. HwndSource.FromHwnd
method returns an HwndSource
for a window where HwndSource
represents WPFcontent within a Win32 window. Then AddHook
method is used to add a callback method named CallBackMethod
, which will receive all messages for the window. For this, the following code has been used:
HwndSource windowSpecificOSMessageListener = HwndSource.FromHwnd(new
WindowInteropHelper(this).Handle);
windowSpecificOSMessageListener.AddHook(new HwndSourceHook(CallBackMethod));
After that LogOffTime
property of AutoLogOffHelper
class is set and MakeAutoLogOffEvent
event of class AutoLogOffHelper
is set to the event handler named AutoLogOffHelper_MakeAutoLogOffEvent
, which redirects user to Login window. Then call the method StartAutoLogoffOption
to start auto logoff timer.
AutoLogOffHelper.LogOffTime = logOffTime;
AutoLogOffHelper.MakeAutoLogOffEvent +=
new AutoLogOffHelper.MakeAutoLogOff(AutoLogOffHelper_MakeAutoLogOffEvent);
AutoLogOffHelper.StartAutoLogoffOption();
In the Callback Method
, all the OS messages of this window specific is received. Then OS message is tested to determine whether the message is user activity or not. For example, 0x0021 is used to test whether windows message is MOUSEACTIVATE
or not. If the message is user activity, AutoLogOffHelper.ResetLogoffTimer()
is called to reset the timer. For this, the following code is used:
private IntPtr CallBackMethod(IntPtr hwnd,
int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if ((msg >= 0x0200 && msg <= 0x020A) || (msg <= 0x0106 && msg >= 0x00A0) ||
msg == 0x0021)
{
AutoLogOffHelper.ResetLogoffTimer();
string time = DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss tt");
tblStatus.Text = "Timer is reset on user activity at " + ": " + time;
tblLastUserActivityTypeOSMessage.Text =
"Last user activity type OS message the application considers " +
": 0x" + msg.ToString("X");
}
else
{
tblLastOSMessage.Text = "Last OS Message " + ": 0x" + msg.ToString("X");
System.Diagnostics.Debug.WriteLine(msg.ToString());
}
return IntPtr.Zero;
}
Conclusion
Thanks for reading this write up. Though it is very simple work, I have posted this because I think many people like me need this feature. Hope this will save some of your time. If you guys have any question, I will love to answer. I always appreciate comments.
History
- Initial release – 09/10/09
References