C# WPF中System.Timers.Timer引发InvalidOperationException问题
问题分析与解决方案
问题根源
System.Timers.Timer的Elapsed事件是在线程池线程中触发的,因此Recorder.StateChanged事件会在非UI线程上触发。而WPF中ICommand.RaiseCanExecuteChanged()方法必须在UI线程调用,否则会抛出InvalidOperationException,或者导致命令关联的按钮状态更新异常。
解决方案
方案1:为Timer设置SynchronizingObject
System.Timers.Timer的SynchronizingObject属性可以指定一个UI线程的控件(如Window、UserControl),这样Elapsed事件会自动切换到UI线程执行,StateChanged事件也会在UI线程触发,无需额外处理线程切换。
- 给
Recorder类添加设置SynchronizingObject的方法(因为Timer是私有成员):
internal class Recorder { // ... 原有代码 ... public void SetTimerSynchronizingObject(ISynchronizeInvoke syncObj) { Timer.SynchronizingObject = syncObj; } }
- 在View的代码后台(如MainWindow.xaml.cs)中初始化Recorder时,传入当前UI控件:
var recorder = new Recorder(feed, data, isLooped); recorder.SetTimerSynchronizingObject(this); // this指代当前Window,继承自ISynchronizeInvoke var vm = new RecorderViewModel(recorder);
方案2:在ViewModel中切换到UI线程更新命令状态
如果无法修改Recorder类的设计,就在StateChanged事件处理方法中,通过Dispatcher切换到UI线程调用RaiseCanExecuteChanged。注意用Invoke(同步)而非BeginInvoke,确保状态更新的及时性:
void OnStateChanged(object? sender, RecorderState state) { Application.Current.Dispatcher.Invoke(() => { PauseCommandField.RaiseCanExecuteChanged(); ResetCommandField.RaiseCanExecuteChanged(); }); }
如果你的ViewModel继承自DependencyObject,也可以直接用自身的Dispatcher:
this.Dispatcher.Invoke(() => { /* ... */ });
方案3:改用WPF专属的DispatcherTimer
如果Recorder类仅在WPF环境中使用,直接将System.Timers.Timer替换为DispatcherTimer,它的Tick事件默认在UI线程触发,彻底避免线程问题:
internal class Recorder { const double TimeIntervalMilliseconds = 20; readonly DispatcherTimer Timer; // ... 其他成员不变 ... public Recorder(IFeed feed, ISessionDataSink data, bool isLooped) { Feed = feed; Data = data; IsLooped = isLooped; state = RecorderState.Initial; Timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(TimeIntervalMilliseconds), AutoReset = true }; Timer.Tick += OnTimeElapsed; } // 调整OnTimeElapsed的方法签名 void OnTimeElapsed(object? sender, EventArgs e) { // 原有定时逻辑不变 } }
内容的提问来源于stack exchange,提问作者Software Carpenter
相关产品推荐
相关产品推荐

