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:
<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>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.MaximumSizeandForm.MinimumSizescale 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:
{
"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.SetColorModerequired 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.SetColorModeis a stable API, andWFO5001no longer fires.
// Program.cs, before Application.Run
Application.SetColorMode(SystemColorMode.System); // Classic, System or DarkSystemColorMode.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:
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:
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:
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.
Best Practices#
- Target the current
-windowsTFM (net10.0-windows) rather than staying onnet8.0-windowspast its support window, so you keep receiving dark mode, async and designer fixes. - Set
Application.SetColorModeonce, inProgram.cs, rather than scattering theme checks through individual forms. - Keep the UI thread free. Use
async/awaitand the newerShowAsync/ShowDialogAsync/InvokeAsyncAPIs instead of blocking calls or rawInvoke. - Push logic into testable services and view models. A
BindingList<T>orObservableCollection<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
BinaryFormatterand menu-control warnings first avoids compounding changes.
Common Pitfalls#
- Calling
.ShowDialog()from anasyncmethod and blocking on.Resultelsewhere. This is the classic UI-thread deadlock; useShowDialogAsyncinstead. - Assuming dark mode is automatic. Controls follow the color mode, but hand-drawn
OnPaintoverrides, 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
ScaleTopLevelFormMinMaxSizeForDpibefore assuming it's a regression. - Leaving
BinaryFormatter-based custom serialization in place. It throws at runtime on .NET 9 and later; migrate toSystem.Text.Jsonor 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?#
| Scenario | WinForms fit | Notes |
|---|---|---|
| Internal LOB app: data grids, forms, dialogs | Strong | Fast to build, huge base of samples and third-party grid controls |
| Existing multi-million-line WinForms codebase | Strong | Incremental modernization beats a rewrite; see the migration section above |
| Modern, animated, brand-driven consumer UI | Weak | WPF or WinUI 3 give far more control over visuals and animation |
| Team wants XAML, compiled bindings and a live designer | Weak | WinForms has no XAML; use WPF or WinUI 3 |
| Touch-first or high-DPI-only new app | Fair | Works, but WinUI 3 was designed for touch and modern displays from the start |
| Cross-platform desktop or mobile requirement | Poor | Not supported at all; see MAUI vs Avalonia vs Uno vs Blazor |
| Small utility, internal tool, quick prototype | Strong | Lower 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.