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.
  • TextChanged fires on every keystroke — debounce with a System.Windows.Forms.Timer before hitting a database.
  • CancelEventArgs.Cancel on Validating prevents leaving the control; the matching Validated runs 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

MechanismBehaviourWhen to use
AnchorKeeps the control's distance to the chosen edges fixedFixed, form-like layouts where controls should stretch or stay put
DockGlues the control to an edge and fills along itToolbars, status bars, a grid that fills the remaining space
TableLayoutPanelGrid of rows and columns with percentage or absolute sizingForm fields arranged in aligned rows and columns
FlowLayoutPanelLays children out left to right, wrappingToolbars, tag chips, a wrapping row of buttons
SplitContainerTwo resizable panes with a draggable dividerMaster/detail panes
Panel with AutoScrollScrollable containerContent 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 void is acceptable only for event handlers, because the framework needs the void signature. Everywhere else return Task.
  • Touch a control's properties from the wrong thread and you get an InvalidOperationException at best, a corrupted handle at worst.
  • Prefer BeginInvoke for 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.

Forms and controls Data binding and when not to use WinForms

Last refreshed 2026-09-18.