基于DateTime.Now的计时器无法在多个实例上正确跟踪

时间:2017-09-26 00:59:02

标签: c# timer uwp

这是我写的第一个正确的C#应用​​程序,用于帮助我工作(我在MSP的帮助台上对脚本和代码感兴趣)并且我正在使用UWP来制作它看起来相当不费力。我们的时间跟踪软件是一个用ASP.Net编写的Web服务,所以通常内置的计时器很好,但它不会在浏览器刷新后继续存在,所以我自己编写了符合我们门票需要的格式。

我从其他Stack问题中获取了一些代码,而我的父亲(一个跨国公司的C#框架开发者)帮助重写了一些定时器代码,因此它没有使用秒表。他目前无法解决此问题。我确实理解它现在是如何工作的,而不是如何调试我得到的问题。

它支持多个同时运行的计时器,并创建一个新的计时器自动暂停所有其他计时器。它处理两种时间格式,分钟和小数时,这将解释您在代码中看到的一些属性。

我的问题是,当我添加一个新计时器时,它会暂停所有其他计时器,但是当我按下旧计时器上的启动时(返回到更早的故障单),时间会立即跳到新计时器运行的时间因为,差异大约为10%(它的运行时间从未确定过。)

这是跟踪笔记和当前时间的类(为了整洁而整理一下):

public sealed class JobTimer:INotifyPropertyChanged
{
    private DateTime _created; // When the timer was created
    private DateTime _started; // When it was most recently started
    private TimeSpan _offset; // The saved value to offset the currently running timer
    Timer _swTimer; // The actual tick that updates the screen

    public JobTimer() : this(TimeSpan.Zero)
    { }

    public JobTimer(TimeSpan offset)
    {
        _offset = offset;
        _created = DateTime.Now;
        IsNotLocked = true;
    }

    // Time in seconds
    public string TimeMin => string.Format("{0:00}:{1:00}:{2:00}", ElapsedTime.Hours, ElapsedTime.Minutes, ElapsedTime.Seconds);

    // Time in decimal hours
    public string TimeDec => string.Format("{0}", 0.1 * Math.Ceiling(10 * ElapsedTime.TotalHours));

    public DateTime Created => _created;

    public TimeSpan ElapsedTime => GetElapsed();

    public void Start()
    {
        _started = DateTime.Now;
        _swTimer = new Timer(TimerChanged, null, 0, 1000);

        NotifyPropertyChanged("IsRunning");
    }

    public void Stop()
    {
        if (_swTimer != null)
        {
            _swTimer.Dispose();
            _swTimer = null;
        }

        _offset = _offset.Add(DateTime.Now.Subtract(_started));

        NotifyPropertyChanged("IsRunning");
    }

    private TimeSpan GetElapsed()
    {
        // This was made as part of my own debugging, the ElaspsedTime property used to just be the if return
        if (IsRunning)
        {
            return _offset.Add(DateTime.Now.Subtract(_started));
        }
        else
        {
            return _offset;
        }
    }

    // Updates the UI
    private void TimerChanged(object state)
    {
        NotifyPropertyChanged("TimeDec");
        NotifyPropertyChanged("TimeMin");
    }

    public bool IsRunning
    {
        get { return _swTimer != null; }
    }

    public void ToggleRunning()
    {
        if (IsRunning)
        {
            Stop();
        }
        else
        {
            Start();
        }
    }
}

这将进入ViewModel:

public class JobListViewModel
{
    private readonly ObservableCollection<JobTimer> _list = new ObservableCollection<JobTimer>();

    public ObservableCollection<JobTimer> JobTimers => _list;

    public JobListViewModel()
    {
        AddTimer();
    }

    public void AddTimer()
    {
        JobTimer t = new JobTimer();
        JobTimers.Add(t);
        t.Start();
    }

    public void PauseAll()
    {
        foreach(JobTimer timer in JobTimers)
        {
            timer.Stop();
        }
    }

    // Other functions unrelated
}

这是添加新计时器的用户界面按钮

    private void AddTimer_Click(object sender, RoutedEventArgs e)
    {
        // Create JobTimer
        ViewModel.PauseAll();
        ViewModel.AddTimer();

        // Scroll to newly created timer
        JobTimer lastTimer = ViewModel.JobTimers.Last();
        viewTimers.UpdateLayout();
        viewTimers.ScrollIntoView(lastTimer);
    }

我意识到要将大量代码转储到帖子中,但我无法确定问题所在的位置。当我按下AddTimer按钮时,无论现有的计时器是否正在运行,我都能找到改变偏移量的东西,但是我无法找到改变它的东西。

1 个答案:

答案 0 :(得分:1)

在构建足够的其他代码以支持您发布的代码后,我能够重现您的问题。

您的代码中的问题是您无条件地调用Stop()方法,无论计时器是否已经停止。并且Stop()方法无条件地重置_offset字段,无论计时器是否已在运行。因此,如果在任何其他计时器已停止时添加计时器,则其_offset值将被错误地重置。

恕我直言,正确的解决方法是Start()Stop()方法仅在计时器处于启动或停止的适当状态时执行其工作。即在实际执行操作之前检查IsRunning属性。

请参阅下文,了解您发布的代码的实际Minimal, Complete, and Verifiable版本,但没有错误。

除了修复bug之外,我还删除了所有未使用的元素(即,在您的场景中似乎没有使用或讨论过的所有代码)并重构了代码,使其更像是典型的WPF实现(最后请参阅帮助程序/基类)。当我运行程序时,即使在列表中添加了新的计时器之后,我也可以毫无困难地启动和停止计时器对象。

值得注意的修改:

  • 使用NotifyPropertyChangedBase类作为模型类的基类。
  • 通过将公共属性保持为根据需要修改的简单值存储属性,利用所述基类功能进行属性更改通知。
  • ICommand实施用于用户操作(即“命令”)。
  • 从特定于视图的滚动视图行为添加计时器时,分离特定于计时器的启动/停止功能。
  • 从非UI模型对象中删除时间格式化逻辑,并将其放入XAML中
  • 使用-+数学
  • 的常规(且更具可读性)DateTimeTimeSpan运算符

<强> JobTimer.cs:

class JobTimer : NotifyPropertyChangedBase
{
    private DateTime _started; // When it was most recently started
    private TimeSpan _offset; // The saved value to offset the currently running timer
    Timer _swTimer; // The actual tick that updates the screen

    private readonly DelegateCommand _startCommand;
    private readonly DelegateCommand _stopCommand;

    public ICommand StartCommand => _startCommand;
    public ICommand StopCommand => _stopCommand;

    public JobTimer() : this(TimeSpan.Zero)
    { }

    public JobTimer(TimeSpan offset)
    {
        _offset = offset;
        _startCommand = new DelegateCommand(Start, () => !IsRunning);
        _stopCommand = new DelegateCommand(Stop, () => IsRunning);
    }

    private TimeSpan _elapsedTime;
    public TimeSpan ElapsedTime
    {
        get { return _elapsedTime; }
        set { _UpdateField(ref _elapsedTime, value); }
    }

    public void Start()
    {
        _started = DateTime.UtcNow;
        _swTimer = new Timer(TimerChanged, null, 0, 1000);
        IsRunning = true;
    }

    public void Stop()
    {
        if (_swTimer != null)
        {
            _swTimer.Dispose();
            _swTimer = null;
        }

        _offset += DateTime.UtcNow - _started;
        IsRunning = false;
    }

    private TimeSpan GetElapsed()
    {
        return IsRunning ? DateTime.UtcNow - _started + _offset : _offset;
    }

    // Updates the UI
    private void TimerChanged(object state)
    {
        ElapsedTime = GetElapsed();
    }

    private bool _isRunning;
    public bool IsRunning
    {
        get { return _isRunning; }
        set { _UpdateField(ref _isRunning, value, _OnIsRunningChanged); }
    }

    private void _OnIsRunningChanged(bool obj)
    {
        _startCommand.RaiseCanExecuteChanged();
        _stopCommand.RaiseCanExecuteChanged();
    }
}

<强> MainViewModel.cs:

class MainViewModel : NotifyPropertyChangedBase
{
    public ObservableCollection<JobTimer> JobTimers { get; } = new ObservableCollection<JobTimer>();

    public ICommand AddTimerCommand { get; }

    public MainViewModel()
    {
        AddTimerCommand = new DelegateCommand(_AddTimer);
        _AddTimer();
    }

    private void _AddTimer()
    {
        foreach (JobTimer timer in JobTimers)
        {
            timer.Stop();
        }

        JobTimer t = new JobTimer();
        JobTimers.Add(t);
        t.Start();
    }
}

<强> MainWindow.xaml.cs:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        MainViewModel model = (MainViewModel)DataContext;

        model.JobTimers.CollectionChanged += _OnJobTimersCollectionChanged;
    }

    private void _OnJobTimersCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        ObservableCollection<JobTimer> jobTimers = (ObservableCollection<JobTimer>)sender;

        // Scroll to newly created timer
        JobTimer lastTimer = jobTimers.Last();
        listBox1.ScrollIntoView(lastTimer);
    }
}

<强> MainWindow.xaml:

<Window x:Class="TestSO46416275DateTimeTimer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:l="clr-namespace:TestSO46416275DateTimeTimer"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
  <Window.DataContext>
    <l:MainViewModel/>
  </Window.DataContext>

  <Window.Resources>
    <DataTemplate DataType="{x:Type l:JobTimer}">
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding ElapsedTime, StringFormat=hh\\:mm\\:ss}"/>
        <Button Content="Start" Command="{Binding StartCommand}"/>
        <Button Content="Stop" Command="{Binding StopCommand}"/>
      </StackPanel>
    </DataTemplate>
  </Window.Resources>

  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition/>
    </Grid.RowDefinitions>

    <Button Content="Add Timer" Command="{Binding AddTimerCommand}" HorizontalAlignment="Left"/>
    <ListBox x:Name="listBox1" ItemsSource="{Binding JobTimers}" Grid.Row="1"/>
  </Grid>
</Window>

<强> NotifyPropertyChangedBase.cs:

class NotifyPropertyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void _UpdateField<T>(ref T field, T newValue,
        Action<T> onChangedCallback = null,
        [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, newValue))
        {
            return;
        }

        T oldValue = field;

        field = newValue;
        onChangedCallback?.Invoke(oldValue);
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

<强> DelegateCommand.cs:

class DelegateCommand : ICommand
{
    private readonly Action _execute;
    private readonly Func<bool> _canExecute;

    public DelegateCommand(Action execute) : this(execute, null)
    { }

    public DelegateCommand(Action execute, Func<bool> canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public event EventHandler CanExecuteChanged;

    public bool CanExecute(object parameter)
    {
        return _canExecute == null || _canExecute();
    }

    public void Execute(object parameter)
    {
        _execute();
    }

    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
}