ViewModel 中计时器的更好解决方案?
Better solution for timer in ViewModel?
我在图形组件的 ViewModel 中有一个 DispatcherTimer,用于定期更新它(滚动它)。
最近我发现这是一个巨大的资源泄漏,因为每次导航到图形视图时都会新创建 ViewModel,并且 DispatcherTimer 会阻止 GC 破坏我的 ViewModel,因为 Tick-Event 对它。
我用 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>();
}
这就是我的使用方式。这与前面没有 "weak" 完全相同:
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)
{
...
}
}
它似乎工作正常,但这真的是 best/easiest 解决方案还是我遗漏了什么?
我在 google 上完全没有发现任何东西,不敢相信我是唯一一个在 ViewModel 中使用计时器来更新某些东西并发生资源泄漏的人...感觉不对!
更新
由于图形组件(SciChart)提供了一种附加修饰符(行为)的方法,我写了一个 SciChartRollingModifier,这基本上是 AlexSeleznyov 在他的回答中建议的。使用 Behavior 也可以,但这更简单!
如果其他人需要滚动 SciChart 线图,请按以下步骤操作:
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
}
您可以将 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 的静态引用。
我可能无法准确理解您的需求,但对我来说,您似乎将更多功能添加到 ViewModel 中,超出了它的处理能力。在视图模型中使用计时器会使单元测试变得更加困难。
我会将这些步骤提取到一个单独的组件中,该组件会通知 ViewModel 计时器间隔已过。而且,如果作为 Interactivity Behavior 实现,这个单独的组件将准确知道 View 何时为 created/destroyed(通过 OnAttached/OnDetached 方法),并且反过来可以 start/stop 计时器。
这里的另一个好处是您可以 unit-test 轻松地使用 ViewModel。
我在图形组件的 ViewModel 中有一个 DispatcherTimer,用于定期更新它(滚动它)。
最近我发现这是一个巨大的资源泄漏,因为每次导航到图形视图时都会新创建 ViewModel,并且 DispatcherTimer 会阻止 GC 破坏我的 ViewModel,因为 Tick-Event 对它。
我用 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>();
}
这就是我的使用方式。这与前面没有 "weak" 完全相同:
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)
{
...
}
}
它似乎工作正常,但这真的是 best/easiest 解决方案还是我遗漏了什么?
我在 google 上完全没有发现任何东西,不敢相信我是唯一一个在 ViewModel 中使用计时器来更新某些东西并发生资源泄漏的人...感觉不对!
更新
由于图形组件(SciChart)提供了一种附加修饰符(行为)的方法,我写了一个 SciChartRollingModifier,这基本上是 AlexSeleznyov 在他的回答中建议的。使用 Behavior 也可以,但这更简单!
如果其他人需要滚动 SciChart 线图,请按以下步骤操作:
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
}
您可以将 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 的静态引用。
我可能无法准确理解您的需求,但对我来说,您似乎将更多功能添加到 ViewModel 中,超出了它的处理能力。在视图模型中使用计时器会使单元测试变得更加困难。
我会将这些步骤提取到一个单独的组件中,该组件会通知 ViewModel 计时器间隔已过。而且,如果作为 Interactivity Behavior 实现,这个单独的组件将准确知道 View 何时为 created/destroyed(通过 OnAttached/OnDetached 方法),并且反过来可以 start/stop 计时器。
这里的另一个好处是您可以 unit-test 轻松地使用 ViewModel。