这是我写的第一个正确的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按钮时,无论现有的计时器是否正在运行,我都能找到改变偏移量的东西,但是我无法找到改变它的东西。
答案 0 :(得分:1)
在构建足够的其他代码以支持您发布的代码后,我能够重现您的问题。
您的代码中的问题是您无条件地调用Stop()
方法,无论计时器是否已经停止。并且Stop()
方法无条件地重置_offset
字段,无论计时器是否已在运行。因此,如果在任何其他计时器已停止时添加计时器,则其_offset
值将被错误地重置。
恕我直言,正确的解决方法是Start()
和Stop()
方法仅在计时器处于启动或停止的适当状态时执行其工作。即在实际执行操作之前检查IsRunning
属性。
请参阅下文,了解您发布的代码的实际Minimal, Complete, and Verifiable版本,但没有错误。
除了修复bug之外,我还删除了所有未使用的元素(即,在您的场景中似乎没有使用或讨论过的所有代码)并重构了代码,使其更像是典型的WPF实现(最后请参阅帮助程序/基类)。当我运行程序时,即使在列表中添加了新的计时器之后,我也可以毫无困难地启动和停止计时器对象。
值得注意的修改:
NotifyPropertyChangedBase
类作为模型类的基类。ICommand
实施用于用户操作(即“命令”)。-
和+
数学DateTime
和TimeSpan
运算符
<强> 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);
}
}