Events and layout
Wiring handlers, the Dock and Anchor rules that keep a resizable form sane, and updating the UI from a worker thread.
Events and handlers
A WinForms event is a multicast delegate. The standard handler shape receives the sender and an EventArgs-derived object, which is how one handler can serve several controls.
// attach in code (the designer emits the same statement)
searchButton.Click += SearchButton_Click;
searchBox.KeyDown += SearchBox_KeyDown;
private void SearchButton_Click(object? sender, EventArgs e)
{
RunSearch(searchBox.Text);
}
// one handler, several senders
private void Category_Click(object? sender, EventArgs e)
{
if (sender is not Button button) return;
LoadCategory(button.Tag as string ?? "all");
}
// keyboard and validation events
private void SearchBox_KeyDown(object? sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
e.SuppressKeyPress = true; // stops the Windows beep
RunSearch(searchBox.Text);
}
}
private void AmountBox_Validating(object? sender, CancelEventArgs e)
{
if (!decimal.TryParse(amountBox.Text, out _))
{
errorProvider1.SetError(amountBox, "Enter a number");
e.Cancel = true; // keeps focus in the control
}
else
{
errorProvider1.SetError(amountBox, string.Empty);
}
}- Always unsubscribe long-lived publishers from short-lived subscribers, or the delegate keeps the subscriber alive and leaks the form.
TextChangedfires on every keystroke — debounce with aSystem.Windows.Forms.Timerbefore hitting a database.CancelEventArgs.CancelonValidatingprevents leaving the control; the matchingValidatedruns only on success.- Handler exceptions on the UI thread terminate the app. Wrap the body or subscribe to
Application.ThreadException.
Dock, Anchor and layout panels
| Mechanism | Behaviour | When to use |
|---|---|---|
Anchor | Keeps the control's distance to the chosen edges fixed | Fixed, form-like layouts where controls should stretch or stay put |
Dock | Glues the control to an edge and fills along it | Toolbars, status bars, a grid that fills the remaining space |
TableLayoutPanel | Grid of rows and columns with percentage or absolute sizing | Form fields arranged in aligned rows and columns |
FlowLayoutPanel | Lays children out left to right, wrapping | Toolbars, tag chips, a wrapping row of buttons |
SplitContainer | Two resizable panes with a draggable divider | Master/detail panes |
Panel with AutoScroll | Scrollable container | Content larger than the visible area |
var layout = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 2,
RowCount = 3,
Padding = new Padding(12)
};
layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100f));
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100f));
layout.Controls.Add(new Label { Text = "Name", AutoSize = true }, 0, 0);
layout.Controls.Add(nameBox, 1, 0);
layout.Controls.Add(new Label { Text = "Notes", AutoSize = true }, 0, 1);
layout.Controls.Add(notesBox, 1, 1);
layout.Controls.Add(resultsGrid, 1, 2);
Controls.Add(layout);⚠️
Avoid absolute
Location and Size for anything a user can resize. The window will be resized, text will be scaled to 150 percent, and a pixel-perfect layout becomes overlapping controls that reviewers only discover on their own machine.Keeping the UI responsive
Only the thread that created a control may touch it. The message loop is that thread, so any long operation must run elsewhere and marshal its updates back with Invoke or BeginInvoke.
private async void LoadButton_Click(object? sender, EventArgs e)
{
loadButton.Enabled = false;
progressBar.Style = ProgressBarStyle.Marquee;
try
{
// await resumes on the UI thread because a SynchronizationContext is installed
var rows = await Task.Run(() => repository.LoadAll());
resultsGrid.DataSource = rows;
}
catch (Exception ex)
{
MessageBox.Show(this, ex.Message, "Load failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
progressBar.Style = ProgressBarStyle.Blocks;
loadButton.Enabled = true;
}
}
// from a non-async worker thread, marshal explicitly
void ReportProgress(int percent)
{
if (progressBar.InvokeRequired)
progressBar.BeginInvoke(() => progressBar.Value = percent);
else
progressBar.Value = percent;
}async voidis acceptable only for event handlers, because the framework needs the void signature. Everywhere else returnTask.- Touch a control's properties from the wrong thread and you get an
InvalidOperationExceptionat best, a corrupted handle at worst. - Prefer
BeginInvokefor progress updates: it queues and returns instead of blocking the worker. - Disable the button during the operation. Users click twice, and the second click arrives before your guard variable is set.
FAQ
Dock or Anchor?
Use
Dock for elements tied to an edge — toolbars, status bars, a grid filling the remainder. Use Anchor when a control should keep its position relative to specific edges. Do not mix both on the same control.Why does my form freeze while loading data?
The work is running on the UI thread and blocking the message loop. Move it to
Task.Run and await it, or use a background worker that marshals updates back through Invoke.Related
Forms and controls Data binding and when not to use WinForms
Last refreshed 2026-09-18.