最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c# - Timer is not ticking when message box is open - Stack Overflow

programmeradmin4浏览0评论

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 System.Windows.Forms.Timer is a wrong timer to use, try either of these. – Sinatr Commented 10 hours ago
  • Is the Application.Idle and/or LeaveIdleMessageFilter relevant for this problem? Is TimeDone 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
  • 3 It is not the timer that's the problem, Application.AddMessageFilter() is the troublemaker. You can count on getting PreFilterMessage callbacks as long as the Winforms dispatcher is generating events. But MessageBox is a native dialog that doesn't know beans about Winforms. No simple fix for that, you'd have to use a windows hook to see input while the message box is active. Or more practically, do this. – Hans Passant Commented 7 hours ago
  • If it's not async these days; it's "bad"; even if it's important ... it just stays "on top"; without shutting everything else down. "Graceful degradation". – Gerry Schmitz Commented 6 hours ago
  • 2 When you take code from a StackOverflow post, you need to refer to it. I've edited your question to reflect that -- This is a quite convoluted way of detecting idle time. See the code in here: GetLastInputInfo not returning dwTime, it works fine when a modal Dialog is presented -- BTW, Hans is right, and the Timer of course is still ticking (or, it could be) – Jimi Commented 4 hours ago
Add a comment  | 

1 Answer 1

Reset to default 0

Jimi is right about Hans being right. So, the way I see it we need to put together four elements to solve this.

  1. A low-level hook that we can run during calls to MessageBox.Show(...).
  2. An IDisposable to wrap the hook with, that does reference counting and disposes the hook when it leaves the using block.
  3. A means so that calling any overload of MessageBox.Show(...) within in our scope calls our own method.
  4. 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);
}
发布评论

评论列表(0)

  1. 暂无评论