2017-09-26 5 views
1

これは仕事で私を助けるために書いた最初の適切なC#アプリケーションです(私はスクリプトとコードに関心のあるMSPのヘルプデスクにいます)私はUWPを使ってかなりの労力をかけずに見えるようにしています。時間トラッキングソフトウェアは、ASP.Netで書かれたWebサービスなので、一般的にタイマーは内蔵されていますが、ブラウザのリフレッシュに耐えることはできませんので、チケットに必要なフォーマットに自分自身を書きました。DateTime.Nowベースのタイマーが複数のインスタンスで正確にトラッキングされていない

他のスタックの質問からいくつかのコードを取りました。私の父親(多国籍企業のC#フレームワーク開発者)は、ストップウォッチを使用していないようにタイマーコードの一部を書き直しました。彼は現時点でこの問題を解決することはできません。私は今、どのように動作しているのか理解しています。問題をデバッグする方法ではありません。

複数のタイマーが同時に実行され、新しいタイマーを作成すると他のタイマーはすべて自動的に一時停止されます。 2つの時刻形式(分と小数の時間)を処理するので、コードに表示されているいくつかのプロパティについて説明します。

私の問題は、新しいタイマーを追加すると、他のタイマーがすべて一時停止しますが、以前のタイマー(直前のチケットに戻る)でstartを押すと、新しいタイマーの長さ約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 
} 

そして、これは、UIのボタンですそれは新しいタイマー

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); 
    } 

を追加]をクリックし、私はそれがポストにダンプするコードの多くのですが、私はcを実現問題が発生している場所を特定できません。 AddTimerボタンを押すと、既存のタイマーが実行されているかどうかにかかわらず、何かがオフセットを変更することがわかりましたが、変更するものを見つけることができません。

+0

確実に問題を再現し、バグが何であるかを確かに言うことができないことがあります。しかし、なぜ、ストップウォッチなしでこれを実装するのが最初の目標でしたか?タイマーの開始と停止を期待しているなら、それを実装するのが最も簡単で信頼性の高い方法だと思います。タイマーが一時停止された合計時間を追跡し、ネット時間からそれを減算しようとしているようです。開始/一時停止/停止機能が組み込まれている完全に使用可能なクラスがある場合、そのポイントは何ですか? –

+0

ローカルで実行する場合は、コードを圧縮することができます。私はUIで毎回再現することができますが、私はどこにブレークポイントを置くべきかわかりません。 'Stopwatch'から離れる理由は、正確にmsである必要はなく、ファイルに保存してからロードする必要があるからです(したがって' _offset'プロパティ)。 'DateTime.Now'を使用して起動し、別の' Now'を取得して開始時刻を引いたものは、停止時間の合計ではなく、実行時間の合計です。また、 'TimeSpan'をテキストとして保存し、後でStopwatchのラッパーを記述せずに再度読み込むこともできます。 – LastElf

+0

また私のお父さんは、最初にこれを行ったときに、あなたがプログラムで持つことができるストップウォッチの数に制限があり、最近のバージョンで変更されていると思われますが、タイマーと同じ方法で処理することはできません。実用的な違いは、パフォーマンスメトリックを取得しようとすると、DateTimeのパフォーマンスが重くなくても正確ではないことです。 – LastElf

答えて

1

投稿したコードをサポートするのに十分な他のコードを作成した後、問題を再現できました。

コード内の問題は、タイマーが既に停止しているかどうかにかかわらず、無条件にStop()メソッドを呼び出すことです。また、Stop()メソッドは、タイマが既に実行中であるかどうかにかかわらず、無条件に_offsetフィールドをリセットします。したがって、他のタイマーが既に停止しているときにタイマーを追加すると、その_offsetの値が正しくリセットされません。

IMHOの場合、適切な修正は、タイマーが適切な開始または停止状態にあるときにのみ、その作業を実行することです(Start()およびStop())。私。実際に操作を行う前にIsRunningプロパティをチェックしてください。

あなたが投稿したコードの実際のMinimal, Complete, and Verifiableバージョンについては、以下を参照してください。バグはありません。

バグを修正するだけでなく、未使用の要素(シナリオで使用または議論されていないすべてのコード)を削除し、コードをリファクタリングして、典型的な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:良い[MCVE]を欠い

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); 
    } 
} 
+0

私のコード例では謝罪しましたが、今はその違いを見ています。私はいくつかのデバッグ用のものとクロックの仕組みに関係していないビットをクリーンアップしましたが、その違いを見ることができます。私は次回のために覚えています!あなたの事例をありがとうございました。あなたの最初の段落を読んだとき、私のコードをチェックし、if文の中で '_offset = _offset.Add'部分をテストしました。私は彼らがどのように動作するかを理解し始めると、あなたの他の変更を実装する方法を確実に見ていきます。コードを書くことは簡単ですが、標準に書き込むことは私が学んでいることです – LastElf

関連する問題