Reputation: 31
I am making a software to show a chronometer on screen and run certain actions at certain times defined by the user. I found a way to show the chronometer on an independent window by using a DispatcherTimer with a 16ms Interval (around 60FPS) but now I have to find a way to run those actions at the defined times.
I made a component QueueableStopwatch
which does the work. It works in the following way:
QueueAction
objectsStopwatch
to count the timeStart()
and Stop()
Start()
starts the internal stopwatch and starts the "queue processing" loop on a separate threadStop()
stops the stopwatch and the "queue processing" loop stops by itself when Stopwatch.IsRunning
changes to falseThe "queue processing" loop does the following:
QueueAction
to be ran. If the internal Stopwatch.Elapsed
> to the referenced QueueAction.Interval
its ran and the reference updates to the next one in the run once QueueActions arrayStopwatch.Elapsed / QueueAction.Interval - QueueAction.TimesExecuted >= 1
. If ran we increase QueueAction.TimesExecuted
by one.Is this solution good enough to be implemented as the "core" of an application running critical actions?
Can the usage of Stopwatch.IsRunning
end up in unexpected behavior as documented here?
This is the component code:
public class QueueAction
{
/// <summary>
/// Interval to run the action
/// </summary>
public TimeSpan Interval { get; set; }
/// <summary>
/// The current action to run
/// </summary>
public Action Action { get; set; }
/// <summary>
/// Dispatcher the action will be ran into
/// </summary>
public Dispatcher Dispatcher { get; set; }
/// <summary>
/// True if the action will be repeated
/// </summary>
public bool Repeat { get; set; }
}
public class QueueableStopwatch
{
private Stopwatch _stopwatch = new Stopwatch();
public TimeSpan Elapsed => _stopwatch.Elapsed;
private RepeatableQueueAction[] _repeatQueue = { };
private QueueAction[] _singleQueue = { };
public QueueAction[] Queue
{
get => _singleQueue;
set
{
_repeatQueue = value.Where(action => action.Repeat).Select(action => new RepeatableQueueAction { QueueAction = action }).ToArray();
_singleQueue = value.Where(action => !action.Repeat).OrderBy(action => action.Interval.TotalMilliseconds).ToArray();
}
}
public void Start()
{
if (_stopwatch.IsRunning)
throw new InvalidOperationException("The chronometer is already running");
_stopwatch.Start();
if(_singleQueue.Length > 0)
{
new Task(() =>
{
int i = 0;
QueueAction selectedAction = selectedAction = _singleQueue[i];
do
{
if (i < _singleQueue.Length && selectedAction.Interval <= _stopwatch.Elapsed) // Single time run queue
{
selectedAction.Dispatcher.Invoke(() => selectedAction.Action());
i++;
if(i < _singleQueue.Length)
selectedAction = _singleQueue[i];
}
foreach(var repetitionAction in _repeatQueue) // Repeat run queue
{
if(_stopwatch.Elapsed / repetitionAction.QueueAction.Interval - repetitionAction.Repetitions >= 1)
{
repetitionAction.QueueAction.Dispatcher.Invoke(() => repetitionAction.QueueAction.Action());
repetitionAction.Repetitions++;
}
}
}
while (_stopwatch.IsRunning);
}).Start();
}
}
public void Stop()
{
_stopwatch.Reset();
}
private class RepeatableQueueAction
{
public QueueAction QueueAction { get; set; }
public int Repetitions { get; set; }
}
}
If you want to run it this xaml does the work:
MainWindow.xaml
<Window x:Class="WpfApp1.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:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800"
Background="Black">
<StackPanel Orientation="Vertical">
<Label Name="lblMessage" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="56"/>
<Button Click="Button_Click" Content="Stop" HorizontalAlignment="Center"/>
</StackPanel>
</Window>
MainWindow.cs
public partial class MainWindow : Window
{
QueueableStopwatch stopwatch = new QueueableStopwatch();
public MainWindow()
{
InitializeComponent();
stopwatch.Queue = new QueueAction[]
{
new QueueAction
{
Dispatcher = lblMessage.Dispatcher,
Interval = TimeSpan.FromSeconds(7),
Action = () => lblMessage.Content = $"[{stopwatch.Elapsed}]I run every 7 seconds",
Repeat = true
},
new QueueAction
{
Dispatcher = lblMessage.Dispatcher,
Interval = TimeSpan.FromSeconds(10),
Action = () => lblMessage.Content = $"[{stopwatch.Elapsed}]Queued first but ran at 10 seconds"
},
new QueueAction
{
Dispatcher = lblMessage.Dispatcher,
Interval = TimeSpan.FromSeconds(3),
Action = () => lblMessage.Content = $"[{stopwatch.Elapsed}]3 seconds elapsed"
}
};
stopwatch.Start();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
stopwatch.Stop();
}
}
Upvotes: 0
Views: 106
Reputation: 36371
A stopwatch is the wrong tool for the job, you are essentially spinwaiting on a thread, burning CPU time and potentially causing other threads to be starved.
The framework already provides this functionality in the form of timers. If you want to run actions on the UI thread a dispatch timer would be suitable. So for each action you want to schedule, create a corresponding timer. You might need a wrapper if you want to decide up front if the action should be repeated or not.
var timer = new DispatcherTimer(){ Interval = TimeSpan.FromSeconds(10) };
timer.Tick += (o, e) => {
lblMessage.Content = $"[{stopwatch.Elapsed}]Queued first but ran at 10 seconds"
timer.Stop();
};
timer.Start();
The resolution of timers depend on the OS, but typically 1-16ms. For a UI program this should be sufficient, there will be various small delays for the screen to render anyway. If you need better resolution there are mediatimer.
Upvotes: 1