I have an inactivity timer that keeps checking for inactivity throughout the app. Problem is, when message box is open, the timer is not ticking and checking for inactivity as long as the MessageBox stays open. When it is closed, it works fine.
The code used here is taken from:
How to detect a Winforms app has been idle for certain amount of time
UIInactivity.cs (Track inactivity)
public class UIInactivity
{
public static Timer IdleTimer;
private readonly int _timeoutDuration;
private InactivityState _inactivityState;
public event Action<InactivityState> NotifyInActivity;
public UIInactivity(int timeoutDurationInMinutes, InactivityState inactivityState)
{
_timeoutDuration = (int)TimeSpan.FromMinutes(timeoutDurationInMinutes).TotalMilliseconds;
_inactivityState = inactivityState;
InitInactivity();
}
private void InitInactivity()
{
//Application.EnableVisualStyles(); //Uncomment this line for new look and feel.
IdleTimer = new Timer();
LeaveIdleMessageFilter limf = new LeaveIdleMessageFilter();
Application.AddMessageFilter(limf);
IdleTimer.Interval = _timeoutDuration;
IdleTimer.Tick += TimeDone;
IdleTimer.Enabled = false;
}
private void Application_Idle(object sender, EventArgs e)
{
if (!IdleTimer.Enabled)
{
IdleTimer.Start();
}
}
private void TimeDone(object sender, EventArgs e)
{
StopTimer();
NotifyInActivity?.Invoke(_inactivityState);
}
public void StopTimer()
{
IdleTimer.Stop();
IdleTimer.Enabled = false;
Application.Idle -= new EventHandler(Application_Idle);
}
public void StartTimer(int minutes)
{
if (IdleTimer != null && !IdleTimer.Enabled)
{
if (minutes > 0)
{
IdleTimer.Interval = (int)TimeSpan.FromMinutes(minutes).TotalMilliseconds;
}
else
{
throw new Exception("Inavlid timeout duration for Activity Logger");
}
IdleTimer.Enabled = true;
IdleTimer.Start();
Application.Idle += new EventHandler(Application_Idle);
}
}
}
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
public class LeaveIdleMessageFilter : IMessageFilter
{
const int WM_NCLBUTTONDOWN = 0x00A1;
const int WM_NCLBUTTONUP = 0x00A2;
const int WM_NCRBUTTONDOWN = 0x00A4;
const int WM_NCRBUTTONUP = 0x00A5;
const int WM_NCMBUTTONDOWN = 0x00A7;
const int WM_NCMBUTTONUP = 0x00A8;
const int WM_NCXBUTTONDOWN = 0x00AB;
const int WM_NCXBUTTONUP = 0x00AC;
const int WM_KEYDOWN = 0x0100;
const int WM_KEYUP = 0x0101;
const int WM_MOUSEMOVE = 0x0200;
const int WM_LBUTTONDOWN = 0x0201;
const int WM_LBUTTONUP = 0x0202;
const int WM_RBUTTONDOWN = 0x0204;
const int WM_RBUTTONUP = 0x0205;
const int WM_MBUTTONDOWN = 0x0207;
const int WM_MBUTTONUP = 0x0208;
const int WM_XBUTTONDOWN = 0x020B;
const int WM_XBUTTONUP = 0x020C;
// The Messages array must be sorted due to use of Array.BinarySearch
static int[] Messages = new int[] {WM_NCLBUTTONDOWN,
WM_NCLBUTTONUP, WM_NCRBUTTONDOWN, WM_NCRBUTTONUP, WM_NCMBUTTONDOWN,
WM_NCMBUTTONUP, WM_NCXBUTTONDOWN, WM_NCXBUTTONUP, WM_KEYDOWN, WM_KEYUP,
WM_LBUTTONDOWN, WM_LBUTTONUP, WM_RBUTTONDOWN, WM_RBUTTONUP,
WM_MBUTTONDOWN, WM_MBUTTONUP, WM_XBUTTONDOWN, WM_XBUTTONUP};
public bool PreFilterMessage(ref Message m)
{
if (m.Msg == WM_MOUSEMOVE) // mouse move is high volume
return false;
if (!UIInactivity.IdleTimer.Enabled) // idling?
return false; // No
if (Array.BinarySearch(Messages, m.Msg) >= 0)
UIInactivity.IdleTimer.Stop();
return false;
}
}
Somewhere in the application a form opens a warning messagebox as follows when trying to save changes,
if (DialogResult.Yes == MessageBox.Show(m_ResMngr.GetString("ChanRangeWarn"),m_ResMngr.GetString("Warning"),MessageBoxButtons.YesNo,MessageBoxIcon.Warning))
{
//Something
}
When this MessageBox is open, its not checking for inactivity. Please help.
I have an inactivity timer that keeps checking for inactivity throughout the app. Problem is, when message box is open, the timer is not ticking and checking for inactivity as long as the MessageBox stays open. When it is closed, it works fine.
The code used here is taken from:
How to detect a Winforms app has been idle for certain amount of time
UIInactivity.cs (Track inactivity)
public class UIInactivity
{
public static Timer IdleTimer;
private readonly int _timeoutDuration;
private InactivityState _inactivityState;
public event Action<InactivityState> NotifyInActivity;
public UIInactivity(int timeoutDurationInMinutes, InactivityState inactivityState)
{
_timeoutDuration = (int)TimeSpan.FromMinutes(timeoutDurationInMinutes).TotalMilliseconds;
_inactivityState = inactivityState;
InitInactivity();
}
private void InitInactivity()
{
//Application.EnableVisualStyles(); //Uncomment this line for new look and feel.
IdleTimer = new Timer();
LeaveIdleMessageFilter limf = new LeaveIdleMessageFilter();
Application.AddMessageFilter(limf);
IdleTimer.Interval = _timeoutDuration;
IdleTimer.Tick += TimeDone;
IdleTimer.Enabled = false;
}
private void Application_Idle(object sender, EventArgs e)
{
if (!IdleTimer.Enabled)
{
IdleTimer.Start();
}
}
private void TimeDone(object sender, EventArgs e)
{
StopTimer();
NotifyInActivity?.Invoke(_inactivityState);
}
public void StopTimer()
{
IdleTimer.Stop();
IdleTimer.Enabled = false;
Application.Idle -= new EventHandler(Application_Idle);
}
public void StartTimer(int minutes)
{
if (IdleTimer != null && !IdleTimer.Enabled)
{
if (minutes > 0)
{
IdleTimer.Interval = (int)TimeSpan.FromMinutes(minutes).TotalMilliseconds;
}
else
{
throw new Exception("Inavlid timeout duration for Activity Logger");
}
IdleTimer.Enabled = true;
IdleTimer.Start();
Application.Idle += new EventHandler(Application_Idle);
}
}
}
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
public class LeaveIdleMessageFilter : IMessageFilter
{
const int WM_NCLBUTTONDOWN = 0x00A1;
const int WM_NCLBUTTONUP = 0x00A2;
const int WM_NCRBUTTONDOWN = 0x00A4;
const int WM_NCRBUTTONUP = 0x00A5;
const int WM_NCMBUTTONDOWN = 0x00A7;
const int WM_NCMBUTTONUP = 0x00A8;
const int WM_NCXBUTTONDOWN = 0x00AB;
const int WM_NCXBUTTONUP = 0x00AC;
const int WM_KEYDOWN = 0x0100;
const int WM_KEYUP = 0x0101;
const int WM_MOUSEMOVE = 0x0200;
const int WM_LBUTTONDOWN = 0x0201;
const int WM_LBUTTONUP = 0x0202;
const int WM_RBUTTONDOWN = 0x0204;
const int WM_RBUTTONUP = 0x0205;
const int WM_MBUTTONDOWN = 0x0207;
const int WM_MBUTTONUP = 0x0208;
const int WM_XBUTTONDOWN = 0x020B;
const int WM_XBUTTONUP = 0x020C;
// The Messages array must be sorted due to use of Array.BinarySearch
static int[] Messages = new int[] {WM_NCLBUTTONDOWN,
WM_NCLBUTTONUP, WM_NCRBUTTONDOWN, WM_NCRBUTTONUP, WM_NCMBUTTONDOWN,
WM_NCMBUTTONUP, WM_NCXBUTTONDOWN, WM_NCXBUTTONUP, WM_KEYDOWN, WM_KEYUP,
WM_LBUTTONDOWN, WM_LBUTTONUP, WM_RBUTTONDOWN, WM_RBUTTONUP,
WM_MBUTTONDOWN, WM_MBUTTONUP, WM_XBUTTONDOWN, WM_XBUTTONUP};
public bool PreFilterMessage(ref Message m)
{
if (m.Msg == WM_MOUSEMOVE) // mouse move is high volume
return false;
if (!UIInactivity.IdleTimer.Enabled) // idling?
return false; // No
if (Array.BinarySearch(Messages, m.Msg) >= 0)
UIInactivity.IdleTimer.Stop();
return false;
}
}
Somewhere in the application a form opens a warning messagebox as follows when trying to save changes,
if (DialogResult.Yes == MessageBox.Show(m_ResMngr.GetString("ChanRangeWarn"),m_ResMngr.GetString("Warning"),MessageBoxButtons.YesNo,MessageBoxIcon.Warning))
{
//Something
}
When this MessageBox is open, its not checking for inactivity. Please help.
Share Improve this question edited 4 hours ago Jimi 32.2k8 gold badges49 silver badges76 bronze badges asked 10 hours ago nikhilnikhil 1,7463 gold badges26 silver badges62 bronze badges 5 |1 Answer
Reset to default 0Jimi is right about Hans being right. So, the way I see it we need to put together four elements to solve this.
- A low-level hook that we can run during calls to
MessageBox.Show(...)
. - An
IDisposable
to wrap the hook with, that does reference counting and disposes the hook when it leaves theusing
block. - A means so that calling any overload of
MessageBox.Show(...)
within in our scope calls our own method. - A suitable
WatchdogTimer
that can be "kicked" (i.e. start-or-restart) when activity is sensed.
And while we're at it, we can optimize by putting messages in a HashSet<WindowsMessage>
for rapid detection.
IDisposable Low-Level Hook
Let's knock out the first two requirements using P\Invoke
along with the NuGet
package shown (or something like it).
// <PackageReference Include="IVSoftware.Portable.Disposable" Version="1.2.0" />
static DisposableHost DHostHook
{
get
{
if (_dhostHook is null)
{
_dhostHook = new DisposableHost();
_dhostHook.BeginUsing += (sender, e) =>
{
_hookID = SetWindowsHookEx(
WH_GETMESSAGE,
_hookProc,
IntPtr.Zero,
GetCurrentThreadId());
};
_dhostHook.FinalDispose += (sender, e) =>
{
UnhookWindowsHookEx(_hookID);
};
}
return _dhostHook;
}
}
static DisposableHost? _dhostHook = default;
static IntPtr _hookID = IntPtr.Zero;
private static HookProc _hookProc = null!;
MessageBox Forwarder
Next, make a static class at local (or app) scope that behaves (in a sense) like an "impossible" extension for the static System.Windows.Forms.MessageBox
class. This means it's "business as usual" when it come to invoking message boxes.
{
.
.
.
// MessageBox wrapper static class nested in MainForm
private static class MessageBox
{
private static readonly Dictionary<int, MethodInfo> _showMethodLookup;
static MessageBox()
{
_showMethodLookup = typeof(System.Windows.Forms.MessageBox)
.GetMethods(BindingFlags.Static | BindingFlags.Public)
.Where(_ => _.Name == "Show")
.ToDictionary(
_ => _.GetParameters()
.Select(p => p.ParameterType)
.Aggregate(17, (hash, type) => hash * 31 + (type?.GetHashCode() ?? 0)),
_ => _
);
}
public static DialogResult Show(params object[] args)
{
// Increment the ref count prior to calling native MessageBox
using (DHostHook.GetToken())
{
int argHash = args
.Select(_ => _?.GetType() ?? typeof(object))
.Aggregate(17, (hash, type) => hash * 31 + (type?.GetHashCode() ?? 0));
if (_showMethodLookup.TryGetValue(argHash, out var bestMatch) && bestMatch is not null)
{
return bestMatch.Invoke(null, args) is DialogResult dialogResult
? dialogResult
: DialogResult.None;
}
return DialogResult.None;
}
}
}
.
.
.
}
WatchdogTimer
Meet requirement #4 using the NuGet
package shown (or something like it).
We'll monitor its status in the Title Bar of the main window as either "Idle" or "Running".
// <PackageReference Include = "IVSoftware.Portable.WatchdogTimer" Version="1.2.1" />
public WatchdogTimer InactivityWatchdog
{
get
{
if (_InactivityWatchdog is null)
{
_InactivityWatchdog = new WatchdogTimer
{
Interval = TimeSpan.FromSeconds(2),
};
_InactivityWatchdog.RanToCompletion += (sender, e) =>
{
Text = "Idle";
};
}
return _InactivityWatchdog;
}
}
WatchdogTimer? _InactivityWatchdog = default;
Minimal MainForm Example
public MainForm()
{
InitializeComponent();
Application.AddMessageFilter(this);
Disposed += (sender, e) => Application.RemoveMessageFilter(this);
_hookProc = HookCallback;
// Button for test
buttonMsg.Click += (sender, e) =>
{
MessageBox.Show("✨ Testing the Hook!");
};
}
public bool PreFilterMessage(ref Message m)
{
CheckForActivity((WindowsMessage)m.Msg);
return false;
}
// Threadsafe Text Setter
public new string Text
{
get => _threadsafeText;
set
{
lock (_lock)
{
if (!Equals(_threadsafeText, value))
{
lock (_lock)
{
_threadsafeText = value;
}
if (InvokeRequired) BeginInvoke(() => base.Text = _threadsafeText);
else base.Text = _threadsafeText;
}
}
}
}
string _threadsafeText = string.Empty;
object _lock = new object();
private void CheckForActivity(WindowsMessage wm_msg)
{
switch (wm_msg)
{
case WindowsMessage.WM_MOUSEMOVE: // Prioritize
case WindowsMessage when _rapidMessageLookup.Contains(wm_msg):
InactivityWatchdog.StartOrRestart();
if(Text != "Running")
{
BeginInvoke(() => Text = "Running");
}
break;
}
}
readonly HashSet<WindowsMessage> _rapidMessageLookup =
new (Enum.GetValues<WindowsMessage>());
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
MSG msg = Marshal.PtrToStructure<MSG>(lParam);
Debug.WriteLine($"Msg: {(WindowsMessage)msg.message} ({msg.message:X}), hWnd: {msg.hwnd}");
Debug.WriteLine($"{(WindowsMessage)msg.message} {_rapidMessageLookup.Contains((WindowsMessage)msg.message)}");
CheckForActivity((WindowsMessage)msg.message);
}
return CallNextHookEx(_hookID, nCode, wParam, lParam);
}
Application.Idle
and/orLeaveIdleMessageFilter
relevant for this problem? IsTimeDone
not called, even when the timer is enabled and elapsed? I'm not really sure I understand how the example is supposed to work. – JonasH Commented 8 hours ago