我在ViewModel的图形模型中有一个DispatcherTimer,用于定期更新(滚动)它。
最近,我发现这是一个巨大的资源泄漏,因为每次我导航到图形视图时都会重新创建ViewModel,并且DispatcherTimer阻止了GC破坏我的ViewModel,因为Tick-Event拥有强大的引用。
我与包装解决了这个围绕它使用的DispatcherTimer FastSmartWeakEvent从CodeProject /丹尼尔·格伦沃尔德以避免强烈的参考VM和破坏自己一旦有没有更多的听众:
public class WeakDispatcherTimer { ////// the actual timer /// 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_tickEvent = new FastSmartWeakEvent (); }
这就是我的用法。没有之前的“弱”,这是完全相同的:
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) { ... } }
看来效果很好,但这真的是最好/最简单的解决方案,还是我错过了什么?
我在Google上一无所获,真不敢相信我是唯一在ViewModel中使用计时器来更新某些内容并造成资源泄漏的人……感觉不对!
更新
由于图形组件(SciChart)提供了一种附加修饰符(Behaviours)的方法,因此我写了一个SciChartRollingModifier,基本上就是AlexSeleznyov在他的回答中建议的内容。有了“行为”,这也是可能的,但这甚至更简单!
如果其他任何人都需要滚动的SciChart线形图,请执行以下操作:
public class SciChartRollingModifier : ChartModifierBase { DispatcherTimer _renderTimer; private DateTime _oldNewestPoint; public SciChartRollingModifier() { _renderTimer = new DispatcherTimer(RenderInterval, DispatcherPriority.Render, RenderTimer_Elapsed, Application.Current.Dispatcher); } ////// Updates the render interval one it's set by the property (e.g. with a binding or in XAML) /// private static void RenderInterval_PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { SciChartRollingModifier modifier = dependencyObject as SciChartRollingModifier; if (modifier == null) return; modifier._renderTimer.Interval = modifier.RenderInterval; } ////// this method actually moves the graph and triggers a repaint by changing the visible range /// 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))); ////// This is the timespan the graph always shows in rolling mode. Default is 1min. /// 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)); ////// This is the repaint interval. In this interval the graph moves a bit and repaints. Default is 300ms. /// 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 }
Alex Selezny.. 5
我可能不完全了解您所追求的,但是对我来说,您似乎在ViewModel中增加了更多功能,无法处理。在视图模型中使用计时器会使单元测试更加困难。
我将那些步骤提取到一个单独的组件中,该组件将通知ViewModel计时器间隔已过去。并且,如果以交互行为的形式实现,则该单独的组件将确切地知道何时创建/销毁View(通过OnAttached / OnDetached方法),进而可以启动/停止计时器。
这里的另一个好处是,您可以轻松地对该ViewModel进行单元测试。
我可能不完全了解您所追求的,但是对我来说,您似乎在ViewModel中增加了更多功能,无法处理。在视图模型中使用计时器会使单元测试更加困难。
我将那些步骤提取到一个单独的组件中,该组件将通知ViewModel计时器间隔已过去。并且,如果以交互行为的形式实现,则该单独的组件将确切地知道何时创建/销毁View(通过OnAttached / OnDetached方法),进而可以启动/停止计时器。
这里的另一个好处是,您可以轻松地对该ViewModel进行单元测试。