Visual Studio 2026 still ships the form designer Alan Cooper drew in 1987
Every UI framework Microsoft has shipped since WinForms (2002) was sold as its successor — WPF, Silverlight, UWP, MAUI, Blazor for desktop. Twenty-four years later, WinForms is still there, on modern .NET, with a designer a VB6 developer would recognise on sight. The Cooper / Geary form-designer architecture from 1987 is still the path of least resistance for a working line-of-business app in 2026, and that is not an accident.
Visual Studio 2026 still ships the form designer Alan Cooper drew in 1987
In 1987, an architect-turned-developer named Alan Cooper sat in California and drew, on paper, what a programming environment for non-programmers ought to look like. Drag a control onto a form. Double-click. Write code that runs when the button is clicked. He called the result Tripod, sold it to Microsoft in 1988, watched it get renamed to Ruby and then to Visual Basic, and saw the model ported forward in 2002 to a thing called WinForms. Microsoft then spent twenty years trying to replace WinForms with something else.
Visual Studio 2026 still ships the same designer. The Cooper / Geary form designer is the longest-lived productive piece of UI tooling Microsoft has ever owned. Microsoft kept it alive mostly by losing the fight to kill it.
I have been writing WinForms code in 2026, on .NET 10, in Visual Studio 2026 — productive enough that the platform genuinely feels current, not legacy. The reflexive "WinForms is dead" framing is wrong, and has been wrong for at least six years. This post is the architectural reason it's wrong, told from inside the stack.
The model that didn't change
What's identical, conceptually, between Cooper's Tripod (1987), VB1 (1991), VB6 (1998), and WinForms running on .NET 10 in Visual Studio 2026:
- The form designer. A canvas, a toolbox, drag-and-drop controls. The visual model is the one Cooper drew on paper.
- The event model.
Form_Load,Button_Click,TextBox_TextChanged. The naming and structure are preserved across thirty-eight years. - The properties window.
F4still opens it. The events tab is still right next to the properties tab. Same layout, same shortcuts, same workflow. - The
.Designer.cs/.Designer.vbseparation. The auto-generated designer code is the descendant of VB6's.frmfile properties block — different file structure, same conceptual line. - The double-click-to-create-handler reflex. Drop a button, double-click it, and Visual Studio creates
Button1_Click, plumbs the event, and drops the cursor inside the handler. Same reflex Cooper sketched.
A VB6 developer dropped into the WinForms designer in current Visual Studio is productive in fifteen minutes. That isn't because Microsoft was lazy. It's because they couldn't find anything better, and customers wouldn't move to the things that weren't better.
Lipstick on the Win32 API
Here is the architectural reality nobody mentions when they call WinForms legacy: WinForms is not a framework. It is a managed wrapper around the Win32 API. Every Form is an HWND. Every Button is an HWND wrapping the USER32 BUTTON window class. Every TextBox is an HWND wrapping EDIT. Every ListBox wraps LISTBOX. The entire control library is a thin, well-shaped, CLR-typed coat of paint over the same C-language API that Notepad and Explorer have used since Windows NT 3.1 in 1993.
That is the durable thing about WinForms. The model survived because Win32 survived, and Win32 survived because Microsoft has, for thirty-three years, treated USER32 as the foundational API contract of the operating system. Notepad still uses it. Explorer still uses it. Every dialog in every part of Windows still uses it. WinForms is sitting on the most aggressively backward-compatible API surface Microsoft owns.
To see the makeup, take it off. Here is what it costs to put a single empty window on the screen if you go straight to Win32 from C# on .NET 10, with no WinForms in scope:
using System;
using System.Runtime.InteropServices;
internal static class Win32
{
public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct WNDCLASSEX
{
public uint cbSize;
public uint style;
public IntPtr lpfnWndProc;
public int cbClsExtra;
public int cbWndExtra;
public IntPtr hInstance;
public IntPtr hIcon;
public IntPtr hCursor;
public IntPtr hbrBackground;
public string? lpszMenuName;
public string lpszClassName;
public IntPtr hIconSm;
}
[StructLayout(LayoutKind.Sequential)]
public struct MSG
{
public IntPtr hwnd;
public uint message;
public IntPtr wParam;
public IntPtr lParam;
public uint time;
public int ptX;
public int ptY;
}
public const uint WS_OVERLAPPEDWINDOW = 0x00CF0000;
public const int SW_SHOW = 5;
public const int CW_USEDEFAULT = unchecked((int)0x80000000);
public const uint WM_DESTROY = 0x0002;
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern ushort RegisterClassExW(ref WNDCLASSEX wc);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr CreateWindowExW(
uint exStyle, string className, string windowName,
uint style, int x, int y, int width, int height,
IntPtr parent, IntPtr menu, IntPtr instance, IntPtr param);
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int cmd);
[DllImport("user32.dll")] public static extern bool UpdateWindow(IntPtr hWnd);
[DllImport("user32.dll")] public static extern int GetMessageW(out MSG msg, IntPtr hWnd, uint min, uint max);
[DllImport("user32.dll")] public static extern bool TranslateMessage(ref MSG msg);
[DllImport("user32.dll")] public static extern IntPtr DispatchMessageW(ref MSG msg);
[DllImport("user32.dll")] public static extern IntPtr DefWindowProcW(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")] public static extern void PostQuitMessage(int code);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr GetModuleHandleW(string? name);
}
internal static class Program
{
// Held as a static field so the GC does not collect the delegate
// while USER32 is still holding a function pointer to it.
private static readonly Win32.WndProc _wndProc = WndProc;
[STAThread]
private static void Main()
{
var hInstance = Win32.GetModuleHandleW(null);
var wc = new Win32.WNDCLASSEX
{
cbSize = (uint)Marshal.SizeOf<Win32.WNDCLASSEX>(),
lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProc),
hInstance = hInstance,
hbrBackground = (IntPtr)6, // COLOR_WINDOW + 1
lpszClassName = "RawWin32Window",
};
Win32.RegisterClassExW(ref wc);
var hWnd = Win32.CreateWindowExW(
0, "RawWin32Window", "Hello, Win32",
Win32.WS_OVERLAPPEDWINDOW,
Win32.CW_USEDEFAULT, Win32.CW_USEDEFAULT, 480, 320,
IntPtr.Zero, IntPtr.Zero, hInstance, IntPtr.Zero);
Win32.ShowWindow(hWnd, Win32.SW_SHOW);
Win32.UpdateWindow(hWnd);
while (Win32.GetMessageW(out var msg, IntPtr.Zero, 0, 0) > 0)
{
Win32.TranslateMessage(ref msg);
Win32.DispatchMessageW(ref msg);
}
}
private static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
if (msg == Win32.WM_DESTROY)
{
Win32.PostQuitMessage(0);
return IntPtr.Zero;
}
return Win32.DefWindowProcW(hWnd, msg, wParam, lParam);
}
}
That is roughly eighty lines, declares two structures and ten P/Invoke signatures, manages a delegate's lifetime by hand to keep it from being garbage-collected while USER32 holds a function pointer to it, and produces a single empty grey rectangle on the screen. No button. No text box. No menu. To add a button you allocate another HWND with CreateWindowExW using the BUTTON class, pass it the parent window's handle, switch on WM_COMMAND in the WndProc to route the click — and write that code yourself for every control.
Now here is the same window in C# with WinForms, on the same .NET 10 runtime:
using System.Windows.Forms;
ApplicationConfiguration.Initialize();
Application.Run(new Form { Text = "Hello, WinForms", ClientSize = new Size(480, 320) });
And in VB.NET, on the same .NET 10 runtime, in the same Visual Studio:
Imports System.Windows.Forms
Module Program
<STAThread>
Sub Main()
Application.Run(New Form With {
.Text = "Hello, WinForms",
.ClientSize = New Size(480, 320)
})
End Sub
End Module
Three lines of body. Same window. Same HWND, same Win32 message pump, same USER32 window class running underneath — the runtime just registered it and wired up the WndProc on your behalf.
That is the lipstick. Specifically:
Application.Runis the message pump. Internally, it is aGetMessageW/TranslateMessage/DispatchMessageWloop, plus some demuxing for modal dialogs and idle handling. The loop in the raw code above is what's running, just behind a single method call.Form,Button,TextBoxeach callCreateWindowExW. Look atControl.CreateHandlein the WinForms source if you want to see it directly. The runtime registers a window class per control type, callsCreateWindowExW, stores the resultingHWNDin the managedControl.Handleproperty, and remembers which managed object owns it.- One thunk WndProc dispatches everything. WinForms installs a single C-callable WndProc per window. When USER32 sends
WM_PAINT, the thunk looks up the managed control byHWND, callsControl.WndProc, and that method demultiplexes the message intoOnPaint, which raises thePaintevent. The whole .NET event model on top of WinForms is a switch statement on Win32 messages. - The designer's
.Designer.csfile is just calls to set those Win32 properties. When the designer writesbutton1.Text = "OK";, that property setter eventually callsSendMessageW(hWnd, WM_SETTEXT, ...). When it writesbutton1.Location = new Point(10, 10);, that'sSetWindowPos. There is no magic. There is just a strongly-typed, well-named, IntelliSense-supported coat of paint over thirty-three years of stable Windows API.
This is also the structural reason WinForms is Windows-only even on cross-platform .NET. Linux and macOS don't have USER32. There is no HWND to wrap. Wine implements a USER32-compatible layer for native Win32 binaries and the .NET Foundation has periodically considered something similar, but the official position is that WinForms targets Windows, and that's where it stays. The cross-platform parts of .NET — the runtime, the BCL, the JIT, the GC, the tooling — all run on Linux and macOS; the WinForms package is the Windows-specific tail.
It is also the structural reason the model has been so durable. WPF made the opposite bet — it threw away USER32 controls and built its own retained-mode rendering pipeline on DirectX, with composition, animation, and data binding owned by the framework rather than the OS. Better at some things (animation, vector graphics, designer-developer separation). Worse at the thing it most needed to be good at: surviving Microsoft's own framework churn. WPF's stack has no equivalent of Win32's thirty-year compatibility guarantee, because no such guarantee was ever offered. WinForms inherited Win32's compatibility guarantee for free.
Lipstick is, on this read, exactly the right metaphor. The makeup is the productivity. The face underneath is the Win32 API. You are not building on top of WinForms. You are building on top of USER32, in a language that doesn't make you write the function pointers by hand.
What did change — and why those changes are upgrades
The implementation moved across thirty-eight years. The model didn't. Honest accounting follows.
Strong typing. No more Variant. No more late-binding-by-default. The compiler catches what the runtime used to surface as Object Doesn't Support This Property Or Method (Error 438). I shipped about a hundred VB3 / 4 / 5 / 6 line-of-business systems and a non-trivial fraction of the bugs I fixed were Variant-coercion errors. Strong typing eliminates that bug class.
Async / await. What VB6 developers worked around with DoEvents loops and Timer controls is now structurally supported. Forms don't lock up while a long operation runs. The whole class of "my UI freezes when I hit the database" bugs is now a syntax decision instead of an architecture problem.
NuGet replaced "is the right OCX registered?". OCX and DLL Hell were a nightmare. Two apps installed on the same Windows box could fight over the same MSVBVM60.DLL version, and the loser silently broke. NuGet — project-local, transitive-dependency-resolved, reproducible from packages.lock.json — is one of the most underrated structural improvements .NET brought.
Cross-platform .NET, since .NET Core 3.0 in September 2019. This is the inflection point that made "WinForms is legacy" factually wrong. WinForms moved onto the same modern runtime as Blazor, MAUI, ASP.NET Core, and the rest of the ecosystem — same CLR, same garbage collector, same JIT, same source generators, same diagnostics tooling. WinForms is Windows-only at the platform layer because Win32 is Windows-only, but the runtime hosting it is the current, actively-developed, open-source .NET. There is no version of "modern .NET" that does not include WinForms.
High-DPI, dark mode, modern accessibility. Microsoft has actively invested in WinForms through .NET 6, 7, 8, 9, and 10 — per-monitor v2 DPI awareness as default behaviour, dark-mode title bars and built-in controls, modern UIA accessibility, source-generated designer output, native AOT-curious work on the runtime side. This is feature work, not maintenance.
The IDE itself. Visual Studio 2026 is light-years ahead of Visual Studio 6.0 from 1998 — better debugger, better refactoring, better IntelliSense, better source control integration, better navigation, AI-assisted code review when you want it. The Cooper / Geary designer sits inside a much better house than it used to.
The platform got better. The model didn't change. VB6 muscle memory survived a generational technology transition mostly intact.
The frameworks that were supposed to kill it
Microsoft did try, repeatedly, to replace WinForms. Each attempt is a story.
Windows Presentation Foundation (WPF), 2006. XAML-based, retained-mode rendering on DirectX, vector graphics, data-binding-as-architecture, designer-developer separation as a first-class concern. Marketed as the future of Windows UI. Real strengths — better animation, better visual polish, better stylability — and real weaknesses, particularly the steep learning curve and the absence of Win32's compatibility guarantee. Lost momentum after Microsoft's pivots to Silverlight and then Windows 8 / WinRT. Still shipping, still has users, still nowhere near WinForms's installed base.
Silverlight, 2007–2013. The browser-plugin runtime that was going to be the cross-platform .NET on the web. Killed by HTML5 and by Microsoft's own internal pivot to WinRT. Officially deprecated October 2021. Not a successor; a casualty.
WinRT / Windows Store apps / UWP, 2012 onward. The Windows 8 era. Sandboxed, restricted-API, Microsoft-Store-only apps, with a strong push toward "no more desktop apps." Customers refused. The desktop-app survival of WinForms (and Win32 generally) was, in retrospect, pure customer pushback against Microsoft's WinRT bet. Microsoft eventually ate the loss; UWP is in maintenance mode at best.
Xamarin Forms (2014) → .NET MAUI (2022). Cross-platform mobile-first UI framework. Strengths real — same XAML on iOS, Android, Mac, Windows. The cross-platform pitch never reached desktop-LOB-developer mindshare; tooling has historically been slow, the layout system complex, and the productivity model less direct than WinForms's drag-drop-double-click for the specific class of apps WinForms is good at.
Blazor (2018–) including Blazor Hybrid / Blazor Desktop. Web-tech-everywhere, real momentum on the server side. The desktop-shell variants haven't displaced WinForms either — running a Chromium engine to host a button is not, for a kiosk app or a departmental data-entry tool, an obviously better choice than calling CreateWindowExW.
Project Reunion / Windows App SDK / WinUI 3, 2020 onward. Microsoft's most recent attempt to unify the Win32 / WinForms / UWP / WinUI tooling story under one umbrella. The rebrand was, in itself, an admission that the previous attempts hadn't worked.
Every successor positioned WinForms as legacy. Every successor either failed outright, settled into a smaller market position than WinForms now occupies, or required a much larger rewrite cost than the old code's failure modes justified. The platform survived not because Microsoft loved it, but because customers refused to leave it, on top of a productivity model that genuinely worked, on top of an OS API that Microsoft can't deprecate without breaking the rest of Windows.
Why it survived
The structural answer.
The productivity argument. For a particular very large class of applications — line-of-business internal tools, admin consoles, kiosks, point-of-sale, departmental utilities, government workflow apps — WinForms is faster to ship a working app than any of its successors. Drag, double-click, ship. Twenty years of Stack Overflow answers exist. Every senior Windows developer of the last twenty-five years has the muscle memory.
The "good enough" trap, in WinForms's favour. Most LOB apps don't need vector graphics, animations, retained-mode rendering, web-tech UI, or cross-platform deployment. They need to render a form, capture data, hit a database, print a report. WinForms does that in fewer lines and fewer abstractions than anything Microsoft has shipped since.
The customer base that wouldn't move. Government, finance, healthcare, manufacturing — industries with twenty-year-old internal apps that work, in stacks where rewriting them in the latest framework is never the highest-priority budget item. Microsoft eventually figured out that fighting these customers cost more than supporting them.
The .NET Core 3.0 pivot in September 2019. The moment Microsoft stopped treating WinForms as a legacy migration target and started treating it as a first-class part of modern .NET. That single decision sealed the platform's future. I work in .NET 10 daily; I was writing WinForms code yesterday; the platform feels current, not legacy.
The honest practitioner take: WinForms is what most working developers reach for first when they need to ship something on Windows that needs to work, look reasonable, and not take a month to architect. WPF reaches for elegance, MAUI reaches for cross-platform, Blazor reaches for web-tech-everywhere. WinForms reaches for done.
What this means for VB6 developers
A direct word, if you came up on VB3 / 4 / 5 / 6 and were told (by Microsoft, by the trade press, by every Stack Overflow answer between 2002 and 2010) that your skills were obsolete:
Your muscle memory is still valid. The form designer, the event model, the drop-a-control / double-click / write-code reflex — all of it works in current Visual Studio.
You will need to learn a new language (VB.NET or C#) and a new runtime (.NET 10 instead of the VB6 runtime). That is real work. But the model is the same.
The .NET Foundation maintains VB.NET. It is no longer in active language evolution — Microsoft stopped adding new language features to it around 2020 — but it is still a supported first-class .NET language, ships in current Visual Studio, runs on .NET 10, and gives you a WinForms designer experience that is conceptually identical to VB6's.
Most VB6 developers who made the leap are writing C# now. That's a tooling and language familiarity question, not a model question. The VB6-to-C#-WinForms transition is mostly syntax; the conceptual framework is preserved.
The thirty-eight-year line is intact. Tripod (1987) → Ruby (1990) → VB1 (1991) → VB6 (1998) → VB.NET / WinForms (2002) → .NET Core 3.0 / WinForms (2019) → .NET 10 / WinForms (2025) → Visual Studio 2026. Same designer. Same event model. Same workflow. Surviving thirty-eight years of Microsoft trying to replace it.
A note on the broader story
This post sits next to a longer historical project I'm writing on the Visual Basic line of products and the people who built them — the Tripod-to-VB6-to-afterlife arc, sourced where possible from the original participants. The book chapter that mirrors this argument is in research; the post is the practitioner-grade version of it, pitched at developers who shipped on this stack rather than at readers interested in the history. If the historical thread interests you, the series is being assembled in the open.
Bottom line
The press has called WinForms legacy since 2006. WinForms is still here, on modern .NET, in current Visual Studio, with the designer Alan Cooper drew in 1987 and a managed wrapper sitting on top of the Win32 API. The frameworks that called it legacy are themselves either deprecated, sidelined, or in markedly smaller market positions than WinForms now occupies. The platform Cooper sketched as a tool for non-programmers is, thirty-eight years later, the path of least resistance for a working line-of-business app in 2026 — because nothing Microsoft built to replace it managed to be enough better to be worth the move. The last framework standing is the one a VB6 developer would recognise on sight, painted over an OS API that Microsoft can't afford to break.
Sources
- Microsoft .NET Foundation — .NET source on GitHub
- Microsoft DevBlogs — .NET release notes and What's new in WinForms posts
- Microsoft .NET — .NET Core 3.0 announcement, September 2019
- Microsoft Learn — Win32 API documentation, USER32 reference
- Microsoft Learn — Silverlight end-of-support
- Personal: about a hundred VB3 / 4 / 5 / 6 line-of-business systems shipped between roughly 1995 and 2010.
-EG_
// comments