2009-06-04 11 views
6

Chcę pokazać użytkownikowi, ile sekund minęło od wystąpienia jakiegoś zdarzenia. Koncepcyjnie, moim zdaniem model ma właściwości tak:Jak aktualizować wiązanie WPF co sekundę?

public DateTime OccurredAtUtc { get; set; } 

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

Jeśli wiążę właściwość TextBlock.Text do SecondsSinceOccurrence, pojawia się wartość, ale to jest statyczny. Przekazywanie czasu nie odzwierciedla rosnącego wieku tego wydarzenia.

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

mógłbym stworzyć czasomierz moim zdaniem model, który odpala PropertyChanged co drugi, ale jest prawdopodobne, aby być wiele takich elementów w interfejsie użytkownika (jego szablon dla pozycji w ItemsControl) i nie chcę stworzyć tak wiele timerów.

Moja wiedza na temat animacji za pomocą scenorysów nie jest świetna. Czy struktura animacji WPF może w tym przypadku pomóc?

Odpowiedz

4

Posiadanie licznika do okresowego wyzwalania zdarzenia PropertyChanged jest jednym ze sposobów na pokonanie problemu. Ale jeśli masz dużo elementów w ContentControl, a właściwość, którą chcesz zaktualizować, jest w numerze ItemTemplate tego ContentControl, oznacza to niepotrzebne tworzenie ponad 100 zegarów i jednoczesne podnoszenie ich wszystkich razem o PropertyChanged. Jednak zachowanie to będzie nadal tworzone dla każdego elementu, jeśli jest używane w postaci ItemsControl, takiej jak ListBox.

Z tego powodu stworzyłem to zachowanie, które zostanie utworzone tylko raz dla każdego wiązania w szablonie. To także czysto MVVM.

Wykorzystanie

<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> 

Zależności

Zauważ, że http://schemas.microsoft.com/expression/2010/interactivity nazw jest dostępna w ramach pakietu Nuget zwanego System.Windows.Interactivity.WPF. Zostanie również automatycznie dodany, jeśli otworzysz projekt w mieszance.

skopiować i wkleić kod

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

To wygląda fantastycznie. Dzięki. –

+0

Wielkie dzięki! Szukałem czystej metody rozwiązania tego problemu. Nie wiem, czy używam starej wersji C# lub, ale musiałem zmienić 'if (weakCallback.TryGetTarget (out callback TimerCallback))' na 'Callback TimerCallback; jeśli (weakCallback.TryGetTarget (out callback)) ', aby to działało. – monoceres

+0

@monoceres Oh yeah! to jest C# 7. Po wypróbowaniu tego nie możesz bez niego żyć – fjch1997

8

Można utworzyć pojedynczy statycznie DispatcherTimer dla modelu widoku, a następnie wszystkie wystąpienia tego modelu widoku będą nasłuchiwały zdarzenia 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

ja ufałem to byłoby możliwe, aby element (lub wiązanie), które mogłyby pociągnąć to okresowo, a nie o źródle danych informacją. Czy można utworzyć powiązanie niestandardowe i dodać właściwość 'RefreshPeriod'? Jeśli tak, wówczas można również połączyć instancje DispatcherTimer. –

+0

Rzeczywiście, jestem również zainteresowany robieniem tego wyłącznie z XAML. Też nie mam wystarczającej wiedzy na temat animacji atm. – buckley