ViewModel中计时器的更好解决方案

本文关键字:更好 解决方案 计时器 ViewModel | 更新日期: 2023-09-27 18:29:29

我在ViewModel中有一个用于图形组件的DispatcherTimer,用于定期更新(滚动)。

最近我发现这是一个巨大的资源泄漏,因为每次我导航到图形视图时都会新创建ViewModel,并且DispatcherTimer阻止GC破坏我的ViewModel,因为Tick事件对它有很强的引用。

我用一个围绕DispatcherTimer的包装器解决了这个问题,该包装器使用Codeproject/Daniel Grunwald的FastSmartWeakEvent来避免对VM的强烈引用,并在没有更多侦听器时自行销毁:

public class WeakDispatcherTimer
{
    /// <summary>
    /// the actual timer
    /// </summary>
    private DispatcherTimer _timer;

    public WeakDispatcherTimer(TimeSpan interval, DispatcherPriority priority, EventHandler callback, Dispatcher dispatcher)
    {
        Tick += callback;
        _timer = new DispatcherTimer(interval, priority, Timer_Elapsed, dispatcher);
    }

    public void Start()
    {
        _timer.Start();
    }

    private void Timer_Elapsed(object sender, EventArgs e)
    {
        _tickEvent.Raise(sender, e);
        if (_tickEvent.EventListenerCount == 0) // all listeners have been garbage collected
        {
            // kill the timer once the last listener is gone
            _timer.Stop(); // this un-registers the timer from the dispatcher
            _timer.Tick -= Timer_Elapsed; // this should make it possible to garbage-collect this wrapper
        }
    }

    public event EventHandler Tick
    {
        add { _tickEvent.Add(value); }
        remove { _tickEvent.Remove(value); }
    }
    FastSmartWeakEvent<EventHandler> _tickEvent = new FastSmartWeakEvent<EventHandler>(); 
}

这就是我使用它的方式。在没有"弱"之前,这是完全一样的:

internal class MyViewModel : ViewModelBase
{
    public MyViewModel()
    {
        if (!IsInDesignMode)
        {
            WeakDispatcherTimer repaintTimer = new WeakDispatcherTimer(TimeSpan.FromMilliseconds(300), DispatcherPriority.Render, RepaintTimer_Elapsed, Application.Current.Dispatcher);
            repaintTimer.Start();
        }
    }
    private void RepaintTimer_Elapsed(object sender, EventArgs e)
    {
        ...
    }
}

它似乎很有效,但这真的是最好/最简单的解决方案吗?还是我遗漏了什么?

我在谷歌上什么都没发现,简直不敢相信我是唯一一个在ViewModel中使用计时器更新内容并发生资源泄漏的人。。。这感觉不对!

更新

由于图形组件(SciChart)提供了一种附加修饰符(Behaviours)的方法,我编写了一个SciChartRollingModifier,这基本上就是AlexSeleznyov在回答中建议的。有了行为,这也是可能的,但这更简单!

如果其他人需要滚动SciChart LineGraph,下面是如何做到的:

public class SciChartRollingModifier : ChartModifierBase
{
    DispatcherTimer _renderTimer;
    private DateTime _oldNewestPoint;

    public SciChartRollingModifier()
    {
        _renderTimer = new DispatcherTimer(RenderInterval, DispatcherPriority.Render, RenderTimer_Elapsed, Application.Current.Dispatcher);
    }


    /// <summary>
    /// Updates the render interval one it's set by the property (e.g. with a binding or in XAML)
    /// </summary>
    private static void RenderInterval_PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
    {
        SciChartRollingModifier modifier = dependencyObject as SciChartRollingModifier;
        if (modifier == null)
            return;
        modifier._renderTimer.Interval = modifier.RenderInterval;
    }

    /// <summary>
    /// this method actually moves the graph and triggers a repaint by changing the visible range
    /// </summary>
    private void RenderTimer_Elapsed(object sender, EventArgs e)
    {
        DateRange maxRange = (DateRange)XAxis.GetMaximumRange();
        var newestPoint = maxRange.Max;
        if (newestPoint != _oldNewestPoint) // prevent the graph from repainting if nothing changed
            XAxis.VisibleRange = new DateRange(newestPoint - TimeSpan, newestPoint);
        _oldNewestPoint = newestPoint;
    }


    #region Dependency Properties
    public static readonly DependencyProperty TimeSpanProperty = DependencyProperty.Register(
        "TimeSpan", typeof (TimeSpan), typeof (SciChartRollingModifier), new PropertyMetadata(TimeSpan.FromMinutes(1)));
    /// <summary>
    /// This is the timespan the graph always shows in rolling mode. Default is 1min.
    /// </summary>
    public TimeSpan TimeSpan
    {
        get { return (TimeSpan) GetValue(TimeSpanProperty); }
        set { SetValue(TimeSpanProperty, value); }
    }

    public static readonly DependencyProperty RenderIntervalProperty = DependencyProperty.Register(
        "RenderInterval", typeof (TimeSpan), typeof (SciChartRollingModifier), new PropertyMetadata(System.TimeSpan.FromMilliseconds(300), RenderInterval_PropertyChangedCallback));

    /// <summary>
    /// This is the repaint interval. In this interval the graph moves a bit and repaints. Default is 300ms.
    /// </summary>
    public TimeSpan RenderInterval
    {
        get { return (TimeSpan) GetValue(RenderIntervalProperty); }
        set { SetValue(RenderIntervalProperty, value); }
    }
    #endregion


    #region Overrides of ChartModifierBase
    protected override void OnIsEnabledChanged()
    {
        base.OnIsEnabledChanged();
        // start/stop the timer only of the modifier is already attached
        if (IsAttached)
            _renderTimer.IsEnabled = IsEnabled;
    }
    #endregion

    #region Overrides of ApiElementBase
    public override void OnAttached()
    {
        base.OnAttached();
        if (IsEnabled)
            _renderTimer.Start();
    }
    public override void OnDetached()
    {
        base.OnDetached();
        _renderTimer.Stop();
    }
    #endregion
}

ViewModel中计时器的更好解决方案

我可能没有得到你想要的东西,但对我来说,你在ViewModel中投入的功能似乎超出了它的能力。视图模型中有一个计时器会使单元测试变得更加困难。

我会将这些步骤提取到一个单独的组件中,该组件会通知ViewModel计时器间隔已过。而且,如果实现为交互行为,这个单独的组件将确切地知道视图是何时创建/销毁的(通过OnAttached/OnDetached方法),然后可以启动/停止计时器。

这里的另一个好处是,您可以轻松地对ViewModel进行单元测试。

您可以将View的Closing事件绑定到ViewModel中的Command,在DispatchTimer上调用Stop()方法。这将允许计时器和ViewModel是CG:ed。

考虑查看

<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <command:EventToCommand Command="{Binding CloseCommand}" />
    </i:EventTrigger>
</i:Interaction.Triggers>

和ViewModel

public class MyViewModel : ViewModelBase
{
    public MyViewModel()
    {
        DispatcherTimer timer = new DispatcherTimer(
            TimeSpan.FromSeconds(1),
            DispatcherPriority.Render,
            (sender, args) => Console.WriteLine(@"tick"),
            Application.Current.Dispatcher);
        timer.Start();
        CloseCommand = new RelayCommand(() => timer.Stop());
    }
    public ICommand CloseCommand { get; set; }
}

另一种解决方案可以是将计时器设置为静态,或者在ViewModelLocator或类似位置保持对VM的静态引用。