I have a DispatcherTimer in a ViewModel for a graph component, to periodically update it (roll it).
Recently I discovered this is a massive resource leak since the ViewModel is created newly every time I navigate to the graph view and the DispatcherTimer is preventing the GC from destroying my ViewModel, because the Tick-Event holds a strong reference on it.
I solved this with a Wrapper around the DispatcherTimer which uses the FastSmartWeakEvent from Codeproject/Daniel Grunwald to avoid a strong reference to the VM and destroys itself once there are no more listeners:
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>(); 
}
This is how I use it. This was exactly the same without the "weak" before:
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)
    {
        ...
    }
}
It seems to work good, but is this really the best/easiest solution or am I missing something?
I found absolutely nothing on google and can't believe I'm the only person using a timer in a ViewModel to update something and have a resource leak... That doesn't feel right!
UPDATE
As the graph component (SciChart) provides a method for attaching Modifiers (Behaviours), i wrote a SciChartRollingModifier, which is basically what AlexSeleznyov suggested in his answer. With a Behaviour it would have also been possible, but this is even simpler!
If anyone else needs a rolling SciChart LineGraph, this is how to do it:
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
}
                I might be not getting exactly what you're after, but to me it looks like you're putting more functionality into ViewModel than it can handle. Having a timer in view model makes unit testing somewhat harder.
I'd have those steps extracted to a separate component which would notify ViewModel that timer interval elapsed. And, if implemented as an Interactivity Behavior, this separate component woudl know exactly when View is created/destroyed (via OnAttached/OnDetached methods) and, in turn, can start/stop timer.
One more benefit here is that you can unit-test that ViewModel with ease.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With