You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于MVVM的WPF应用:定时执行方法及倒计时实现咨询

嘿,这个需求在MVVM架构的WPF应用里非常典型,我来给你一步步拆解实现方案,保证完全符合MVVM的解耦原则,同时兼顾性能和线程安全。

实现思路与步骤

1. 核心原则:遵守MVVM边界

首先要明确:所有定时器逻辑、任务执行、倒计时计算都必须放在ViewModel中,View只负责通过数据绑定展示倒计时,绝对不能在View里写任何业务或定时器逻辑。

2. 选择合适的定时器

WPF里别用System.Windows.Forms.Timer(它绑定UI线程,耗时任务会导致卡顿),推荐用:

  • System.Timers.Timer:用于执行每分钟的核心任务(后台线程触发,不阻塞UI)
  • 结合异步循环更新倒计时(比额外加一个定时器更简洁,也更易维护)

3. ViewModel实现(带完整代码)

首先需要一个实现INotifyPropertyChanged的基类(所有ViewModel都应该继承它,用于通知View更新):

using System.ComponentModel;
using System.Runtime.CompilerServices;

public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

然后是具体的业务ViewModel,包含任务定时器、倒计时逻辑:

using System;
using System.Diagnostics;
using System.Timers;
using System.Windows;

public class TaskSchedulerViewModel : ViewModelBase, IDisposable
{
    private readonly Timer _taskTimer;
    private DateTime _nextRunTime;
    private int _countdownSeconds;

    // 绑定到View的倒计时属性
    public int CountdownSeconds
    {
        get => _countdownSeconds;
        set
        {
            if (_countdownSeconds != value)
            {
                _countdownSeconds = value;
                OnPropertyChanged();
            }
        }
    }

    public TaskSchedulerViewModel()
    {
        // 初始化任务定时器:间隔60秒(60000毫秒)
        _taskTimer = new Timer(60000)
        {
            AutoReset = true // 自动重复触发
        };
        _taskTimer.Elapsed += OnTaskTimerElapsed;

        // 立即执行第一次任务,然后启动定时器
        ExecuteScheduledTask();
        _taskTimer.Start();

        // 启动异步倒计时更新循环(替代第二个定时器,更简洁)
        _ = StartCountdownUpdateLoop();
    }

    private void OnTaskTimerElapsed(object sender, ElapsedEventArgs e)
    {
        ExecuteScheduledTask();
    }

    // 你的核心业务逻辑:每分钟执行一次
    private void ExecuteScheduledTask()
    {
        // 注意:如果逻辑耗时较长,建议放到Task.Run里异步执行,避免阻塞定时器回调
        Debug.WriteLine($"任务执行时间:{DateTime.Now:HH:mm:ss}");

        // 更新下次执行时间
        _nextRunTime = DateTime.Now.AddSeconds(60);

        // 切换到UI线程更新倒计时(因为定时器回调在后台线程)
        Application.Current.Dispatcher.Invoke(() =>
        {
            CountdownSeconds = 60;
        });
    }

    // 异步循环更新倒计时,每秒刷新一次
    private async System.Threading.Tasks.Task StartCountdownUpdateLoop()
    {
        while (true)
        {
            var remainingTime = _nextRunTime - DateTime.Now;
            if (remainingTime.TotalSeconds > 0)
            {
                // 切换到UI线程更新属性
                Application.Current.Dispatcher.Invoke(() =>
                {
                    CountdownSeconds = (int)Math.Ceiling(remainingTime.TotalSeconds);
                });
            }
            else
            {
                CountdownSeconds = 0;
            }

            // 等待1秒再更新
            await System.Threading.Tasks.Task.Delay(1000);
        }
    }

    // 释放资源,避免内存泄漏
    public void Dispose()
    {
        _taskTimer?.Stop();
        _taskTimer?.Dispose();
    }
}

4. View层绑定实现

在XAML里,将View的DataContext设置为这个ViewModel,然后绑定CountdownSeconds属性即可:

<Window x:Class="WpfMvvmTimer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfMvvmTimer"
        Title="任务调度器" Height="300" Width="500"
        Closing="Window_Closing">
    <Window.DataContext>
        <local:TaskSchedulerViewModel />
    </Window.DataContext>
    <Grid>
        <TextBlock FontSize="28" HorizontalAlignment="Center" VerticalAlignment="Center">
            距离下次任务执行:<Run Text="{Binding CountdownSeconds}" FontWeight="Bold" />秒
        </TextBlock>
    </Grid>
</Window>

别忘了在窗口关闭时释放ViewModel的资源,避免内存泄漏(在Code-behind里):

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    if (DataContext is IDisposable disposableVm)
    {
        disposableVm.Dispose();
    }
}

关键注意事项

  • 线程安全:System.Timers.Timer的回调是在后台线程触发的,更新ViewModel属性时必须切换到UI线程(用Dispatcher.Invoke或SynchronizationContext.Current.Post),否则会抛出跨线程访问UI元素的异常。
  • 内存泄漏:必须实现IDisposable接口释放定时器资源,否则ViewModel会被定时器引用无法被GC回收。
  • 异步优化:如果你的任务逻辑比较耗时,一定要用Task.Run异步执行,避免阻塞定时器的回调线程,导致后续任务延迟。

内容的提问来源于stack exchange,提问作者Belcookies

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 07:21:13