Windows Forms shipped with the original .NET Framework in 2002, and it still ships with every modern .NET release: .NET 8, .NET 9, .NET 10 and the .NET 11 preview. It is not a compatibility shim. Microsoft actively adds features to it, and it remains one of the fastest ways to build a Windows line-of-business app in C#. This guide covers what has actually changed in Windows Forms on modern .NET, how dark mode, high-DPI and async support evolved release by release, how data binding and MVVM fit in, and when Windows Forms is still the right technology to reach for instead of WPF, WinUI 3 or a cross-platform framework.

What Is Windows Forms on Modern .NET?#

Windows Forms (WinForms) is a Windows-only, GDI+-based desktop UI framework built around controls, an event-driven programming model and a visual designer. On modern .NET it runs on the same runtime as everything else: CoreCLR, the same garbage collector, the same dotnet CLI, NuGet and SDK-style projects. The framework assembly itself moved to the dotnet/winforms repository, developed in the open, with System.Drawing's source code folded into the same repository as of .NET 8.

Windows Forms targets net10.0-windows (or net8.0-windows, net9.0-windows), never plain net10.0, because it depends on Win32 and COM interop that only exists on Windows. That single fact shapes every architecture decision in this guide: Windows Forms is not, and will not become, cross-platform. If that is a hard requirement, see Choosing a .NET UI Framework: MAUI vs Avalonia vs Uno vs Blazor instead.

How Windows Forms Works: The Event-Driven Desktop Model#

A Windows Forms app owns a single UI thread that runs a message loop, pumping Win32 window messages and dispatching them as .NET events: Click, TextChanged, Paint, and so on. Controls are thin, mutable wrappers around native Win32 window handles (HWNDs). This is a fundamentally different model from WPF's retained-mode, DirectX-composited visual tree, and it is why Windows Forms starts fast and uses comparatively little memory: there is no separate scene graph to build and diff.

Every control read or write that touches the native handle must happen on the UI thread. Background work has to marshal back with Control.Invoke, Control.BeginInvoke, or, since .NET 9, the newer async-friendly APIs described later in this guide. This single-threaded-apartment model is simple to reason about but means one slow event handler freezes the entire window.

Getting Started: A Minimal Windows Forms App on .NET 10#

A new project uses top-level statements and the ApplicationConfiguration source generator, which reads defaults from the project file instead of hand-written boilerplate:

XML
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
    <ApplicationVisualStyles>true</ApplicationVisualStyles>
    <ApplicationUseCompatibleTextRendering>false</ApplicationUseCompatibleTextRendering>
  </PropertyGroup>
</Project>
C#
using MyApp;

ApplicationConfiguration.Initialize();
Application.SetColorMode(SystemColorMode.System);
Application.Run(new MainForm());

ApplicationConfiguration.Initialize() expands, at build time, into the Application.SetHighDpiMode, Application.EnableVisualStyles and Application.SetCompatibleTextRenderingDefault calls that every Program.cs used to hand-write. Create the project with dotnet new winforms -n MyApp, and open the generated form with Visual Studio's designer or, on any platform, edit its .Designer.cs partial class by hand.

High-DPI Support: What Actually Changed#

High-DPI handling used to be the most common source of blurry text and misplaced controls in Windows Forms apps. PerMonitorV2, the recommended DPI mode, has been the default in new project templates since .NET Core 3.0, but .NET 8 fixed real scaling bugs rather than just documenting the mode:

  • Nested controls now scale correctly as their container moves between monitors with different DPI settings. Previously a button inside a panel inside a tab page could end up the wrong size.
  • Form.MaximumSize and Form.MinimumSize scale with the monitor's DPI automatically. This is on by default starting in .NET 8; to restore the old behavior, opt out with a runtime configuration switch:
JSON
{
  "runtimeOptions": {
    "configProperties": {
      "System.Windows.Forms.ScaleTopLevelFormMinMaxSizeForDpi": false
    }
  }
}

Visual Studio 2022 17.8 also decoupled the designer's own DPI awareness from the app's: set <ForceDesignerDPIUnaware>true</ForceDesignerDPIUnaware> in the project file to design a DPI-unaware app without making Visual Studio itself blurry, or leave it unset to design at the same scale the app will run at.

Dark Mode in Windows Forms: From Preview to Fully Supported#

Dark mode support is real, but it arrived in two stages, and the stage matters for which .NET version you target:

  • .NET 9: preliminary dark mode was shipped but marked experimental. Calling Application.SetColorMode required suppressing compiler error WFO5001 by opting in explicitly in the project file, and coverage of third-party and custom controls was incomplete.
  • .NET 10: dark mode is fully integrated and no longer experimental. Application.SetColorMode is a stable API, and WFO5001 no longer fires.
C#
// Program.cs, before Application.Run
Application.SetColorMode(SystemColorMode.System); // Classic, System or Dark

SystemColorMode.System follows the Windows setting, Dark forces dark mode, and Classic keeps the pre-.NET-9 light appearance. Most built-in controls repaint themselves automatically, but a control that draws with raw Win32 common controls (a native scroll bar, for instance) stays light unless it opts in. Override CreateParams and call SetStyle before the base class reads it, since the style cannot be set from the constructor:

C#
protected override CreateParams CreateParams
{
    get
    {
        SetStyle(ControlStyles.ApplyThemingImplicitly, true);
        return base.CreateParams;
    }
}

If you inherit a control that already themes itself and want full manual control over its drawing, pass false instead. Budget real QA time for dark mode: custom-drawn Graphics calls, hard-coded Color.White backgrounds and owner-drawn ListView or TreeView items need manual updates regardless of which mode you target.

Async Forms: Showing Dialogs Without Blocking the UI Thread#

Classic Windows Forms code calls form.ShowDialog(), which blocks the calling thread until the dialog closes β€” awkward when the caller is itself inside an async method. .NET 9 added async-friendly alternatives behind an experimental flag; .NET 10 made them stable:

C#
private async void OnEditCustomerClick(object? sender, EventArgs e)
{
    using var editForm = new CustomerEditForm(selectedCustomer);
    DialogResult result = await editForm.ShowDialogAsync(this);

    if (result == DialogResult.OK)
    {
        await customerService.SaveAsync(selectedCustomer, cancellationToken: default);
    }
}

Form.ShowAsync, Form.ShowDialogAsync and TaskDialog.ShowDialogAsync all became non-experimental in .NET 10, and the async task now holds only a weak reference to the form, so a form left open no longer keeps a completed task alive. Control.InvokeAsync, the async replacement for Control.Invoke when marshaling work back to the UI thread, was never experimental and is safe to use on .NET 8 as well. Together these remove most of the reasons WinForms code used to call .Result or .Wait() on the UI thread, a pattern that risks a deadlock and should be treated as a bug wherever you find it.

Data Binding and MVVM in Windows Forms#

Windows Forms has always had a data-binding engine through BindingSource and Control.DataBindings, but .NET 8 added a second, WPF-inspired engine aimed squarely at MVVM. It is implemented through IBindableComponent, which Control implements, and it is what backs the newer Command and CommandParameter properties on ButtonBase-derived controls:

C#
public sealed partial class CustomerListForm : Form
{
    private readonly CustomerListViewModel viewModel = new();

    public CustomerListForm()
    {
        InitializeComponent();
        saveButton.Command = viewModel.SaveCommand;
        customerGrid.DataSource = viewModel.Customers;
    }
}

public sealed class CustomerListViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    public BindingList<Customer> Customers { get; } = [];

    public ICommand SaveCommand { get; }

    public CustomerListViewModel()
    {
        SaveCommand = new RelayCommand(_ => Save(), _ => Customers.Count > 0);
    }

    private void Save()
    {
        // Persist changes, then raise PropertyChanged for any derived properties.
    }
}

Assigning an ICommand to Button.Command wires up Click automatically: the button invokes the command and disables itself when ICommand.CanExecute returns false, exactly as in WPF. You can write RelayCommand yourself in a few lines or bring in a small MVVM toolkit; either way, the important architectural point is that view models built for WPF or Avalonia no longer need a Windows Forms-specific rewrite β€” the same INotifyPropertyChanged and ICommand contracts work here. It's still code-behind heavy compared to XAML frameworks, because there is no markup language and no compiled bindings; every binding is set up imperatively or through the designer's Properties window.

The Windows Forms Designer Today#

The Windows Forms Designer runs out-of-process from Visual Studio, hosting your controls in a separate designer process so that a bug in a custom control cannot crash the IDE. It supports .NET 8, 9 and 10 projects, and .NET 10 ported several UITypeEditor implementations back from .NET Framework, including collection editors for ToolStrip and several DataGridView-related editors, so they show up again in the Properties window and the Designer Actions panel. If your project targets .NET Framework 4.x, Visual Studio still uses the older in-process designer; once you move to net8.0-windows or later, you get the out-of-process one automatically.

The designer emits partial classes exactly like the .NET Framework designer did, so .Designer.cs files, resource (.resx) files and the visual editing experience will feel immediately familiar to anyone coming from WinForms on .NET Framework.

Migrating from .NET Framework to Modern .NET#

Most WinForms migrations are mechanical, not architectural, because the control set and much of the API surface carried over unchanged. A typical path:

  1. Convert the project file to SDK-style with the .NET Upgrade Assistant, which rewrites the .csproj, retargets to net10.0-windows, and flags APIs that need attention:

bash dotnet tool install -g upgrade-assistant upgrade-assistant upgrade MyWinFormsApp.sln

  1. Replace removed or obsolete APIs. BinaryFormatter was removed starting in .NET 9 and now throws PlatformNotSupportedException wherever it is still referenced, including in some clipboard and drag-and-drop code paths; the .NET 9 clipboard and DataObject changes analyzer flags these call sites. Menu-related controls (MainMenu, MenuItem) are gone in favor of MenuStrip and ToolStripMenuItem, a change that dates back to .NET Core 3.1.
  2. Reach for the Windows Compatibility Pack (Microsoft.Windows.Compatibility) for Framework-only APIs β€” such as parts of System.Configuration or System.Data.OracleClient-adjacent code β€” that have no direct modern replacement but that you are not ready to remove.
  3. Re-test high-DPI and printing code. Both areas changed enough in .NET 8 and later that visual regression testing pays for itself, especially for forms with fixed pixel layouts instead of anchored or docked controls.
  4. Decide on dark mode last, once the app builds and runs cleanly on the new target, since it is additive and does not block the rest of the migration.

For apps with a large amount of business logic entangled in code-behind, this is also a natural point to extract that logic into plain C# services and view models, which pays off further if you later add a second client, such as a Blazor Hybrid or MAUI front end, that can share the same service layer. See Modernizing .NET Framework Applications to Modern .NET for the broader migration playbook beyond Windows Forms specifically.

Best Practices#

  • Target the current -windows TFM (net10.0-windows) rather than staying on net8.0-windows past its support window, so you keep receiving dark mode, async and designer fixes.
  • Set Application.SetColorMode once, in Program.cs, rather than scattering theme checks through individual forms.
  • Keep the UI thread free. Use async/await and the newer ShowAsync/ShowDialogAsync/InvokeAsync APIs instead of blocking calls or raw Invoke.
  • Push logic into testable services and view models. A BindingList<T> or ObservableCollection<T> backed by a plain C# view model can be unit tested without spinning up a form.
  • Run the migration analyzers before you touch dark mode or DPI work. Fixing BinaryFormatter and menu-control warnings first avoids compounding changes.

Common Pitfalls#

  • Calling .ShowDialog() from an async method and blocking on .Result elsewhere. This is the classic UI-thread deadlock; use ShowDialogAsync instead.
  • Assuming dark mode is automatic. Controls follow the color mode, but hand-drawn OnPaint overrides, hard-coded colors and native common controls need explicit updates.
  • Skipping the DPI opt-out check. If pixel-perfect fixed-size dialogs broke after upgrading to .NET 8 or later, check ScaleTopLevelFormMinMaxSizeForDpi before assuming it's a regression.
  • Leaving BinaryFormatter-based custom serialization in place. It throws at runtime on .NET 9 and later; migrate to System.Text.Json or a custom binary format before upgrading.
  • Treating the designer as legacy and avoiding it. The out-of-process designer is actively maintained and is still the fastest way to lay out complex forms with many controls.

Windows Forms vs WPF vs WinUI 3: When Is WinForms Still the Right Choice?#

ScenarioWinForms fitNotes
Internal LOB app: data grids, forms, dialogsStrongFast to build, huge base of samples and third-party grid controls
Existing multi-million-line WinForms codebaseStrongIncremental modernization beats a rewrite; see the migration section above
Modern, animated, brand-driven consumer UIWeakWPF or WinUI 3 give far more control over visuals and animation
Team wants XAML, compiled bindings and a live designerWeakWinForms has no XAML; use WPF or WinUI 3
Touch-first or high-DPI-only new appFairWorks, but WinUI 3 was designed for touch and modern displays from the start
Cross-platform desktop or mobile requirementPoorNot supported at all; see MAUI vs Avalonia vs Uno vs Blazor
Small utility, internal tool, quick prototypeStrongLower ceremony than XAML frameworks for simple forms

Frequently Asked Questions#

Is Windows Forms still supported in .NET 10?#

Yes. Windows Forms ships as part of every .NET release, including .NET 8 (LTS), .NET 9, .NET 10 (LTS) and the .NET 11 preview, with real feature work β€” not just maintenance β€” in each version, such as dark mode, async forms and designer improvements.

Does Windows Forms support dark mode?#

Yes, as of .NET 10. .NET 9 shipped preliminary dark mode behind an experimental compiler warning (WFO5001); .NET 10 made Application.SetColorMode a stable, non-experimental API. Custom-drawn controls still need manual updates to respect the color mode.

Can Windows Forms apps use MVVM?#

Yes. Since .NET 8, a WPF-inspired data-binding engine plus ICommand-backed Button.Command/CommandParameter properties make MVVM practical, though there is no XAML or compiled bindings, so more of the wiring happens in code than in WPF or MAUI.

Should I start a new desktop project in Windows Forms in 2026?#

For a Windows-only internal tool or data-entry app with tight deadlines, yes, it is still a strong, low-ceremony choice. For a customer-facing or highly visual product, or anything that must run outside Windows, start with WPF, WinUI 3 or a cross-platform framework instead.

How do I migrate a Windows Forms app from .NET Framework to modern .NET?#

Run the .NET Upgrade Assistant to convert the project to SDK-style and retarget it, fix flagged obsolete APIs such as BinaryFormatter and legacy menu controls, add the Windows Compatibility Pack for anything with no direct replacement, and re-test DPI and printing behavior before adopting dark mode.

Is Windows Forms cross-platform?#

No. Windows Forms depends on Win32 and GDI+ and only targets net8.0-windows or later -windows target framework monikers. For shared UI code across platforms, use .NET MAUI, Avalonia, Uno Platform or Blazor, compared in our cross-platform UI guide.

Summary#

  • Windows Forms is actively developed on .NET 8, 9, 10 and the .NET 11 preview, not merely kept alive for compatibility.
  • Dark mode and async dialog APIs both went from experimental in .NET 9 to fully supported in .NET 10.
  • High-DPI scaling for nested controls and form min/max sizes improved by default starting in .NET 8.
  • A WPF-style data-binding engine and ICommand-backed button commands, added in .NET 8, make MVVM realistic without adopting XAML.
  • It remains the right call for Windows-only internal tools and large existing codebases; reach for WPF, WinUI 3 or a cross-platform framework for new, visually ambitious or multi-platform products.

Further Reading#