2009-06-04 4 views
6

Je veux montrer à l'utilisateur combien de secondes se sont écoulées depuis qu'un événement se produit. Conceptuellement, mon modèle de vue a des propriétés comme celui-ci:Comment avoir une mise à jour de liaison WPF chaque seconde?

public DateTime OccurredAtUtc { get; set; } 

public int SecondsSinceOccurrence 
{ 
    get { return (int)(DateTime.UtcNow - OccurredAtUtc).TotalSeconds; } 
} 

Si je lie une propriété TextBlock.Text-SecondsSinceOccurrence, la valeur apparaît mais il est statique. Le passage du temps ne reflète pas l'âge croissant de cet événement.

<!-- static value won't update as time passes --> 
<TextBlock Text="{Binding SecondsSinceOccurrence}" /> 

Je pourrais créer une minuterie dans mon modèle de vue qui se déclenche PropertyChanged chaque seconde, mais il y a probablement beaucoup de ces éléments dans l'interface utilisateur (son modèle pour les éléments dans un ItemsControl) et je ne veux pas pour créer autant de minuteurs.

Ma connaissance de l'animation avec des storyboards n'est pas géniale. Le framework d'animation WPF peut-il aider dans ce cas?

Répondre

4

Avoir une minuterie pour déclencher périodiquement PropertyChanged événement est une façon d'aller. Mais si vous avez beaucoup d'éléments dans un ContentControl et que la propriété que vous voulez mettre à jour se trouve dans le ItemTemplate de ce ContentControl, cela signifie créer inutilement 100+ temporisateurs et les faire lever PropertyChanged en même temps. Toutefois, ce comportement sera toujours créé pour chaque élément lorsqu'il est utilisé dans un ItemsControl comme ListBox.

Pour cette raison, j'ai créé ce comportement qui ne sera créé qu'une fois pour chaque liaison dans votre modèle. C'est aussi purement MVVM.

Utilisation

<Label xmlns:b="clr-namespace:Lloyd.Shared.Behaviors" 
     xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
     Content="{Binding MyContent}" Width="80" Foreground="{Binding MyColor}"> 
    <i:Interaction.Behaviors> 
     <b:PeriodicBindingUpdateBehavior Interval="0:00:01" Property="{x:Static ContentControl.ContentProperty}" Mode="UpdateTarget" /> 
     <b:PeriodicBindingUpdateBehavior Interval="0:00:01" Property="{x:Static Control.ForegroundProperty}" Mode="UpdateTarget" /> 
    </i:Interaction.Behaviors> 
</Label> 

dépendances

Notez que l'espace de noms http://schemas.microsoft.com/expression/2010/interactivity est disponible sous un paquet NuGet appelé System.Windows.Interactivity.WPF. Il sera également ajouté automatiquement si vous ouvrez le projet en fusion.

Copiez et collez le code

using System; 
using System.Threading; 
using System.Threading.Tasks; 
using System.Windows; 
using System.Windows.Data; 
using System.Windows.Interactivity; 

namespace Lloyd.Shared.Behaviors 
{ 
    public class PeriodicBindingUpdateBehavior : Behavior<DependencyObject> 
    { 
     public TimeSpan Interval { get; set; } 
     public DependencyProperty Property { get; set; } 
     public PeriodicBindingUpdateMode Mode { get; set; } = PeriodicBindingUpdateMode.UpdateTarget; 
     private WeakTimer timer; 
     private TimerCallback timerCallback; 
     protected override void OnAttached() 
     { 
      if (Interval == null) throw new ArgumentNullException(nameof(Interval)); 
      if (Property == null) throw new ArgumentNullException(nameof(Property)); 
      //Save a reference to the callback of the timer so this object will keep the timer alive but not vice versa. 
      timerCallback = s => 
      { 
       try 
       { 
        switch (Mode) 
        { 
         case PeriodicBindingUpdateMode.UpdateTarget: 
          Dispatcher.Invoke(() => BindingOperations.GetBindingExpression(AssociatedObject, Property)?.UpdateTarget()); 
          break; 
         case PeriodicBindingUpdateMode.UpdateSource: 
          Dispatcher.Invoke(() => BindingOperations.GetBindingExpression(AssociatedObject, Property)?.UpdateSource()); 
          break; 
        } 
       } 
       catch (TaskCanceledException) { }//This exception will be thrown when application is shutting down. 
      }; 
      timer = new WeakTimer(timerCallback, null, Interval, Interval); 

      base.OnAttached(); 
     } 

     protected override void OnDetaching() 
     { 
      timer.Dispose(); 
      timerCallback = null; 
      base.OnDetaching(); 
     } 
    } 

    public enum PeriodicBindingUpdateMode 
    { 
     UpdateTarget, UpdateSource 
    } 

    /// <summary> 
    /// Wraps up a <see cref="System.Threading.Timer"/> with only a <see cref="WeakReference"/> to the callback so that the timer does not prevent GC from collecting the object that uses this timer. 
    /// Your object must hold a reference to the callback passed into this timer. 
    /// </summary> 
    public class WeakTimer : IDisposable 
    { 
     private Timer timer; 
     private WeakReference<TimerCallback> weakCallback; 
     public WeakTimer(TimerCallback callback) 
     { 
      timer = new Timer(OnTimerCallback); 
      weakCallback = new WeakReference<TimerCallback>(callback); 
     } 

     public WeakTimer(TimerCallback callback, object state, int dueTime, int period) 
     { 
      timer = new Timer(OnTimerCallback, state, dueTime, period); 
      weakCallback = new WeakReference<TimerCallback>(callback); 
     } 

     public WeakTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) 
     { 
      timer = new Timer(OnTimerCallback, state, dueTime, period); 
      weakCallback = new WeakReference<TimerCallback>(callback); 
     } 

     public WeakTimer(TimerCallback callback, object state, uint dueTime, uint period) 
     { 
      timer = new Timer(OnTimerCallback, state, dueTime, period); 
      weakCallback = new WeakReference<TimerCallback>(callback); 
     } 

     public WeakTimer(TimerCallback callback, object state, long dueTime, long period) 
     { 
      timer = new Timer(OnTimerCallback, state, dueTime, period); 
      weakCallback = new WeakReference<TimerCallback>(callback); 
     } 

     private void OnTimerCallback(object state) 
     { 
      if (weakCallback.TryGetTarget(out TimerCallback callback)) 
       callback(state); 
      else 
       timer.Dispose(); 
     } 

     public bool Change(int dueTime, int period) 
     { 
      return timer.Change(dueTime, period); 
     } 
     public bool Change(TimeSpan dueTime, TimeSpan period) 
     { 
      return timer.Change(dueTime, period); 
     } 

     public bool Change(uint dueTime, uint period) 
     { 
      return timer.Change(dueTime, period); 
     } 

     public bool Change(long dueTime, long period) 
     { 
      return timer.Change(dueTime, period); 
     } 

     public bool Dispose(WaitHandle notifyObject) 
     { 
      return timer.Dispose(notifyObject); 
     } 
     public void Dispose() 
     { 
      timer.Dispose(); 
     } 
    } 
} 
+0

Cela semble fantastique. Merci. –

+0

Merci beaucoup! Été à la recherche d'un moyen propre pour résoudre ce problème. Je ne sais pas si j'utilise une ancienne version de C# ou quoi, mais j'ai dû changer 'if (weakCallback.TryGetTarget (out TimerCallback callback))' en callback de TimerCallback; if (weakCallback.TryGetTarget (out callback)) 'pour le faire fonctionner. – monoceres

+0

@monoceres Oh ouais! C'est C# 7. Une fois que vous l'avez essayé, vous ne pouvez pas vivre sans elle – fjch1997

8

Vous pouvez créer un seul DispatcherTimer statiquement pour votre modèle de vue, puis toutes les instances de ce modèle de vue écoutent l'événement Tick.

public class YourViewModel 
{ 
    private static readonly DispatcherTimer _timer; 

    static YourViewModel() 
    { 
     //create and configure timer here to tick every second 
    } 

    public YourViewModel() 
    { 
     _timer.Tick += (s, e) => OnPropertyChanged("SecondsSinceOccurence"); 
    } 
} 
+1

J'espérais qu'il aurait été possible d'avoir un élément (ou la reliure) qui rassemblerait ce périodique, plutôt que d'avoir la notification de la source de données sous-jacentes. Peut-on créer une liaison personnalisée et ajouter une propriété 'RefreshPeriod'? Si tel est le cas, les instances DispatcherTimer peuvent également être regroupées. –

+0

En effet, je suis également intéressé à le faire purement à partir de XAML. Je n'ai pas assez de connaissances sur l'animation atm. – buckley

Questions connexes