I'm experiencing two main issues while using Stopwatch
to track elapsed time in my application.
Delayed Start: When I click the start button, the elapsed time takes about 3-5 seconds before it starts counting, even though the application process is already running.
Time Jumps: Occasionally, the elapsed time "jumps" by a few seconds. For example, if the stopwatch reaches 00:00:25
, it sometimes freezes momentarily and then jumps straight to 00:00:27
, skipping a second.
I attempted to force a UI update immediately by adding a delay, but that didn't fix the initial issues.
I tried different ways to ensure smooth UI updates, but the problems persist.
At the top of Form1
, I have the following code:
private Stopwatch stopwatch = new Stopwatch();
System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
in the constructor :
public Form1()
{
InitializeComponent();
LoadSettings();
InitializeTooltips();
lblWebpage.Select();
}
the button click event that start the operation abd the elapsed time:
private async void btnDownload_Click(object sender, EventArgs e)
{
SaveSettings();
InitElapsedTime(); // ✅ Start elapsed time immediately without using Task.Run
await Task.Delay(10); // ✅ Give the UI a chance to update before proceeding
string url = txtUrl.Text.Trim();
string startPattern = txtStartPattern.Text.Trim();
string endPattern = txtEndPattern.Text.Trim();
bool findAll = chkFindAll.Checked;
bool useRegex = chkUseRegex.Checked;
string regexPattern = cmbRegexPresets.SelectedIndex > 0 ? cmbRegexPresets.SelectedItem.ToString() : txtRegexPattern.Text.Trim();
if (string.IsNullOrEmpty(url) || (string.IsNullOrEmpty(startPattern) && !useRegex))
{
LogMessage("Error: Please fill in all fields.", Color.Red);
return;
}
LogMessage($"Downloading source from: {url}...", Color.Cyan);
progressBar.Value = 0;
try
{
using (HttpClient client = new HttpClient())
{
string pageSource = await client.GetStringAsync(url);
LogMessage("Download successful!", Color.Green);
progressBar.Value = 50;
await ExtractAndSaveResultsAsync(pageSource, startPattern, endPattern, findAll, useRegex, regexPattern);
}
}
catch (Exception ex)
{
LogMessage($"Error downloading source: {ex.Message}", Color.Red);
}
finally
{
stopwatch.Stop(); // ✅ Stop elapsed time when the process is finished
}
}
handle extraction operation:
private async Task ExtractAndSaveResultsAsync(string source, string startPattern, string endPattern, bool findAll, bool useRegex, string regexPattern)
{
List<string> extractedResults = await Task.Run(() => Extractor.Extract(source, startPattern, endPattern, findAll, useRegex, regexPattern));
if (extractedResults.Count == 0)
{
LogMessage("No matches found!", Color.Red);
}
else
{
int count = extractedResults.Count;
int processed = 0;
foreach (var result in extractedResults)
{
LogMessage($"Extracted: {result}", Color.LightGreen);
// ✅ Update progress live
processed++;
lblAmountExtractedCounter.Invoke(new Action(() =>
{
lblAmountExtractedCounter.Text = processed.ToString();
}));
progressBar.Invoke(new Action(() =>
{
progressBar.Value = (int)((processed / (float)count) * 100);
}));
await Task.Delay(1); // ✅ Prevents UI freezing
}
}
if (chkSaveResults.Checked)
{
await Task.Run(() => Exporter.SaveResults(extractedResults, txtSavePath.Text, chkSaveAsTxt.Checked, chkSaveAsCsv.Checked));
LogMessage("Results saved successfully!", Color.Green);
}
progressBar.Invoke(new Action(() =>
{
progressBar.Value = 100;
}));
// ✅ Final log message: extracted count and timestamp
LogMessage($"Extraction Complete: {extractedResults.Count} items found | {DateTime.Now:yyyy-MM-dd HH:mm:ss}", Color.Cyan);
}
init the elapsed time:
private void InitElapsedTime()
{
stopwatch.Reset(); // ✅ Reset stopwatch to start from zero
stopwatch.Start(); // ✅ Start counting elapsed time immediately
lblTime.Invoke(new Action(() => lblTime.Text = "00:00:00")); // ✅ Force UI update immediately
timer.Interval = 1000; // ✅ Set 1-second update interval
timer.Tick -= timer_Tick; // ✅ Remove previous event handlers (prevent multiple events)
timer.Tick += timer_Tick; // ✅ Attach new event
timer.Start();
}
timer tick event:
void timer_Tick(object sender, EventArgs e)
{
UpdateText();
}
update text method:
void UpdateText()
{
if (stopwatch.IsRunning)
{
TimeSpan elapsed = stopwatch.Elapsed;
lblTime.Invoke(new Action(() =>
{
lblTime.Text = string.Format("{0:D2}:{1:D2}:{2:D2}",
elapsed.Hours, elapsed.Minutes, elapsed.Seconds);
}));
}
}
and last the log message method:
private void LogMessage(string message, Color color)
{
if (this.IsDisposed || !this.IsHandleCreated) return; // ✅ Prevent error if form is closing
try
{
rtbLogger.Invoke(new Action(() =>
{
if (this.IsDisposed || !this.IsHandleCreated) return; // ✅ Extra check
rtbLogger.SelectionStart = rtbLogger.TextLength;
rtbLogger.SelectionLength = 0;
rtbLogger.SelectionColor = color;
rtbLogger.AppendText($"{DateTime.Now:HH:mm:ss} - {message}\n");
rtbLogger.SelectionColor = rtbLogger.ForeColor;
}));
}
catch (ObjectDisposedException) { /* ✅ Ignore if form is already closed */ }
catch (InvalidOperationException) { /* ✅ Ignore invalid UI access */ }
}
Take a look at the following section:
processed++;
lblAmountExtractedCounter.Invoke(new Action(() =>
{
lblAmountExtractedCounter.Text = processed.ToString();
}));
progressBar.Invoke(new Action(() =>
{
progressBar.Value = (int)((processed / (float)count) * 100);
}));
await Task.Delay(1); // ✅ Prevents UI freezing
The point of await
is that anything after it will continue to run in the same context, i.e. the UI thread, so all controls can be accessed directly, no need for .Invoke
. The same with Windows.Forms.Timer, it also raises its events on the UI thread. So the .Invoke
does not do anything helpful, and may very well cause issues. The only code runnin in the background is Extractor.Extract
and that is not displayed. Given the problem description I would guess you are adding messages to the message queue faster than they can be processed, resulting in erratic behavior.
The entire loop seem rather pointless since it will not start to do anything until Extractor.Extract
has completed. So it will not report any meaningful progress. The correct way would be to create a Progress<T>
object, attach an event handler to it that updates the progress bar, and update the progress object from inside the Extractor.Extract
method.