Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to have a WPF binding update every second?

I want to show the user how many seconds have passed since some event occurs. Conceptually, my view model has properties like this:

public DateTime OccurredAtUtc { get; set; }

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

If I bind a TextBlock.Text property to SecondsSinceOccurrence, the value appears but it is static. The passing of time does not reflect the increasing age of this event.

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

I could create a timer in my view model that fires PropertyChanged every second, but there are likely to be many such elements in the UI (its a template for items in an ItemsControl) and I don't want to create that many timers.

My knowledge of animation with storyboards isn't great. Can the WPF animation framework help in this case?

like image 641
Drew Noakes Avatar asked Jun 04 '09 09:06

Drew Noakes


2 Answers

A purely MVVM sulution

Usage

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

Dependencies

Note that http://schemas.microsoft.com/expression/2010/interactivity namespace is available under a NuGet package called System.Windows.Interactivity.WPF. It will also be automatically added if you open the project in blend.

Copy and paste 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();
        }
    }
}
like image 122
fjch1997 Avatar answered Nov 15 '22 23:11

fjch1997


You could create a single DispatcherTimer statically for your view model, and then have all instances of that view model listen to the Tick event.

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");
    }
}
like image 41
Kent Boogaart Avatar answered Nov 15 '22 23:11

Kent Boogaart