2011-02-01 11 views
5

Ho un'applicazione kiosk MVVM che devo riavviare quando è stata inattiva per un determinato periodo di tempo. Sto usando Prism e Unity per facilitare il pattern MVVM. Ho riavviato e so anche come gestire il timer. Quello che voglio sapere è come sapere quando si è verificata l'attività, cioè qualsiasi evento del mouse. L'unico modo in cui so come farlo è iscrivermi agli eventi del mouse di anteprima della finestra principale. Che rompe MVVM pensato, non è vero?Come determinare l'inattività in un'applicazione MVVM?

Ho pensato di esporre la mia finestra come un'interfaccia che espone gli eventi alla mia domanda, ma ciò richiederebbe che la finestra di implementare tale interfaccia che sembra anche a rompere MVVM.

risposta

4

Un'altra opzione è utilizzare il metodo API di Windows GetLastInputInfo.

Alcuni cavets

  • sto supponendo di Windows perché è WPF
  • Verificare se il chiosco supporta GetLastInputInfo
  • Io non so niente di MVVM. Questo metodo utilizza una tecnica che è indipendente dall'interfaccia utente, quindi penserei che potrebbe funzionare per voi.

L'utilizzo è semplice. Chiama UserIdleMonitor.RegisterForNotification. Si passa in un metodo di notifica e un TimeSpan. Se l'attività dell'utente si verifica e quindi cessa per il periodo specificato, viene chiamato il metodo di notifica. Devi registrarti di nuovo per ricevere un'altra notifica e puoi annullare la registrazione in qualsiasi momento. Se non ci sono attività per 49,7 giorni (più idlePeriod), verrà chiamato il metodo di notifica.

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); 
} 

Aggiornato

Risolto il problema per cui se si è tentato di registrare nuovamente dal metodo di notifica si potrebbe deadlock.

Calcolo matematico senza segno e aggiunta deselezionato.

Leggera ottimizzazione nel gestore del timer per assegnare le notifiche solo se necessario.

Ha commentato l'output di debug.

modificato per utilizzare DispatchTimer.

Aggiunta la possibilità di annullare la registrazione.

Aggiunto controllo del thread nei metodi pubblici in quanto non è più thread-safe.

+0

@Tergiver, ho intenzione di provare questo. 'System.Windows.Threading.DispatcherTimer' torna sullo stesso thread del Dispatcher per il quale è stato creato. In questo modo posso evitare di usare i lucchetti. Basta aggiungere il riferimento WindowsBase per quel bambino. – Jordan

+0

@Jordan: Ho usato System.Timers.Timer in modo che non dipendesse dal WPF, ma questa è una buona idea. – Tergiver

+0

@Tergiver, 'System.Windows.Threading.DispatcherTimer' è abbastanza fondamentale nel framework che è possibile utilizzarlo in WinForms o in un'altra interfaccia utente. – Jordan

1

È possibile utilizzare il comportamento di EventToCommand MVVM Light's Event per collegare l'evento MouseMove/MouseLeftButtonDown a un comando. Questo è normalmente fatto in blend perché è veramente facile.

Ecco qualche esempio di XAML, se non si dispone di mix:

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

Dove i: è un namespace XML per Blend.Interactivity.

0

L'unico modo in cui so come farlo è iscrivermi agli eventi del mouse di anteprima della finestra principale. Che rompe MVVM pensato, non è vero?

Questo dipende molto da come lo si fa.

Si potrebbe facilmente scrivere un comportamento o una proprietà associata che si aggancia a questo evento e utilizzarlo per attivare un ICommand nel ViewModel. In questo modo, stai praticamente spingendo un evento "Qualcosa è successo" fino alla VM, dove puoi gestirlo completamente nella tua logica aziendale.

1

Questa non è una risposta ufficiale, ma ecco la mia versione di UserIdleMonitor per chiunque sia interessato:

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 
} 
+0

Ah, vedo che non mi piace usare un timer per ogni istanza di qualcosa (spreco di risorse), quindi utilizzo automaticamente un tipo di schema di pianificazione. Ora, in questo caso, non avrai davvero bisogno di più di una di queste cose, quindi l'approccio di pianificazione è eccessivo. La tua versione è perfettamente a posto. – Tergiver

+0

Due osservazioni banali: non è davvero necessario 1/10 di secondo polling per rilevare l'inattività dell'utente. Se memorizzi '_startTime' come un' DateTime' puoi calcolare 'aliveFor' con' DateTime.Now - _startTime' che restituisce un 'TimeSpan'. – Tergiver