如何使WPF绑定每秒更新一次?

9

我希望向用户展示自某事件发生以来经过了多少秒。从概念上讲,我的视图模型具有以下属性:

public DateTime OccurredAtUtc { get; set; }

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

如果我将属性绑定到,则该值会显示出来,但它是静态的。时间的流逝不反映此事件的年龄增长。
<!-- static value won't update as time passes -->
<TextBlock Text="{Binding SecondsSinceOccurrence}" />

我可以在我的视图模型中创建一个定时器,每秒触发 PropertyChanged 事件,但UI中可能有许多这样的元素(它是ItemsControl中项的模板),我不想创建那么多定时器。

我对使用 storyboards 进行动画的知识不是很多。WPF 动画框架能在这种情况下帮助吗?

2个回答

15

一种纯粹的MVVM解决方案

用法

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

依赖项

请注意,http://schemas.microsoft.com/expression/2010/interactivity 命名空间可在名为 System.Windows.Interactivity.WPF 的 NuGet 包中使用。如果在 Blend 中打开项目,则该命名空间也将自动添加。

复制并粘贴代码

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

这看起来很棒。谢谢。 - Drew Noakes
非常感谢!一直在寻找解决这个问题的简洁方法。不知道我是否使用了旧版的C#,但我不得不将if (weakCallback.TryGetTarget(out TimerCallback callback))更改为TimerCallback callback; if (weakCallback.TryGetTarget(out callback))才能使其正常工作。 - monoceres
1
@monoceres 哦,是的!那就是C# 7。一旦你尝试过它,就再也离不开它了。 - fjch1997
太棒了。要能够与多重绑定一起使用,只需将BindingOperation部分包装到一个方法中(例如GetBinding()),其中包含片段return BindingOperations.GetBindingExpression(AssociatedObject,Property)??(BindingExpressionBase)BindingOperations.GetMultiBindingExpression(AssociatedObject,Property); - ChriPf

11

您可以为您的视图模型静态地创建一个单独的 DispatcherTimer,然后使该视图模型的所有实例都侦听 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
我希望能够有一个元素(或绑定),可以定期地拉取数据,而不是让底层数据源通知。是否可以创建自定义绑定并添加“RefreshPeriod”属性?如果可以的话,那么DispatcherTimer实例也可以被池化。 - Drew Noakes
事实上,我也对纯粹使用XAML进行操作很感兴趣。目前我对动画的知识也不够丰富。 - buckley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接