dotnet-winforms · v1.0.0 · 2026-03-15 · sha256 5726e03ea0a59f1f

dotnet-winforms v1.0.0A

Immutable. This exact content is served forever at /api/v1/blob/5726e03ea0a59f1f.

---
name: dotnet-winforms
version: "1.0.0"
category: "Desktop"
description: "Build, maintain, or modernize Windows Forms applications with practical guidance on designer-driven UI, event handling, data binding, and migration to modern .NET."
compatibility: "Requires a Windows Forms project on .NET or .NET Framework."
---

# Windows Forms

## Trigger On

- working on Windows Forms UI, event-driven workflows, or classic LOB applications
- migrating WinForms from .NET Framework to modern .NET
- cleaning up oversized form code or designer coupling
- implementing data binding, validation, or control customization

## Documentation

- [Windows Forms Overview](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/overview/)
- [What's New in Windows Forms](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/whats-new/)
- [Data Binding Overview](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/how-to-bind-a-windows-forms-control-to-a-type)
- [Migration Guide](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/migration/)
- [Controls Reference](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/)

### References

- [patterns.md](references/patterns.md) - WinForms architectural patterns (MVP, MVVM, Passive View), data binding patterns, validation patterns, form communication, and threading patterns
- [migration.md](references/migration.md) - Step-by-step migration guide from .NET Framework to modern .NET, common issues, deployment options, and gradual migration strategies

## Workflow

1. **Respect designer boundaries** — avoid editing generated `.Designer.cs` code directly
2. **Separate business logic** — forms should orchestrate, not contain business rules
3. **Use consistent naming** — control naming and layout should be predictable
4. **Consider MVP/MVVM patterns** — even WinForms benefits from separation
5. **Validate at runtime** — designer success alone proves very little
6. **Modernize incrementally** — choose better structure before rewriting

## Project Structure

```
MyWinFormsApp/
├── MyWinFormsApp/
│   ├── Program.cs              # Application entry
│   ├── Forms/                  # Form classes
│   │   ├── MainForm.cs
│   │   └── MainForm.Designer.cs
│   ├── Presenters/             # MVP Presenters or ViewModels
│   ├── Models/                 # Domain models
│   ├── Services/               # Business logic
│   ├── Controls/               # Custom user controls
│   └── Resources/              # Images, strings, etc.
└── MyWinFormsApp.Tests/
```

## MVP Pattern (Model-View-Presenter)

### View Interface
```csharp
public interface ICustomerView
{
    string CustomerName { get; set; }
    string CustomerEmail { get; set; }
    BindingSource CustomersBindingSource { get; }

    event EventHandler LoadRequested;
    event EventHandler SaveRequested;
    event EventHandler<int> CustomerSelected;

    void ShowError(string message);
    void ShowSuccess(string message);
}
```

### Presenter
```csharp
public class CustomerPresenter
{
    private readonly ICustomerView _view;
    private readonly ICustomerService _service;

    public CustomerPresenter(ICustomerView view, ICustomerService service)
    {
        _view = view;
        _service = service;

        _view.LoadRequested += OnLoadRequested;
        _view.SaveRequested += OnSaveRequested;
        _view.CustomerSelected += OnCustomerSelected;
    }

    private async void OnLoadRequested(object? sender, EventArgs e)
    {
        try
        {
            var customers = await _service.GetAllAsync();
            _view.CustomersBindingSource.DataSource = customers;
        }
        catch (Exception ex)
        {
            _view.ShowError($"Failed to load: {ex.Message}");
        }
    }

    private async void OnSaveRequested(object? sender, EventArgs e)
    {
        try
        {
            var customer = new Customer
            {
                Name = _view.CustomerName,
                Email = _view.CustomerEmail
            };
            await _service.SaveAsync(customer);
            _view.ShowSuccess("Customer saved successfully");
        }
        catch (Exception ex)
        {
            _view.ShowError($"Failed to save: {ex.Message}");
        }
    }

    private async void OnCustomerSelected(object? sender, int customerId)
    {
        var customer = await _service.GetByIdAsync(customerId);
        if (customer != null)
        {
            _view.CustomerName = customer.Name;
            _view.CustomerEmail = customer.Email;
        }
    }
}
```

### Form Implementation
```csharp
public partial class CustomerForm : Form, ICustomerView
{
    private readonly CustomerPresenter _presenter;
    public BindingSource CustomersBindingSource { get; } = new();

    public string CustomerName
    {
        get => txtName.Text;
        set => txtName.Text = value;
    }

    public string CustomerEmail
    {
        get => txtEmail.Text;
        set => txtEmail.Text = value;
    }

    public event EventHandler? LoadRequested;
    public event EventHandler? SaveRequested;
    public event EventHandler<int>? CustomerSelected;

    public CustomerForm(ICustomerService service)
    {
        InitializeComponent();
        _presenter = new CustomerPresenter(this, service);

        dgvCustomers.DataSource = CustomersBindingSource;
        dgvCustomers.SelectionChanged += (s, e) =>
        {
            if (dgvCustomers.CurrentRow?.DataBoundItem is Customer c)
            {
                CustomerSelected?.Invoke(this, c.Id);
            }
        };
    }

    private void CustomerForm_Load(object sender, EventArgs e)
        => LoadRequested?.Invoke(this, EventArgs.Empty);

    private void btnSave_Click(object sender, EventArgs e)
        => SaveRequested?.Invoke(this, EventArgs.Empty);

    public void ShowError(string message)
        => MessageBox.Show(message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);

    public void ShowSuccess(string message)
        => MessageBox.Show(message, "Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
```

## Dependency Injection

```csharp
internal static class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();

        var services = new ServiceCollection();
        ConfigureServices(services);

        using var serviceProvider = services.BuildServiceProvider();
        var mainForm = serviceProvider.GetRequiredService<MainForm>();
        Application.Run(mainForm);
    }

    private static void ConfigureServices(IServiceCollection services)
    {
        // Services
        services.AddSingleton<ICustomerService, CustomerService>();
        services.AddSingleton<IOrderService, OrderService>();

        // Forms
        services.AddTransient<MainForm>();
        services.AddTransient<CustomerForm>();
        services.AddTransient<OrderForm>();
    }
}
```

## Data Binding

### BindingSource Pattern
```csharp
public partial class ProductForm : Form
{
    private readonly BindingSource _bindingSource = new();
    private readonly List<Product> _products;

    public ProductForm()
    {
        InitializeComponent();
        SetupBindings();
    }

    private void SetupBindings()
    {
        // Bind list to grid
        dgvProducts.DataSource = _bindingSource;

        // Bind current item to detail controls
        txtName.DataBindings.Add("Text", _bindingSource, "Name",
            true, DataSourceUpdateMode.OnPropertyChanged);
        txtPrice.DataBindings.Add("Text", _bindingSource, "Price",
            true, DataSourceUpdateMode.OnPropertyChanged, "0.00");

        // Enable/disable based on selection
        _bindingSource.CurrentChanged += (s, e) =>
        {
            btnEdit.Enabled = _bindingSource.Current != null;
            btnDelete.Enabled = _bindingSource.Current != null;
        };
    }

    private async Task LoadDataAsync()
    {
        var products = await _productService.GetAllAsync();
        _bindingSource.DataSource = new BindingList<Product>(products.ToList());
    }
}
```

### INotifyPropertyChanged Support
```csharp
public class Product : INotifyPropertyChanged
{
    private string _name = string.Empty;
    private decimal _price;

    public string Name
    {
        get => _name;
        set
        {
            if (_name != value)
            {
                _name = value;
                OnPropertyChanged();
            }
        }
    }

    public decimal Price
    {
        get => _price;
        set
        {
            if (_price != value)
            {
                _price = value;
                OnPropertyChanged();
            }
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string? name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}
```

## Validation

```csharp
public partial class CustomerForm : Form
{
    private readonly ErrorProvider _errorProvider = new();

    private void txtEmail_Validating(object sender, CancelEventArgs e)
    {
        if (!IsValidEmail(txtEmail.Text))
        {
            _errorProvider.SetError(txtEmail, "Invalid email address");
            e.Cancel = true;
        }
        else
        {
            _errorProvider.SetError(txtEmail, string.Empty);
        }
    }

    private void txtName_Validating(object sender, CancelEventArgs e)
    {
        if (string.IsNullOrWhiteSpace(txtName.Text))
        {
            _errorProvider.SetError(txtName, "Name is required");
            e.Cancel = true;
        }
        else
        {
            _errorProvider.SetError(txtName, string.Empty);
        }
    }

    private bool IsValidEmail(string email)
    {
        return !string.IsNullOrWhiteSpace(email) &&
               email.Contains('@') &&
               email.Contains('.');
    }

    private void btnSave_Click(object sender, EventArgs e)
    {
        if (ValidateChildren(ValidationConstraints.Enabled))
        {
            // All validations passed
            SaveCustomer();
        }
    }
}
```

## Async Operations

```csharp
public partial class DataForm : Form
{
    // Good: Use async/await properly
    private async void btnLoad_Click(object sender, EventArgs e)
    {
        btnLoad.Enabled = false;
        progressBar.Visible = true;

        try
        {
            var data = await LoadDataAsync();
            dgvData.DataSource = data;
        }
        catch (Exception ex)
        {
            MessageBox.Show($"Error: {ex.Message}");
        }
        finally
        {
            btnLoad.Enabled = true;
            progressBar.Visible = false;
        }
    }

    // Progress reporting
    private async void btnProcess_Click(object sender, EventArgs e)
    {
        var progress = new Progress<int>(percent =>
        {
            progressBar.Value = percent;
            lblStatus.Text = $"Processing: {percent}%";
        });

        await ProcessDataAsync(progress);
    }

    private async Task ProcessDataAsync(IProgress<int> progress)
    {
        for (int i = 0; i <= 100; i += 10)
        {
            await Task.Delay(100);
            progress.Report(i);
        }
    }
}
```

## .NET 8+ Features

```csharp
// Button commands (.NET 8+)
public partial class ModernForm : Form
{
    private readonly ICommand _saveCommand;

    public ModernForm()
    {
        InitializeComponent();

        _saveCommand = new RelayCommand(
            execute: _ => Save(),
            canExecute: _ => CanSave());

        // Bind command to button
        btnSave.Command = _saveCommand;
    }

    private bool CanSave() => !string.IsNullOrEmpty(txtName.Text);
    private void Save() { /* save logic */ }
}

// Modern system icons (.NET 8+)
var infoIcon = SystemIcons.GetStockIcon(StockIconId.Info, StockIconOptions.Large);
pictureBox.Image = infoIcon.ToBitmap();
```

## Anti-Patterns to Avoid

| Anti-Pattern | Why It's Bad | Better Approach |
|--------------|--------------|-----------------|
| Business logic in forms | Hard to test, tight coupling | Use MVP/Presenter pattern |
| Editing Designer.cs | Changes lost on regeneration | Modify in Form.cs only |
| Synchronous I/O in events | UI freezes | Use async/await |
| Giant form classes | Unmaintainable | Split into user controls |
| Direct database calls in forms | Coupling, hard to test | Use service layer |
| Ignoring validation events | Silent failures | Use ErrorProvider, Validating |
| Manual control population | Error-prone | Use data binding |
| Nested event handler logic | Spaghetti code | Extract to methods/services |

## Best Practices

1. **Use User Controls for reusable UI:**
   ```csharp
   public partial class AddressControl : UserControl
   {
       public string Street { get; set; }
       public string City { get; set; }
       public string ZipCode { get; set; }
   }
   ```

2. **Implement proper disposal:**
   ```csharp
   protected override void Dispose(bool disposing)
   {
       if (disposing)
       {
           _bindingSource?.Dispose();
           _errorProvider?.Dispose();
           components?.Dispose();
       }
       base.Dispose(disposing);
   }
   ```

3. **Use BindingList for observable collections:**
   ```csharp
   var bindingList = new BindingList<Product>(products);
   bindingList.ListChanged += (s, e) => UpdateStatus();
   dgvProducts.DataSource = bindingList;
   ```

4. **Handle high-DPI properly:**
   ```xml
   <!-- app.manifest -->
   <application xmlns="urn:schemas-microsoft-com:asm.v3">
     <windowsSettings>
       <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
       <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
     </windowsSettings>
   </application>
   ```

5. **Configure application settings properly:**
   ```csharp
   ApplicationConfiguration.Initialize(); // .NET 6+
   Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
   Application.EnableVisualStyles();
   Application.SetCompatibleTextRenderingDefault(false);
   ```

## Testing

```csharp
[Fact]
public async Task Presenter_LoadsCustomers_OnLoadRequested()
{
    var mockView = new Mock<ICustomerView>();
    var mockService = new Mock<ICustomerService>();
    var bindingSource = new BindingSource();

    mockView.Setup(v => v.CustomersBindingSource).Returns(bindingSource);
    mockService.Setup(s => s.GetAllAsync())
        .ReturnsAsync(new[] { new Customer { Name = "Test" } });

    var presenter = new CustomerPresenter(mockView.Object, mockService.Object);

    mockView.Raise(v => v.LoadRequested += null, EventArgs.Empty);

    await Task.Delay(100); // Allow async completion
    Assert.Single((IList<Customer>)bindingSource.DataSource);
}

[Fact]
public void Presenter_ShowsError_OnLoadFailure()
{
    var mockView = new Mock<ICustomerView>();
    var mockService = new Mock<ICustomerService>();
    var bindingSource = new BindingSource();

    mockView.Setup(v => v.CustomersBindingSource).Returns(bindingSource);
    mockService.Setup(s => s.GetAllAsync()).ThrowsAsync(new Exception("DB Error"));

    var presenter = new CustomerPresenter(mockView.Object, mockService.Object);
    mockView.Raise(v => v.LoadRequested += null, EventArgs.Empty);

    mockView.Verify(v => v.ShowError(It.IsAny<string>()), Times.Once);
}
```

## Deliver

- less brittle form code and event handling
- better separation between UI and business logic
- pragmatic modernization guidance for WinForms-heavy apps
- MVP pattern with testable presenters

## Validate

- designer files stay stable
- forms are not acting as the application service layer
- Windows-only runtime behavior is tested
- async operations do not block the UI
- validation is implemented consistently