Click here to Skip to main content
15,888,113 members
Articles / Desktop Programming / Windows Forms
Article

Detecting Application Idleness

Rate me:
Please Sign up or sign in to vote.
4.71/5 (25 votes)
11 Apr 2006CPOL5 min read 164.7K   7.6K   129   22
A utility class that alerts your code when the application is idle.

Sample Image - screenshot.jpg

Introduction

If you've ever written a GUI (WinForms) application and wanted to periodically run a background process (such as poll a database), or need to log off the user or close a connection after a certain amount of inactivity, this class may be for you.

Background

I created this class so my application could occasionally check the database for updated records (by other users). I didn't want to run the check while the user was busy entering data, and I certainly didn't want to hit the database in the middle of some transactional save and risk blocking or even deadlocking the operation. So I needed to know when both the user and the CPU were idle. I also wanted to know when the application became idle, and also wanted periodic updates while the application remained idle.

I wound up using a two-step detection process:

  1. Hook into the System.Windows.Forms.Application.Idle event to help detect when the user is idle. Note, I said help ....
  2. If the user is idle, use System.Diagnostics.Process.GetCurrentProcess().TotalProcessorTime to determine if the CPU is idle (at least for this process).

I also had to add a third component, a timer, in order to continue detecting idle state. I found that while an application has focus, it seems to get a "pulse" (in the form of an Application.Idle event) once a second, even if there are no timers in the application. However, once a different application gets focus, the background app no longer gets that pulse. If I dig around, I would expect to find that Windows™ is sending a system clock update to whichever application has focus. Regardless of its origin, I needed to give the class its own "heartbeat" so it would continue to operate as a background application.

Using the code

The main code is in the static class ApplicationIdleTimer.

C#
public static event ApplicationIdle;

This static event is what you would normally use to listen for idleness. When the application is determined to be idle, you will receive an event containing an ApplicationIdleEventArgs which tells you:

  • DateTime IdleSince: the time the application became idle.
  • TimeSpan IdleDuration: the length of time the application has been idle.

Additionally, you can query the following static class properties to learn:

  • bool IsIdle : Returns the last determined idle state. Idle state is recomputed once per second. Both the GUI and the CPU must be idle for this property to be true.
  • double CurrentGUIActivity: Returns an "indication" of the GUI activity, expressed as activity per second. 0 indicates no activity. GUI activities include user interactions (typing, moving mouse) as well as events, paint operations, etc.
  • double CurrentCPUUsage: Returns the percent CPU use for the current process (0.0-1.0). Will return double.NaN if indeterminate.

Additionally, there are a couple of adjustable settings you can use to tune the idle detection for your application:

  • double GUIActivityThreshold: The threshold (GUI activity) for determining idleness. GUI activity below this level is considered "idle".
  • double CPUUsageThreshold: The threshold (CPU usage) for determining idleness. CPU usage below this level is considered "idle". A value >= 1.0 will disable CPU idle checking.

The demo app, shown above, displays all the available properties, and allows you to adjust behavior at run-time. Run the application and simply move the mouse, and the status indicator should flip from "idle" to "busy". Increase the GUI threshold until normal mouse movement does not reset the state. Then click the "Do iterations" button to perform a CPU-intensive task, which will also flip the state to "busy". You can adjust the CPU threshold as well, and a threshold of 100% will disable CPU checks in the idle detection. The "yield" checkbox determines if the form will continue to process GUI messages while the iterations are running.

If you watch closely, you may notice that the GUI indicator sometimes exceeds its threshold, but the class continues to think the application is idle! This is because the activity may occasionally spike, but the threshold is compared to average activity per second. So as long as the average activity over a second stays below the threshold, the application is deemed idle. The CPU counter works generally the same way, except its duration can vary (based on how frequently -- or infrequently -- it is called).

Points of Interest

In reality, this demo is not the best demonstration, because all the timers and frequent painting tend to throw off the idle detection. The act of showing you how idle the application is keeps it fairly busy!

Watch the App.Idle events counter in the demo. It displays the total number of System.Windows.Forms.Application.Idle events received. You may want to compare it to the display from the following extremely trimmed down code:

C#
using System;
using System.Windows.Forms;

namespace Demo
{
    public class IdleForm : System.Windows.Forms.Form
    {
        private System.Windows.Forms.Label label1;
        private long idleCounter = 0;

        public IdleForm()
        {
            InitializeComponent();
            Application.Idle += 
               new System.EventHandler(this.Idle_Count);
        }
        private void InitializeComponent()
        {
            this.Size = new System.Drawing.Size(300,300);
            this.label1 = new System.Windows.Forms.Label();
            this.label1.Font = 
                new System.Drawing.Font("Microsoft Sans Serif", 18F, 
                System.Drawing.FontStyle.Regular, 
                System.Drawing.GraphicsUnit.Point, ((System.Byte)(0)));
            this.label1.Location = new System.Drawing.Point(0, 0);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(300,300);
            this.Controls.Add(label1);
        }

        public static void Main(string[] args) 
        {
            Application.Run(new IdleForm());
        }
        
        private void Idle_Count(object sender, System.EventArgs e)
        {
            idleCounter ++;
            label1.Text = idleCounter.ToString();
        }
    }
}

Areas of Improvement

  • Because "idleness" depends to a large degree on the capacity and speed of the machine the application runs on, one improvement would be to let the threshold values "auto-tune" by collecting usage stats for a few minutes, and adjusting the values accordingly.
  • A third idle check could be added if you wanted to make sure the computer, and not just the application, is idle. In 1.1, you simply need to monitor Process.GetProcessById(0).TotalProcessorTime, which tells you the amount of CPU time spent on the System Idle Process. In 2.0, I've heard it's a bit more difficult since the Framework no longer allows access to process 0.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Soren.Persian5-Jul-12 5:06
professionalSoren.Persian5-Jul-12 5:06 
QuestionCan Anyone change this C# code to vb.net ( Only ApplicationIdleTimer.cs ) thanks Pin
ThetNaing_8322-Jun-11 0:30
ThetNaing_8322-Jun-11 0:30 
GeneralThis is great Pin
jojodim3-Feb-11 22:23
jojodim3-Feb-11 22:23 
GeneralWhen typing the textbox in the demo application it show it is idle.. Pin
himanshu31322-Oct-09 10:56
himanshu31322-Oct-09 10:56 
GeneralRe: When typing the textbox in the demo application it show it is idle.. Pin
GWSyZyGy22-Oct-09 11:15
GWSyZyGy22-Oct-09 11:15 
GeneralDetect Application Busyness Pin
pluniewski9-Jan-09 0:25
pluniewski9-Jan-09 0:25 
Hey, I find this art very useful.

How about the opposite, guys? I mean the issue to detect app being busy. I case you might want to show a WaitCursor.
Check this out, orginally found here http://www.maximumpc.com/forums/viewtopic.php?t=78317&sid=a6356faf032b93df0c81cdfa17c9e54b[^]

using System; 
using System.Diagnostics; 
using System.Runtime.InteropServices; 
using System.Threading; 
using System.Windows.Forms; 

namespace AutoWaitCursor 
{ 
    #region README 
    /// 
    /// This static utility class can be used to automatically show a wait cursor when the application 
    /// is busy (ie not responding to user input). The class automatically monitors the application 
    /// state, removing the need for manually changing the cursor. 
    /// 
    /// 
    /// To use, simply insert the following line in your Application startup code 
    /// 
    ///  private void Form1_Load(object sender, System.EventArgs e) 
    ///  { 
    ///   AutoWaitCursor.Cursor = Cursors.WaitCursor; 
    ///   AutoWaitCursor.Delay = new TimeSpan(0, 0, 0, 0, 25); 
    ///   // Set the window handle to the handle of the main form in your application 
    ///   AutoWaitCursor.MainWindowHandle = this.Handle; 
    ///   AutoWaitCursor.Start(); 
    ///  } 
    /// 
    /// This installs changes to cursor after 100ms of blocking work (ie. work carried out on the main application thread). 
    /// 
    /// Note, the above code GLOBALLY replaces the following: 
    /// 
    /// public void DoWork() 
    /// { 
    ///  try 
    ///  { 
    ///   Screen.Cursor = Cursors.Wait; 
    ///   GetResultsFromDatabase(); 
    ///  } 
    ///  finally 
    ///  { 
    ///   Screen.Cursor = Cursors.Default; 
    ///  } 
    /// } 
    /// 
    #endregion 

    [DebuggerStepThrough()] 
    public class AutoWaitCursor 
    { 
        #region Member Variables 
        //The application state monitor class (which monitors the application busy status). 
        private static ApplicationStateMonitor _appStateMonitor = new ApplicationStateMonitor(Cursors.WaitCursor, DEFAULT_DELAY); 
        private static readonly TimeSpan DEFAULT_DELAY = new TimeSpan(0, 0, 0, 0, 25); 
        #endregion 

        #region Constructors 
        private AutoWaitCursor() 
        { 
            // Intentionally blank 
        } 
        #endregion 

        #region Public Static Properties 
        // Returns the amount of time the application has been idle. 
        public TimeSpan ApplicationIdleTime 
        { 
            get { return _appStateMonitor.ApplicationIdleTime; } 
        } 

        //Returns true if the auto wait cursor has been started. 
        public static bool IsStarted 
        { 
            get { return _appStateMonitor.IsStarted; } 
        } 

        //Gets or sets the Cursor to use during Application busy periods. 
        public static Cursor Cursor 
        { 
            get { return _appStateMonitor.Cursor; } 
            set { _appStateMonitor.Cursor = value; } 
        } 

        // Enables or disables wait cursor 
        public static bool Enabled 
        { 
            get { return _appStateMonitor.Enabled; } 
            set { _appStateMonitor.Enabled = value; } 
        } 

        // Gets or sets the period of Time to wait before showing the WaitCursor whilst Application is working 
        public static TimeSpan Delay 
        { 
            get { return _appStateMonitor.Delay; } 
            set { _appStateMonitor.Delay = value; } 
        } 

        // Gets or sets the main window handle of the application (ie the handle of an MDI form). 
        // This is the window handle monitored to detect when the application becomes busy. 
        public static IntPtr MainWindowHandle 
        { 
            get { return _appStateMonitor.MainWindowHandle; } 
            set { _appStateMonitor.MainWindowHandle = value; } 
        } 
        #endregion 

        #region Public Methods 
        // Starts the auto wait cursor monitoring the application. 
        public static void Start() { _appStateMonitor.Start(); } 

        // Stops the auto wait cursor monitoring the application. 
        public static void Stop() { _appStateMonitor.Stop(); } 
        #endregion 

        #region Private Class ApplicationStateMonitor 
        // Private class that monitors the state of the application and automatically 
        // changes the cursor accordingly. 
        private class ApplicationStateMonitor : IDisposable 
        { 
            #region Member Variables 
            // The time the application became inactive. 
            private DateTime _inactiveStart = DateTime.Now; 
            // If the monitor has been started. 
            private bool _isStarted = false; 

            // Delay to wait before calling back 
            private TimeSpan _delay; 

            // The windows handle to the main process window. 
            private IntPtr _mainWindowHandle = IntPtr.Zero; 

            // Thread to perform the wait and callback 
            private Thread _callbackThread = null; 

            // Stores if the class has been disposed of. 
            private bool _isDisposed = false; 

            // Stores if the class is enabled or not. 
            private bool _enabled = true; 

            // GUI Thread Id . 
            private uint _mainThreadId; 

            // Callback Thread Id. 
            private uint _callbackThreadId; 

            // Stores the old cursor. 
            private Cursor _oldCursor; 

            // Stores the new cursor. 
            private Cursor _waitCursor; 
            #endregion 

            #region PInvokes 

            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] 
            [return: MarshalAs(UnmanagedType.Bool)] 
            private static extern bool SendMessageTimeout(IntPtr hWnd, int Msg, int wParam, string lParam, int fuFlags, int uTimeout, out int lpdwResult); 

            [DllImport("USER32.DLL")] 
            private static extern uint AttachThreadInput(uint attachTo, uint attachFrom, bool attach); 

            [DllImport("KERNEL32.DLL")] 
            private static extern uint GetCurrentThreadId(); 

            private const int SMTO_NORMAL = 0x0000; 
            private const int SMTO_BLOCK = 0x0001; 
            private const int SMTO_NOTIMEOUTIFNOTHUNG = 0x0008; 

            #endregion 

            #region Constructors 
            // Default member initialising Constructor 
            // The wait cursor to use. 
            // The delay before setting the cursor to the wait cursor. 

            // Constructor is called from (what is treated as) the main thread 
            public ApplicationStateMonitor(Cursor waitCursor, TimeSpan delay) 
            { 
                _mainThreadId = GetCurrentThreadId(); 
                _delay = delay; 
                _waitCursor = waitCursor; 

                // Gracefully shuts down the state monitor 
                Application.ThreadExit += new EventHandler(_OnApplicationThreadExit); 
            } 
            #endregion 

            #region IDisposable 
            // On Disposal terminates the Thread, calls Finish (on thread) if Start has been called 
            public void Dispose() 
            { 
                if (_isDisposed) { return; } 

                // Kills the Thread loop 
                _isDisposed = true; 
            } 

            #endregion IDisposable 

            #region Public Methods 
            // Starts the application state monitor. 
            public void Start() 
            { 
                if (!_isStarted) 
                { 
                    _isStarted = true; 
                    this._CreateMonitorThread(); 
                } 
            } 

            // Stops the application state monitor. 
            public void Stop() 
            { 
                if (_isStarted) 
                { 
                    _isStarted = false; 
                } 
            } 

            // Set the Cursor to wait. 
            public void SetWaitCursor() 
            { 
                // Start is called in a new Thread, grab the new Thread Id so 
                // we can attach to Main thread’s input 
                _callbackThreadId = GetCurrentThreadId(); 

                // Have to call this before calling Cursor.Current 
                AttachThreadInput(_callbackThreadId, _mainThreadId, true); 

                _oldCursor = Cursor.Current; 
                Cursor.Current = _waitCursor; 
            } 

            // Finish showing the Cursor (switch back to previous Cursor) 
            public void RestoreCursor() 
            { 
                // Restore the cursor 
                Cursor.Current = _oldCursor; 
                // Detach from Main thread input 
                AttachThreadInput(_callbackThreadId, _mainThreadId, false); 
            } 

            // Enable/Disable the call to Start (note, once Start is called it *always* calls the paired Finish) 
            public bool Enabled 
            { 
                get { return _enabled; } 
                set { _enabled = value; } 
            } 

            /// Gets or sets the period of Time to wait before calling the Start method 
            public TimeSpan Delay 
            { 
                get { return _delay; } 
                set { _delay = value; } 
            } 

            #endregion 

            #region Public Properties 
            // Returns true if the auto wait cursor has been started. 
            public bool IsStarted 
            { 
                get { return _isStarted; } 
            } 

            // Gets or sets the main window handle of the application (ie the handle of an MDI form). 
            // This is the window handle monitored to detect when the application becomes busy. 
            public IntPtr MainWindowHandle 
            { 
                get { return _mainWindowHandle; } 
                set { _mainWindowHandle = value; } 
            } 

            // Gets or sets the Cursor to show 
            public Cursor Cursor 
            { 
                get { return _waitCursor; } 
                set { _waitCursor = value; } 
            } 

            // Returns the amount of time the application has been idle. 
            public TimeSpan ApplicationIdleTime 
            { 
                get { return DateTime.Now.Subtract(_inactiveStart); } 
            } 

            #endregion 

            #region Private Methods 
            // Prepares the class creating a Thread that monitors the main application state. 
            private void _CreateMonitorThread() 
            { 
                // Create the monitor thread 
                _callbackThread = new Thread(new ThreadStart(_ThreadCallbackLoop)); 
                _callbackThread.Name = "AutoWaitCursorCallback"; 
                _callbackThread.IsBackground = true; 
                // Start the thread 
                _callbackThread.Start(); 
            } 

            // Thread callback method. 
            // Loops calling SetWaitCursor and RestoreCursor until Disposed. 
            private void _ThreadCallbackLoop() 
            { 
                try 
                { 
                    do 
                    { 
                        if (!_enabled || _mainWindowHandle == IntPtr.Zero) 
                        { 
                            // Just sleep 
                            Thread.Sleep(_delay); 
                        } 
                        else 
                        { 
                            // Wait for start 
                            if (_IsApplicationBusy(_delay, _mainWindowHandle)) 
                            { 
                                try 
                                { 
                                    this.SetWaitCursor(); 
                                    _WaitForIdle(); 
                                } 
                                finally 
                                { 
                                    // Always calls Finish (even if we are Disabled) 
                                    this.RestoreCursor(); 
                                    // Store the time the application became inactive 
                                    _inactiveStart = DateTime.Now; 
                                } 
                            } 
                            else 
                            { 
                                // Wait before checking again 
                                Thread.Sleep(25); 
                            } 
                        } 
                    } while (!_isDisposed && _isStarted); 
                } 
                catch (ThreadAbortException) 
                { 
                    // The thread is being aborted, just reset the abort and exit gracefully 
                    Thread.ResetAbort(); 
                } 
            } 

            // Blocks until the application responds to a test message. 
            // If the application doesn’t respond with the timespan, will return false, 
            // else returns true. 
            private bool _IsApplicationBusy(TimeSpan delay, IntPtr windowHandle) 
            { 
                const int INFINITE = Int32.MaxValue; 
                const int WM_NULL = 0; 
                int result = 0; 
                bool success; 

                // See if the application is responding 
                if (delay == TimeSpan.MaxValue) 
                { 
                    success = SendMessageTimeout(windowHandle, WM_NULL, 0, null, 
                    SMTO_BLOCK, INFINITE, out result); 
                } 
                else 
                { 
                    success = SendMessageTimeout(windowHandle, WM_NULL, 0, null, 
                    SMTO_BLOCK, System.Convert.ToInt32(delay.TotalMilliseconds), out result); 
                } 

                if (result != 0) 
                { 
                    return true; 
                } 

                return false; 
            } 

            // Waits for the ResetEvent (set by Dispose and Reset), 
            // since Start has been called we *have* to call RestoreCursor once the thread is idle again. 
            private void _WaitForIdle() 
            { 
                // Wait indefinately until the application is idle 
                _IsApplicationBusy(TimeSpan.MaxValue, _mainWindowHandle); 
            } 

            // The application is closing, shut the state monitor down. 
            private void _OnApplicationThreadExit(object sender, EventArgs e) 
            { 
                this.Dispose(); 
            } 

            #endregion 
        } 
        #endregion 
    } 
} 


have fun!

/PL01

QuestionHow do i avoid cross-threading exception in vb.net? Pin
josepaulino4-Oct-07 14:32
josepaulino4-Oct-07 14:32 
AnswerRe: How do i avoid cross-threading exception in vb.net? Pin
GWSyZyGy29-Oct-07 5:53
GWSyZyGy29-Oct-07 5:53 
GeneralProblems Pin
olegbi_q24-Oct-07 7:40
olegbi_q24-Oct-07 7:40 
GeneralRe: Problems Pin
GWSyZyGy29-Oct-07 5:51
GWSyZyGy29-Oct-07 5:51 
GeneralNice Example! Here a fix for crossthreading issue Pin
wbekker2-Apr-07 23:08
wbekker2-Apr-07 23:08 
QuestionRe: Nice Example! Here a fix for crossthreading issue Pin
josepaulino4-Oct-07 14:28
josepaulino4-Oct-07 14:28 
AnswerRe: Nice Example! Here a fix for crossthreading issue Pin
Patrice Dargenton9-Sep-11 21:26
Patrice Dargenton9-Sep-11 21:26 
QuestionCool, but Pin
alex.almeida14-Feb-07 8:15
alex.almeida14-Feb-07 8:15 
AnswerRe: Cool, but Pin
GWSyZyGy28-Mar-07 5:58
GWSyZyGy28-Mar-07 5:58 
GeneralAnother way Pin
gbarcalow7-Feb-07 12:46
gbarcalow7-Feb-07 12:46 
GeneralRe: Another way Pin
Sara Kulu17-Jul-07 10:58
Sara Kulu17-Jul-07 10:58 
Generalapplication idle time Pin
Higgs9621-Sep-06 10:20
Higgs9621-Sep-06 10:20 
GeneralThmbs up man! Pin
Sing Abend12-Aug-06 5:28
professionalSing Abend12-Aug-06 5:28 
GeneralAwesome seconded! Pin
pennidren14-Jun-06 6:54
pennidren14-Jun-06 6:54 
GeneralAwesome Pin
Girb12-Apr-06 1:54
Girb12-Apr-06 1:54 
GeneralRe: Awesome Pin
GWSyZyGy12-Apr-06 6:19
GWSyZyGy12-Apr-06 6:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.