5

I have an MVVM kiosk application that I need to restart when it has been inactive for a set amount of time. I'm using Prism and Unity to facilitate the MVVM pattern. I've got the restarting down and I even know how to handle the timer. What I want to know is how to know when activity, that is any mouse event, has taken occurred. The only way I know how to do that is by subscribing to the preview mouse events of the main window. That breaks MVVM thought, doesn't it?

I've thought about exposing my window as an interface that exposes those events to my application, but that would require that the window implement that interface which also seems to break MVVM.

Jordan
  • 9,642
  • 10
  • 71
  • 141

4 Answers4

4

Another option is to use the Windows API method GetLastInputInfo.

Some cavets

  • I'm assuming Windows because it's WPF
  • Check if your kiosk supports GetLastInputInfo
  • I don't know anything about MVVM. This method uses a technique that is UI agnostic, so I would think it would work for you.

Usage is simple. Call UserIdleMonitor.RegisterForNotification. You pass in a notification method and a TimeSpan. If user activity occurs and then ceases for the period specified, the notification method is called. You must re-register to get another notification, and can Unregister at any time. If there is no activity for 49.7 days (plus the idlePeriod), the notification method will be called.

public static class UserIdleMonitor
{
    static UserIdleMonitor()
    {
        registrations = new List<Registration>();
        timer = new DispatcherTimer(TimeSpan.FromSeconds(1.0), DispatcherPriority.Normal, TimerCallback, Dispatcher.CurrentDispatcher);
    }

    public static TimeSpan IdleCheckInterval
    {
        get { return timer.Interval; }
        set
        {
            if (Dispatcher.CurrentDispatcher != timer.Dispatcher)
                throw new InvalidOperationException("UserIdleMonitor can only be used from one thread.");
            timer.Interval = value;
        }
    }

    public sealed class Registration
    {
        public Action NotifyMethod { get; private set; }
        public TimeSpan IdlePeriod { get; private set; }
        internal uint RegisteredTime { get; private set; }

        internal Registration(Action notifyMethod, TimeSpan idlePeriod)
        {
            NotifyMethod = notifyMethod;
            IdlePeriod = idlePeriod;
            RegisteredTime = (uint)Environment.TickCount;
        }
    }

    public static Registration RegisterForNotification(Action notifyMethod, TimeSpan idlePeriod)
    {
        if (notifyMethod == null)
            throw new ArgumentNullException("notifyMethod");
        if (Dispatcher.CurrentDispatcher != timer.Dispatcher)
            throw new InvalidOperationException("UserIdleMonitor can only be used from one thread.");

        Registration registration = new Registration(notifyMethod, idlePeriod);

        registrations.Add(registration);
        if (registrations.Count == 1)
            timer.Start();

        return registration;
    }

    public static void Unregister(Registration registration)
    {
        if (registration == null)
            throw new ArgumentNullException("registration");
        if (Dispatcher.CurrentDispatcher != timer.Dispatcher)
            throw new InvalidOperationException("UserIdleMonitor can only be used from one thread.");

        int index = registrations.IndexOf(registration);
        if (index >= 0)
        {
            registrations.RemoveAt(index);
            if (registrations.Count == 0)
                timer.Stop();
        }
    }

    private static void TimerCallback(object sender, EventArgs e)
    {
        LASTINPUTINFO lii = new LASTINPUTINFO();
        lii.cbSize = Marshal.SizeOf(typeof(LASTINPUTINFO));
        if (GetLastInputInfo(out lii))
        {
            TimeSpan idleFor = TimeSpan.FromMilliseconds((long)unchecked((uint)Environment.TickCount - lii.dwTime));
            //Trace.WriteLine(String.Format("Idle for {0}", idleFor));

            for (int n = 0; n < registrations.Count; )
            {
                Registration registration = registrations[n];

                TimeSpan registeredFor = TimeSpan.FromMilliseconds((long)unchecked((uint)Environment.TickCount - registration.RegisteredTime));
                if (registeredFor >= idleFor && idleFor >= registration.IdlePeriod)
                {
                    registrations.RemoveAt(n);
                    registration.NotifyMethod();
                }
                else n++;
            }

            if (registrations.Count == 0)
                timer.Stop();
        }
    }

    private static List<Registration> registrations;
    private static DispatcherTimer timer;

    private struct LASTINPUTINFO
    {
        public int cbSize;
        public uint dwTime;
    }

    [DllImport("User32.dll")]
    private extern static bool GetLastInputInfo(out LASTINPUTINFO plii);
}

Updated

Fixed issue where if you tried to re-register from the notification method you could deadlock.

Fixed unsigned math and added unchecked.

Slight optimization in timer handler to allocate notifications only as needed.

Commented out the debugging output.

Altered to use DispatchTimer.

Added ability to Unregister.

Added thread checks in public methods as this is no longer thread-safe.

Tergiver
  • 14,171
  • 3
  • 41
  • 68
  • @Tergiver, I'm going to try this. `System.Windows.Threading.DispatcherTimer` posts back on the same thread as the Dispatcher for which it is created. This way I can avoid using locks. Just need to add the WindowsBase reference for that baby. – Jordan Feb 01 '11 at 20:48
  • @Jordan: I used System.Timers.Timer so that it wasn't WPF dependent, but that's a good idea. – Tergiver Feb 01 '11 at 20:51
  • @Tergiver, `System.Windows.Threading.DispatcherTimer` is fundamental enough in the framework that you can use it in WinForms or some other user interface. – Jordan Feb 02 '11 at 13:43
  • @Jordan: I agree that using DispatcherTimer is a good choice for your WPF app, but disagree with the statement that it is a fundamental part of "the framework". WindowsBase is WPF. If you're targetting .NET 3.0 or better, than it'll be there. If you have to target .NET 2.0, use something else. – Tergiver Feb 02 '11 at 13:56
  • @Jordan: I didn't know you can use it with WinForms. You can even use it in a console application though you have to execute Dispatcher.Run on some thread to do so. That's actually pretty cool. – Tergiver Feb 02 '11 at 14:16
  • @Jordan: I altered it to use DispatcherTimer. Though you have probably already done so, it might be useful to future visitors. I also added the ability to Unregister in case you need to. – Tergiver Feb 02 '11 at 14:59
  • @Tergiver, Your solution worked for me. I made my version a little less static-statemachine-ish by making it an instance class and I'm using a single event, so you effectively have one instance per registration. Also, I don't think you have to call `Dispatcher.Run`. As soon as you access `Dispatcher.CurrentDispatcher`, it creates a `Dispatcher` for your thread. I may be wrong, but I believe it's pretty self contained. Thanks. :) – Jordan Feb 02 '11 at 16:33
  • @Jordan: In a console app you do have to call `Dispatcher.Run` because there is no dispatch loop (message pump) like in a WinForms or WPF app. – Tergiver Feb 02 '11 at 16:53
  • @Jordan: I'd like to see your version. – Tergiver Feb 02 '11 at 16:54
  • @Tergiver, I added it as an answer below. Its very simple. You just create a new instance, attach an event to `Timeout`, and call `Start`. It works well. – Jordan Feb 02 '11 at 19:14
1

You could maybe use MVVM Light's EventToCommand behaviour to link the MouseMove/MouseLeftButtonDown event to a command. This is normally done in blend because it's really easy.

Here's some example xaml if you don't have blend:

<Grid>
  <i:Interaction.Triggers>
    <i:EventTrigger EventName="MouseLeftButtonDown">
      <GalaSoft_MvvmLight_Command:EventToCommand Command="{Binding theCommand} />
    </i:EventTrigger>
  </i:Interaction.Triggers>
</Grid>

Where i: is a xml namespace for Blend.Interactivity.

Dave Arkell
  • 3,920
  • 2
  • 22
  • 27
1

This is not an official answer, but here is my version of UserIdleMonitor for anyone who is interested:

public class UserIdleMonitor
{
    private DispatcherTimer _timer;
    private TimeSpan _timeout;
    private DateTime _startTime;

    public event EventHandler Timeout;

    public UserIdleMonitor(TimeSpan a_timeout)
    {
        _timeout = a_timeout;

        _timer = new DispatcherTimer(DispatcherPriority.Normal, Dispatcher.CurrentDispatcher);
        _timer.Interval = TimeSpan.FromMilliseconds(100);
        _timer.Tick += new EventHandler(timer_Tick);
    }

    public void Start()
    {
        _startTime = new DateTime();
        _timer.Start();
    }

    public void Stop()
    {
        _timer.Stop();
    }

    private void timer_Tick(object sender, EventArgs e)
    {
        LASTINPUTINFO lii = new LASTINPUTINFO();
        lii.cbSize = Marshal.SizeOf(typeof(LASTINPUTINFO));
        if (GetLastInputInfo(out lii))
        {
            TimeSpan idleFor = TimeSpan.FromMilliseconds((long)unchecked((uint)Environment.TickCount - lii.dwTime));

            TimeSpan aliveFor = TimeSpan.FromMilliseconds((long)unchecked((uint)Environment.TickCount - _startTime.Millisecond));
            Debug.WriteLine(String.Format("aliveFor = {0}, idleFor = {1}, _timeout = {2}", aliveFor, idleFor, _timeout));
            if (aliveFor >= idleFor && idleFor >= _timeout)
            {
                _timer.Stop();
                if (Timeout != null)
                    Timeout.Invoke(this, EventArgs.Empty);
            }
        }
    }

    #region Win32 Stuff

    private struct LASTINPUTINFO
    {
        public int cbSize;
        public uint dwTime;
    }

    [DllImport("User32.dll")]
    private extern static bool GetLastInputInfo(out LASTINPUTINFO plii);

    #endregion 
}
Jordan
  • 9,642
  • 10
  • 71
  • 141
  • Ah, see I dislike using one timer for every instance of something (waste of resources), so I automatically use some kind of scheduling pattern. Now in this case, you're not really going to need more than one of these things, so the scheduling approach is overkill. Your version is perfectly fine. – Tergiver Feb 02 '11 at 20:31
  • Two trivial observations: You really don't need 1/10th of a second polling to detect user inactivity. If you store `_startTime` as a `DateTime` you can calculate `aliveFor` with `DateTime.Now - _startTime` which returns a `TimeSpan`. – Tergiver Feb 02 '11 at 20:33
0

The only way I know how to do that is by subscribing to the preview mouse events of the main window. That breaks MVVM thought, doesn't it?

That really depends on how you do it.

You could pretty easily write a Behavior or an Attached Property that you hook into this event and use it to trigger an ICommand in your ViewModel. This way, you're basically pushing a "Something happened" event down to the VM, where you can handle this completely in your business logic.

Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373