5

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
}
JCH2k
  • 3,361
  • 32
  • 25

2 Answers2

6

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.

Alex Seleznyov
  • 905
  • 6
  • 18
  • This is the way to go i think. I could even move the whole work-part into the Behaviour, so its completely reusable! I'll post my code above, it's not an Interactivity Behaviour, because the graph component has something similar, but it's almost the same (with OnAttached/OnDetached). – JCH2k Jan 12 '16 at 16:41
4

You could bind your View's Closing event to a Command in your ViewModel, calling Stop() method on your DispatchTimer. This would allow the timer and ViewModel to be CG:ed.

Consider View

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

and 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; }
}

Other solution could be making timer static or holding static reference to your VM in ViewModelLocator or similar place.

Mikko Viitala
  • 8,344
  • 4
  • 37
  • 62
  • This solution worked good, but I like @AlexSeleznyov solution even better. .. Sorry :) – JCH2k Jan 12 '16 at 16:50
  • 1
    This is exactly the technique we've used in the SciChart WPF Examples application - we created our own behaviour which binds from UserControl.Unloaded to a ViewModel.CloseCommand. We use this to stop timers (used extensively) and resources get cleaned up!! Furthermore, I recommend using DispatcherPriority.Render in DispatcherTimers with SciChart. It's much smoother! – Dr. Andrew Burnett-Thompson Jan 29 '16 at 07:44