
Winforms
- 21 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
winforms is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- winforms
- AI & Agent Building
- AI-coding skill
Winforms by the numbers
- 21 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #10,289 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill winformsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 21 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
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
Workflow
1. Respect designer boundaries — never edit .Designer.cs directly; changes are lost on regeneration. 2. Separate business logic from forms — use MVP (Model-View-Presenter) pattern. Forms orchestrate UI; presenters contain logic; services handle data access.
// View interface — forms implement this
public interface ICustomerView
{
string CustomerName { get; set; }
event EventHandler SaveRequested;
void ShowError(string message);
}
// Presenter — testable without UI
public class CustomerPresenter
{
private readonly ICustomerView _view;
private readonly ICustomerService _service;
public CustomerPresenter(ICustomerView view, ICustomerService service)
{
_view = view;
_service = service;
_view.SaveRequested += async (s, e) =>
{
try { await _service.SaveAsync(_view.CustomerName); }
catch (Exception ex) { _view.ShowError(ex.Message); }
};
}
}3. Use DI from Program.cs (.NET 6+):
var services = new ServiceCollection();
services.AddSingleton<ICustomerService, CustomerService>();
services.AddTransient<MainForm>();
using var sp = services.BuildServiceProvider();
Application.Run(sp.GetRequiredService<MainForm>());4. Use data binding via BindingSource and INotifyPropertyChanged instead of manual control population. See references/patterns.md for complete binding patterns. 5. Use async/await for I/O operations — disable controls during loading, use Progress<T> for progress reporting. Never block the UI thread. 6. Validate with `ErrorProvider` and the Validating event. Call ValidateChildren() before save operations. 7. Modernize incrementally — prefer better structure over big-bang rewrites. Use .NET 8+ features (button commands, stock icons) when available.
Current Upstream Notes
- The refreshed Windows Forms overview remains focused on Windows desktop, designer-driven controls, events, data binding, and migration to modern .NET. Keep WinForms guidance pragmatic: improve form boundaries and designer safety before proposing a framework rewrite.
- For docs-driven updates, validate whether the app targets .NET Framework, modern .NET, or mixed libraries before changing project format, designer files, or deployment assumptions.
flowchart LR
A["Form event"] --> B["Presenter handles logic"]
B --> C["Service layer / data access"]
C --> D["Update view via interface"]
D --> E["Validate and display results"]Key Decisions
| Decision | Guidance |
|---|---|
| MVP vs MVVM | Prefer MVP for WinForms — simpler with event-driven model |
| BindingSource vs manual | Always prefer BindingSource for list/detail binding |
| Sync vs async I/O | Always async — use async void only for event handlers |
| Custom controls | Extract reusable UserControl when form grows beyond ~300 lines |
| .NET Framework → .NET | Use the official migration guide; validate designer compatibility first |
Deliver
- less brittle form code with clear UI/logic separation
- MVP pattern with testable presenters
- pragmatic modernization guidance for WinForms-heavy apps
- data binding and validation patterns that reduce manual wiring
Validate
- designer files stay stable and are not hand-edited
- forms are not acting as the application service layer
- async operations do not block the UI thread
- validation is implemented consistently with ErrorProvider
- Windows-only runtime behavior is tested on target
References
- references/patterns.md - WinForms architectural patterns (MVP, MVVM, Passive View), data binding, validation, form communication, threading, DI setup, and .NET 8+ features
- references/migration.md - step-by-step migration from .NET Framework to modern .NET, common issues, deployment options, and gradual migration strategies
{
"version": "1.0.2",
"category": "Desktop",
"package_prefix": "Microsoft.WindowsDesktop.App.WindowsForms"
}
WinForms Migration to Modern .NET
Migration Overview
Migrating Windows Forms applications from .NET Framework to modern .NET (6, 7, 8, 9, 10) provides:
- Better performance and memory efficiency
- Access to modern C# language features
- Side-by-side deployment without system-wide runtime
- Continued support and security updates
- Access to new WinForms features
Prerequisites Assessment
Compatibility Analysis
Before migrating, analyze your application for compatibility:
# Install the .NET Upgrade Assistant
dotnet tool install -g upgrade-assistant
# Analyze project
upgrade-assistant analyze MyWinFormsApp.csproj
# Or run interactive upgrade
upgrade-assistant upgrade MyWinFormsApp.csprojCommon Blockers
| Blocker | Impact | Mitigation |
|---|---|---|
| WCF Client | Requires change | Use CoreWCF or gRPC |
| WCF Server | Not supported | Migrate to ASP.NET Core + gRPC |
| AppDomain | Limited support | Redesign with AssemblyLoadContext |
| Remoting | Not supported | Use gRPC or REST APIs |
| Code Access Security | Not supported | Remove or redesign |
| Windows Workflow Foundation | Not supported | Use Elsa or other workflow engine |
| Crystal Reports | May not work | Test or use alternative |
Check for Deprecated APIs
// These patterns indicate potential issues:
// App.config usage - may need migration
ConfigurationManager.AppSettings["MySetting"];
// System.Web references - not available
System.Web.HttpUtility.UrlEncode(value);
// Drawing.Common differences on non-Windows
System.Drawing.Image.FromFile(path);Project File Migration
Before (.NET Framework)
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{GUID-HERE}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>MyWinFormsApp</RootNamespace>
<AssemblyName>MyWinFormsApp</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<!-- Many more references -->
</ItemGroup>
<ItemGroup>
<Compile Include="Form1.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Form1.Designer.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
<!-- Many more compile items -->
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>After (Modern .NET SDK-Style)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
</ItemGroup>
</Project>Step-by-Step Migration
Step 1: Create New Project
# Create new WinForms project
dotnet new winforms -n MyWinFormsApp.Modern -f net9.0
# Or use specific template features
dotnet new winforms -n MyWinFormsApp.Modern --no-restoreStep 2: Copy Source Files
Copy these files from the old project:
- All
.csfiles (forms, classes, controls) - All
.resxfiles (resources) - All
.Designer.csfiles - Assets (images, icons, etc.)
Step 3: Update Program.cs
// .NET Framework style
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
// Modern .NET style
internal static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
Application.Run(new MainForm());
}
}
// Modern .NET with DI
internal static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
var host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
services.AddSingleton<MainForm>();
services.AddTransient<ICustomerService, CustomerService>();
})
.Build();
var mainForm = host.Services.GetRequiredService<MainForm>();
Application.Run(mainForm);
}
}Step 4: Update Configuration
Replace app.config with appsettings.json:
{
"ConnectionStrings": {
"Default": "Server=...;Database=...;"
},
"AppSettings": {
"MaxRetries": 3,
"TimeoutSeconds": 30
}
}// Reading configuration
public class AppConfig
{
private readonly IConfiguration _configuration;
public AppConfig()
{
_configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")}.json", optional: true)
.Build();
}
public string ConnectionString => _configuration.GetConnectionString("Default")!;
public int MaxRetries => _configuration.GetValue<int>("AppSettings:MaxRetries");
}Step 5: Update NuGet References
Replace packages.config with PackageReference:
<!-- Old packages.config style -->
<packages>
<package id="Newtonsoft.Json" version="13.0.1" targetFramework="net48" />
<package id="Dapper" version="2.0.123" targetFramework="net48" />
</packages>
<!-- New PackageReference style in .csproj -->
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Dapper" Version="2.1.35" />
</ItemGroup>Step 6: Handle API Differences
// BinaryFormatter - no longer recommended, use alternatives
// Old
var formatter = new BinaryFormatter();
formatter.Serialize(stream, obj);
// New - use System.Text.Json or other serializers
var json = JsonSerializer.Serialize(obj);
await File.WriteAllTextAsync(path, json);
// System.Drawing differences
// Old - worked everywhere
using var bitmap = new Bitmap(path);
// New - Windows-only by default, use SkiaSharp for cross-platform
// Or add package reference:
// <PackageReference Include="System.Drawing.Common" Version="8.0.0" />Step 7: Update Assembly Info
Remove AssemblyInfo.cs and use project properties:
<PropertyGroup>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<Version>1.0.0</Version>
<Company>My Company</Company>
<Product>My WinForms App</Product>
<Copyright>Copyright 2024</Copyright>
</PropertyGroup>Common Migration Issues
Designer Issues
// Issue: Designer fails to load after migration
// Solution: Ensure all dependencies are available and rebuild
// Issue: User controls not showing in toolbox
// Solution: Build solution, then refresh toolbox
// Issue: Resources not loading
// Solution: Ensure .resx files have correct build action<!-- Ensure resources are embedded -->
<ItemGroup>
<EmbeddedResource Update="Form1.resx">
<DependentUpon>Form1.cs</DependentUpon>
</EmbeddedResource>
</ItemGroup>Third-Party Controls
// Check compatibility before migration
// Many vendors provide .NET 6+ compatible versions
// DevExpress, Telerik, Infragistics, etc. - check vendor documentation
// Older/abandoned controls - may need replacement
// If control source is available, consider migrating it too
// Or replace with:
// - Built-in .NET controls
// - Open-source alternatives (be mindful of licensing)
// - Custom implementationsDatabase Access
// Entity Framework 6 to EF Core
// Old (EF6)
using (var context = new MyDbContext())
{
var customers = context.Customers.Where(c => c.IsActive).ToList();
}
// New (EF Core)
await using var context = new MyDbContext();
var customers = await context.Customers
.Where(c => c.IsActive)
.ToListAsync();WCF Client Migration
// Option 1: Use System.ServiceModel packages
// <PackageReference Include="System.ServiceModel.Http" Version="6.0.0" />
// Option 2: Generate new client
// dotnet-svcutil https://service.example.com/MyService?wsdl
// Option 3: Replace with HTTP client for REST services
public class MyServiceClient
{
private readonly HttpClient _client;
public async Task<Customer> GetCustomerAsync(int id)
{
var response = await _client.GetAsync($"api/customers/{id}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Customer>();
}
}High-DPI and Modern Features
Enable High-DPI Support
// In Program.cs (already included in ApplicationConfiguration.Initialize())
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);<!-- 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>Use New .NET 8/9 Features
// Button commands (.NET 8+)
btnSave.Command = new RelayCommand(Save, CanSave);
// System icons (.NET 8+)
var icon = SystemIcons.GetStockIcon(StockIconId.Info);
// Improved data binding (.NET 9+)
// Better performance and memory usage
// FolderBrowserDialog improvements
using var dialog = new FolderBrowserDialog
{
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
ShowNewFolderButton = true,
UseDescriptionForTitle = true,
Description = "Select output folder"
};Testing After Migration
Functional Testing Checklist
- [ ] Application launches without errors
- [ ] All forms open correctly
- [ ] Designer loads all forms
- [ ] Data binding works correctly
- [ ] Validation behaves as expected
- [ ] Database operations work
- [ ] File operations work
- [ ] Printing works (if applicable)
- [ ] Third-party controls function
- [ ] Resources (images, icons) load
- [ ] Localization works (if applicable)
- [ ] High-DPI displays correctly
- [ ] Keyboard shortcuts work
- [ ] Tab order is correct
Performance Testing
// Basic startup timing
var sw = Stopwatch.StartNew();
Application.Run(new MainForm());
Console.WriteLine($"Startup: {sw.ElapsedMilliseconds}ms");
// Memory usage comparison
// Use dotnet-counters or Visual Studio diagnostics
// dotnet-counters monitor --process-id <PID>Deployment
Framework-Dependent Deployment
# Requires .NET runtime on target machine
dotnet publish -c Release -r win-x64 --self-contained falseSelf-Contained Deployment
# Includes runtime, larger but no dependencies
dotnet publish -c Release -r win-x64 --self-contained true
# Single file (recommended for distribution)
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
# Trimmed (smaller size, test thoroughly)
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishTrimmed=true<!-- Project settings for publishing -->
<PropertyGroup>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
</PropertyGroup>Gradual Migration Strategy
For large applications, consider incremental migration:
1. Shared Library Approach
Solution/
├── MyApp.Core/ # .NET Standard 2.0 - shared
│ ├── Models/
│ ├── Services/
│ └── Interfaces/
├── MyApp.WinForms.Legacy/ # .NET Framework 4.8 - old UI
│ └── References MyApp.Core
├── MyApp.WinForms.Modern/ # .NET 9 - new UI
│ └── References MyApp.Core2. Feature-by-Feature Migration
1. Migrate shared business logic to .NET Standard 2. Create new modern .NET WinForms project 3. Migrate forms one at a time 4. Test each migrated form thoroughly 5. Retire old project when complete
3. Side-by-Side Development
// Multi-targeting for shared code
<PropertyGroup>
<TargetFrameworks>net48;net9.0-windows</TargetFrameworks>
</PropertyGroup>
// Conditional compilation when needed
#if NET48
// .NET Framework specific code
#else
// Modern .NET code
#endifResources
WinForms Patterns Reference
Architectural Patterns
MVP (Model-View-Presenter)
MVP is the recommended pattern for WinForms applications that need testability and separation of concerns.
Structure:
- Model: Domain entities and business logic
- View: Form implementing an interface, handles UI concerns only
- Presenter: Mediates between Model and View, contains presentation logic
Key Characteristics:
- View is passive and raises events
- Presenter subscribes to view events and updates view properties
- Presenter can be unit tested without UI
- View interface enables mocking
// View contract
public interface IOrderView
{
int OrderId { get; set; }
string CustomerName { get; set; }
decimal Total { get; set; }
IEnumerable<OrderLine> Lines { set; }
event EventHandler LoadRequested;
event EventHandler SaveRequested;
event EventHandler CancelRequested;
void Close();
void ShowValidationError(string field, string message);
void ClearValidationErrors();
}
// Presenter
public class OrderPresenter
{
private readonly IOrderView _view;
private readonly IOrderRepository _repository;
private Order? _currentOrder;
public OrderPresenter(IOrderView view, IOrderRepository repository)
{
_view = view;
_repository = repository;
_view.LoadRequested += async (s, e) => await LoadOrderAsync();
_view.SaveRequested += async (s, e) => await SaveOrderAsync();
_view.CancelRequested += (s, e) => _view.Close();
}
private async Task LoadOrderAsync()
{
_currentOrder = await _repository.GetByIdAsync(_view.OrderId);
if (_currentOrder != null)
{
_view.CustomerName = _currentOrder.CustomerName;
_view.Total = _currentOrder.Total;
_view.Lines = _currentOrder.Lines;
}
}
private async Task SaveOrderAsync()
{
_view.ClearValidationErrors();
if (string.IsNullOrWhiteSpace(_view.CustomerName))
{
_view.ShowValidationError("CustomerName", "Customer name is required");
return;
}
if (_currentOrder != null)
{
_currentOrder.CustomerName = _view.CustomerName;
await _repository.SaveAsync(_currentOrder);
_view.Close();
}
}
}MVVM (Model-View-ViewModel)
MVVM can be used in WinForms with data binding, though it is more common in WPF. Use when:
- Heavy data binding requirements
- Sharing ViewModels between WinForms and WPF
- Team is familiar with MVVM from other frameworks
public class OrderViewModel : INotifyPropertyChanged
{
private string _customerName = string.Empty;
private decimal _total;
private bool _isBusy;
public string CustomerName
{
get => _customerName;
set { _customerName = value; OnPropertyChanged(); }
}
public decimal Total
{
get => _total;
set { _total = value; OnPropertyChanged(); }
}
public bool IsBusy
{
get => _isBusy;
set { _isBusy = value; OnPropertyChanged(); OnPropertyChanged(nameof(IsNotBusy)); }
}
public bool IsNotBusy => !IsBusy;
public ICommand SaveCommand { get; }
public ICommand LoadCommand { get; }
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}Passive View
A stricter variant of MVP where the view contains zero logic:
- All decisions made by presenter
- View only exposes properties and events
- Maximum testability, minimum view code
// Passive view - no logic at all
public partial class CustomerForm : Form, ICustomerView
{
public string FirstName { get => txtFirstName.Text; set => txtFirstName.Text = value; }
public string LastName { get => txtLastName.Text; set => txtLastName.Text = value; }
public bool SaveEnabled { get => btnSave.Enabled; set => btnSave.Enabled = value; }
public event EventHandler? FirstNameChanged;
public event EventHandler? LastNameChanged;
public event EventHandler? SaveClicked;
public CustomerForm()
{
InitializeComponent();
txtFirstName.TextChanged += (s, e) => FirstNameChanged?.Invoke(this, e);
txtLastName.TextChanged += (s, e) => LastNameChanged?.Invoke(this, e);
btnSave.Click += (s, e) => SaveClicked?.Invoke(this, e);
}
}
// Presenter controls everything
public class CustomerPresenter
{
private readonly ICustomerView _view;
public CustomerPresenter(ICustomerView view)
{
_view = view;
_view.SaveEnabled = false;
_view.FirstNameChanged += (s, e) => UpdateSaveEnabled();
_view.LastNameChanged += (s, e) => UpdateSaveEnabled();
}
private void UpdateSaveEnabled()
{
_view.SaveEnabled = !string.IsNullOrWhiteSpace(_view.FirstName)
&& !string.IsNullOrWhiteSpace(_view.LastName);
}
}Data Binding Patterns
Master-Detail Binding
Common pattern for list-detail UIs:
public partial class MasterDetailForm : Form
{
private readonly BindingSource _masterSource = new();
private readonly BindingSource _detailSource = new();
public MasterDetailForm()
{
InitializeComponent();
// Link detail to master
_detailSource.DataSource = _masterSource;
_detailSource.DataMember = "OrderLines"; // Navigation property
dgvOrders.DataSource = _masterSource;
dgvOrderLines.DataSource = _detailSource;
// Detail controls bind to detail source
txtLineDescription.DataBindings.Add("Text", _detailSource, "Description");
txtLineQuantity.DataBindings.Add("Text", _detailSource, "Quantity");
}
private async Task LoadAsync()
{
var orders = await _orderService.GetAllWithLinesAsync();
_masterSource.DataSource = new BindingList<Order>(orders.ToList());
}
}Two-Way Binding with Validation
public partial class EditForm : Form
{
private readonly BindingSource _bindingSource = new();
private readonly ErrorProvider _errorProvider = new();
private void SetupBindings(Customer customer)
{
_bindingSource.DataSource = customer;
// Two-way binding with format and parse
var nameBinding = new Binding("Text", _bindingSource, "Name", true);
nameBinding.Format += (s, e) => e.Value = e.Value?.ToString()?.Trim();
nameBinding.Parse += (s, e) => e.Value = e.Value?.ToString()?.Trim();
txtName.DataBindings.Add(nameBinding);
// Binding with null handling
txtEmail.DataBindings.Add("Text", _bindingSource, "Email",
true, DataSourceUpdateMode.OnPropertyChanged, string.Empty);
// Checkbox binding
chkActive.DataBindings.Add("Checked", _bindingSource, "IsActive",
true, DataSourceUpdateMode.OnPropertyChanged);
// ComboBox binding
cboCategory.DataSource = _categories;
cboCategory.DisplayMember = "Name";
cboCategory.ValueMember = "Id";
cboCategory.DataBindings.Add("SelectedValue", _bindingSource, "CategoryId");
}
}Observable Collection Pattern
public class ObservableList<T> : BindingList<T>
{
private bool _raiseListChangedEvents = true;
public void AddRange(IEnumerable<T> items)
{
_raiseListChangedEvents = false;
try
{
foreach (var item in items)
{
Add(item);
}
}
finally
{
_raiseListChangedEvents = true;
ResetBindings();
}
}
protected override void OnListChanged(ListChangedEventArgs e)
{
if (_raiseListChangedEvents)
{
base.OnListChanged(e);
}
}
}Validation Patterns
Centralized Validation
public class FormValidator
{
private readonly ErrorProvider _errorProvider;
private readonly Dictionary<Control, Func<string?>> _validators = new();
public FormValidator(Form form)
{
_errorProvider = new ErrorProvider(form);
_errorProvider.BlinkStyle = ErrorBlinkStyle.NeverBlink;
}
public void AddRule(Control control, Func<string?> validator)
{
_validators[control] = validator;
control.Validating += (s, e) =>
{
var error = validator();
_errorProvider.SetError(control, error ?? string.Empty);
if (!string.IsNullOrEmpty(error))
{
e.Cancel = true;
}
};
}
public bool ValidateAll()
{
var isValid = true;
foreach (var kvp in _validators)
{
var error = kvp.Value();
_errorProvider.SetError(kvp.Key, error ?? string.Empty);
if (!string.IsNullOrEmpty(error))
{
isValid = false;
}
}
return isValid;
}
public void ClearAll()
{
foreach (var control in _validators.Keys)
{
_errorProvider.SetError(control, string.Empty);
}
}
}
// Usage
public partial class CustomerForm : Form
{
private readonly FormValidator _validator;
public CustomerForm()
{
InitializeComponent();
_validator = new FormValidator(this);
_validator.AddRule(txtName, () =>
string.IsNullOrWhiteSpace(txtName.Text) ? "Name is required" : null);
_validator.AddRule(txtEmail, () =>
!txtEmail.Text.Contains('@') ? "Invalid email format" : null);
_validator.AddRule(txtAge, () =>
!int.TryParse(txtAge.Text, out var age) || age < 0 || age > 150
? "Age must be between 0 and 150" : null);
}
private void btnSave_Click(object sender, EventArgs e)
{
if (_validator.ValidateAll())
{
SaveCustomer();
}
}
}IDataErrorInfo Validation
public class Customer : IDataErrorInfo, INotifyPropertyChanged
{
private string _name = string.Empty;
private string _email = string.Empty;
public string Name
{
get => _name;
set { _name = value; OnPropertyChanged(); }
}
public string Email
{
get => _email;
set { _email = value; OnPropertyChanged(); }
}
// IDataErrorInfo implementation
public string Error => string.Empty;
public string this[string columnName]
{
get
{
return columnName switch
{
nameof(Name) when string.IsNullOrWhiteSpace(Name) => "Name is required",
nameof(Email) when !string.IsNullOrEmpty(Email) && !Email.Contains('@') => "Invalid email",
_ => string.Empty
};
}
}
public bool IsValid => string.IsNullOrEmpty(this[nameof(Name)])
&& string.IsNullOrEmpty(this[nameof(Email)]);
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}Form Communication Patterns
Mediator Pattern
For complex multi-form coordination:
public interface IFormMediator
{
void Register<TMessage>(Action<TMessage> handler);
void Send<TMessage>(TMessage message);
}
public class FormMediator : IFormMediator
{
private readonly Dictionary<Type, List<Delegate>> _handlers = new();
public void Register<TMessage>(Action<TMessage> handler)
{
var type = typeof(TMessage);
if (!_handlers.ContainsKey(type))
{
_handlers[type] = new List<Delegate>();
}
_handlers[type].Add(handler);
}
public void Send<TMessage>(TMessage message)
{
var type = typeof(TMessage);
if (_handlers.TryGetValue(type, out var handlers))
{
foreach (var handler in handlers.Cast<Action<TMessage>>())
{
handler(message);
}
}
}
}
// Messages
public record CustomerSelectedMessage(int CustomerId);
public record CustomerUpdatedMessage(Customer Customer);
// Usage
public partial class CustomerListForm : Form
{
private readonly IFormMediator _mediator;
public CustomerListForm(IFormMediator mediator)
{
_mediator = mediator;
dgvCustomers.SelectionChanged += (s, e) =>
{
if (dgvCustomers.CurrentRow?.DataBoundItem is Customer c)
{
_mediator.Send(new CustomerSelectedMessage(c.Id));
}
};
}
}
public partial class CustomerDetailForm : Form
{
private readonly IFormMediator _mediator;
public CustomerDetailForm(IFormMediator mediator)
{
_mediator = mediator;
_mediator.Register<CustomerSelectedMessage>(msg => LoadCustomer(msg.CustomerId));
}
}Parent-Child Form Pattern
public partial class MainForm : Form
{
public void OpenCustomerEditor(Customer customer)
{
using var editor = new CustomerEditorForm(customer);
editor.CustomerSaved += OnCustomerSaved;
if (editor.ShowDialog(this) == DialogResult.OK)
{
RefreshCustomerList();
}
}
private void OnCustomerSaved(object? sender, CustomerSavedEventArgs e)
{
// Handle save notification
statusLabel.Text = $"Customer {e.Customer.Name} saved";
}
}
public partial class CustomerEditorForm : Form
{
public event EventHandler<CustomerSavedEventArgs>? CustomerSaved;
private readonly Customer _customer;
public CustomerEditorForm(Customer customer)
{
InitializeComponent();
_customer = customer;
BindCustomer();
}
private void btnSave_Click(object sender, EventArgs e)
{
if (ValidateChildren())
{
UpdateCustomerFromControls();
CustomerSaved?.Invoke(this, new CustomerSavedEventArgs(_customer));
DialogResult = DialogResult.OK;
Close();
}
}
}
public class CustomerSavedEventArgs : EventArgs
{
public Customer Customer { get; }
public CustomerSavedEventArgs(Customer customer) => Customer = customer;
}Threading Patterns
Safe UI Updates
public partial class DataForm : Form
{
private readonly SynchronizationContext _syncContext;
public DataForm()
{
InitializeComponent();
_syncContext = SynchronizationContext.Current!;
}
private async Task ProcessInBackgroundAsync()
{
// Start background work
var data = await Task.Run(() => LoadExpensiveData());
// Already on UI thread due to await in WinForms context
dgvData.DataSource = data;
}
// For fire-and-forget or manual threading
private void StartBackgroundWork()
{
Task.Run(() =>
{
var result = DoWork();
// Post back to UI thread
_syncContext.Post(_ =>
{
lblResult.Text = result;
}, null);
});
}
// Extension method approach
private void UpdateStatusSafe(string status)
{
if (InvokeRequired)
{
Invoke(() => UpdateStatusSafe(status));
return;
}
lblStatus.Text = status;
}
}Cancellation Pattern
public partial class LongOperationForm : Form
{
private CancellationTokenSource? _cts;
private async void btnStart_Click(object sender, EventArgs e)
{
_cts = new CancellationTokenSource();
btnStart.Enabled = false;
btnCancel.Enabled = true;
try
{
await ProcessDataAsync(_cts.Token);
MessageBox.Show("Completed");
}
catch (OperationCanceledException)
{
MessageBox.Show("Cancelled");
}
finally
{
btnStart.Enabled = true;
btnCancel.Enabled = false;
_cts.Dispose();
_cts = null;
}
}
private void btnCancel_Click(object sender, EventArgs e)
{
_cts?.Cancel();
}
private async Task ProcessDataAsync(CancellationToken ct)
{
var items = await GetItemsAsync();
var progress = new Progress<int>(p => progressBar.Value = p);
for (int i = 0; i < items.Count; i++)
{
ct.ThrowIfCancellationRequested();
await ProcessItemAsync(items[i]);
((IProgress<int>)progress).Report((i + 1) * 100 / items.Count);
}
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (_cts != null)
{
_cts.Cancel();
e.Cancel = true; // Prevent close until operation stops
// Or: wait for cancellation to complete before allowing close
}
base.OnFormClosing(e);
}
}