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.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-winforms-bfa4ebd.zip · 11 KB
Part of postpartum-genushyacinthus29/dotnet-skills — 80 skills

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

  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.
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.

No comments yet.

Reviews (0)

No reviews yet.

Related