Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Implementation of Auto Logoff Based on User Inactivity in WPF Application

0.00/5 (No votes)
9 Oct 2009 1  
How to check user inactivity in WPF Application

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)
{
    //  Testing OS message to determine whether it is a user activity or not
    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");
        // For debugging purpose
        // If this auto logoff does not work for some user activity, you 
        // can detect the integer code of that activity  using the following line.
        // Then All you need to do is add this integer code to the above if condition.
        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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here