DateTime.Now基于定时器没有结束多个实例正确跟踪(DateTime.Now based t

2019-09-28 18:22发布

这是我写的,以帮助我在工作中我的第一个适当的C#应用​​程序(我在服务台用的脚本和代码一时兴趣的MSP)和我使用UWP只是为了让自己显得漂亮毫不费力。 我们的时间跟踪软件是写在ASP.Net所以一般的网络服务内置的计时器是好的,但也不会生存刷新浏览器,所以我写了我自己的,适合成我们需要为我们的票的格式。

我已经采取了从其他协议栈的问题一些代码,我爸(为跨国公司AC#框架DEV)帮助所以它不是用秒表重新写的一些计时器代码。 他只是不可的时刻来解决这个问题。 我不明白它是如何工作的,现在,只是没怎么调试,我发现了问题。

它支持多个计时器在同一时间运行,创建一个新的计时器自动暂停所有其他人。 它可以处理两个时间格式,分十进制小时,这样就解释一些你在代码中看到的属性。

我的问题是,当我添加一个新的计时器,它会暂停所有其他人,但后来当我按下启动旧的计时器(返回到早期的门票)时立即跳转到新的计时器运行了多长时间,有约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();
        }
    }
}

这进入视图模型:

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按钮现有的计时器是否正在运行或不偏移,但我找不到什么改变它。

Answer 1:

建设足够多的其他代码来支持您发布的代码后,我能够重现你的问题。

在你的代码的问题是,你无条件调用Stop()方法,该计时器是否已经停止或没有。 和Stop()方法无条件重置_offset领域,计时器是否已经运行。 所以,如果你添加一个计时器,当任何其他计时器已经停止,其_offset值不正确地重置。

恕我直言,正确的解决方法是在Start()Stop()方法时,计时器在适当的状态下启动或停止只执行他们的工作。 即检查IsRunning实际上做术前财产。

请参阅下面的实际最小,完整,可验证您发布的代码版本,但没有错误。

除了修复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);
    }
}


文章来源: DateTime.Now based timer not tracking correctly over multiple instances
标签: c# timer uwp