Claude
Skill
dotnet-winforms
Build, maintain, or modernize Windows Forms applications with practical guidance on designer-driven UI, event handling, data binding, MVP separation, and migration to modern .NET. Use when working on WinForms projects or migrating from .NET Framework.
Virus-scanned
Reviewed automatically before listing.
Download
postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-winforms-bfa4ebd.zip · 11 KB
Install
skills CLI
npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-winforms
Claude Code
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
Git
git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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
- Respect designer boundaries — never edit
.Designer.csdirectly; changes are lost on regeneration. - 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); } }; } } - 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>()); - Use data binding via
BindingSourceandINotifyPropertyChangedinstead of manual control population. See references/patterns.md for complete binding patterns. - Use async/await for I/O operations — disable controls during loading, use
Progress<T>for progress reporting. Never block the UI thread. - Validate with
ErrorProviderand theValidatingevent. CallValidateChildren()before save operations. - Modernize incrementally — prefer better structure over big-bang rewrites. Use .NET 8+ features (button commands, stock icons) when available.
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
Files (dotnet-skills)
-
references
-
migration.md 13.3 KB
# 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: ```bash # 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.csproj ``` ### Common 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 ```csharp // 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 <?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) ```xml <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 ```bash # 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-restore ``` ### Step 2: Copy Source Files Copy these files from the old project: - All `.cs` files (forms, classes, controls) - All `.resx` files (resources) - All `.Designer.cs` files - Assets (images, icons, etc.) ### Step 3: Update Program.cs ```csharp // .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`: ```json { "ConnectionStrings": { "Default": "Server=...;Database=...;" }, "AppSettings": { "MaxRetries": 3, "TimeoutSeconds": 30 } } ``` ```csharp // 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: ```xml <!-- 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 ```csharp // 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: ```xml <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 ```csharp // 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 ``` ```xml <!-- Ensure resources are embedded --> <ItemGroup> <EmbeddedResource Update="Form1.resx"> <DependentUpon>Form1.cs</DependentUpon> </EmbeddedResource> </ItemGroup> ``` ### Third-Party Controls ```csharp // 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 implementations ``` ### Database Access ```csharp // 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 ```csharp // 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 ```csharp // In Program.cs (already included in ApplicationConfiguration.Initialize()) Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); ``` ```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> ``` ### Use New .NET 8/9 Features ```csharp // 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 ```csharp // 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 ```bash # Requires .NET runtime on target machine dotnet publish -c Release -r win-x64 --self-contained false ``` ### Self-Contained Deployment ```bash # 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 ``` ```xml <!-- 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 ```text 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.Core ``` ### 2. 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 ```csharp // 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 #endif ``` ## Resources - [Official Migration Guide](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/migration/) - [.NET Upgrade Assistant](https://learn.microsoft.com/en-us/dotnet/core/porting/upgrade-assistant-overview) - [Breaking Changes](https://learn.microsoft.com/en-us/dotnet/core/compatibility/winforms) - [What's New in Windows Forms](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/whats-new/) -
patterns.md 16.8 KB
# 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 ```csharp // 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 ```csharp 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 ```csharp // 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: ```csharp 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 ```csharp 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 ```csharp 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 ```csharp 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 ```csharp 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: ```csharp 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 ```csharp 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 ```csharp 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 ```csharp 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); } } ```
-
-
SKILL.md 4.1 KB
--- name: dotnet-winforms version: "1.0.1" category: "Desktop and Mobile" description: "Build, maintain, or modernize Windows Forms applications with practical guidance on designer-driven UI, event handling, data binding, MVP separation, and migration to modern .NET. Use when working on WinForms projects or migrating from .NET Framework." 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 ## 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. ```csharp // 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+): ```csharp 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. ```mermaid 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
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.