See also Cross-platform Driver Model
Terminal.Gui applications run on a single main thread with an event loop that processes keyboard, mouse, and system events. This document explains how to properly handle background work, timers, and asynchronous operations while keeping your UI responsive.
Terminal.Gui follows the standard UI toolkit pattern where all UI operations must happen on the main thread. Attempting to modify views or their properties from background threads will result in undefined behavior and potential crashes.
Always use
App?.Invoke()(from within a View) orapp.Invoke()(with an IApplication instance) to update the UI from background threads.
The preferred way to handle background work is using C#'s async/await pattern:
private async void LoadDataButton_Clicked()
{
loadButton.Enabled = false;
statusLabel.Text = "Loading...";
try
{
// This runs on a background thread
var data = await FetchDataFromApiAsync();
// This automatically returns to the main thread
dataView.LoadData(data);
statusLabel.Text = $"Loaded {data.Count} items";
}
catch (Exception ex)
{
statusLabel.Text = $"Error: {ex.Message}";
}
finally
{
loadButton.Enabled = true;
}
}
When working with traditional threading APIs or when async/await isn't suitable:
From within a View (recommended):
private void StartBackgroundWork()
{
Task.Run(() =>
{
// This code runs on a background thread
for (int i = 0; i <= 100; i++)
{
Thread.Sleep(50); // Simulate work
// Marshal back to main thread for UI updates
App?.Invoke(() =>
{
progressBar.Fraction = i / 100f;
statusLabel.Text = $"Progress: {i}%";
});
}
App?.Invoke(() =>
{
statusLabel.Text = "Complete!";
});
});
}
Using IApplication instance:
private void StartBackgroundWork(IApplication app)
{
Task.Run(() =>
{
// This code runs on a background thread
for (int i = 0; i <= 100; i++)
{
Thread.Sleep(50); // Simulate work
// Marshal back to main thread for UI updates
app.Invoke(() =>
{
progressBar.Fraction = i / 100f;
statusLabel.Text = $"Progress: {i}%";
});
}
app.Invoke(() =>
{
statusLabel.Text = "Complete!";
});
});
}
Use timers for periodic updates like clocks, status refreshes, or animations:
public class ClockView : View
{
private Label timeLabel;
private object timerToken;
public ClockView()
{
timeLabel = new Label { Text = DateTime.Now.ToString("HH:mm:ss") };
Add(timeLabel);
// Update every second using the View's App property
timerToken = App?.AddTimeout(
TimeSpan.FromSeconds(1),
UpdateTime
);
}
private bool UpdateTime()
{
timeLabel.Text = DateTime.Now.ToString("HH:mm:ss");
return true; // Continue timer
}
protected override void Dispose(bool disposing)
{
if (disposing && timerToken != null)
{
App?.RemoveTimeout(timerToken);
}
base.Dispose(disposing);
}
}
true from timer callbacks to continue, false to stopprivate async void ProcessFiles()
{
var files = Directory.GetFiles(folderPath);
progressBar.Fraction = 0;
for (int i = 0; i < files.Length; i++)
{
await ProcessFileAsync(files[i]);
// Update progress on main thread
progressBar.Fraction = (float)(i + 1) / files.Length;
statusLabel.Text = $"Processed {i + 1} of {files.Length} files";
// Allow UI to update
await Task.Yield();
}
}
private CancellationTokenSource cancellationSource;
private async void StartLongOperation()
{
cancellationSource = new CancellationTokenSource();
cancelButton.Enabled = true;
try
{
await LongRunningOperationAsync(cancellationSource.Token);
statusLabel.Text = "Operation completed";
}
catch (OperationCanceledException)
{
statusLabel.Text = "Operation cancelled";
}
finally
{
cancelButton.Enabled = false;
}
}
private void CancelButton_Clicked()
{
cancellationSource?.Cancel();
}
private async void ProcessLargeDataset()
{
var data = GetLargeDataset();
var batchSize = 100;
for (int i = 0; i < data.Count; i += batchSize)
{
// Process a batch
var batch = data.Skip(i).Take(batchSize);
ProcessBatch(batch);
// Update UI and yield control
progressBar.Fraction = (float)i / data.Count;
await Task.Yield(); // Allows UI events to process
}
}
Task.Run(() =>
{
label.Text = "This will crash!"; // Wrong!
});
Task.Run(() =>
{
// From within a View:
App?.Invoke(() =>
{
label.Text = "This is safe!"; // Correct!
});
// Or with IApplication instance:
// app.Invoke(() => { label.Text = "This is safe!"; });
});
// Memory leak - timer keeps running after view is disposed
// From within a View:
App?.AddTimeout(TimeSpan.FromSeconds(1), UpdateStatus);
// Or with IApplication instance:
app.AddTimeout(TimeSpan.FromSeconds(1), UpdateStatus);
protected override void Dispose(bool disposing)
{
if (disposing && timerToken != null)
{
// From within a View, use App property
App?.RemoveTimeout(timerToken);
// Or with IApplication instance:
// app.RemoveTimeout(timerToken);
}
base.Dispose(disposing);
}
await Task.Yield()ConfigureAwait(false) for non-UI async operations