Visual Studio 2026 still ships the form designer Alan Cooper drew in 1987
Pangram verdict · v3.3
We believe that this document is primarily human-written, with some AI-generated content detected
AI likelihood · overall
MixedArticle text · 1,425 words · 6 segments analyzed
0x06│ 2026.04.27│ 12 min read │ winforms · vb6 · visual-basic · dotnet · csharp · win32 · visual-studio · software-history · programming · opinion │ history (v5)
Every UI framework Microsoft has shipped since WinForms (2002) was sold as its successor. WPF, Silverlight, UWP, MAUI, Blazor desktop. Twenty-four years on, WinForms is still there, on modern .NET, with a designer that any VB6 developer would recognise on sight. The Cooper and 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.
EvilGenius⏱ 12 min ▸ 10 tags
Visual Studio 2026 still ships the form designer Alan Cooper drew in 1987In 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 it. 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 and 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 changeWhat'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. F4 still opens it. The events tab is still right next to the properties tab. Same layout, same shortcuts, same workflow. The .Designer.cs file (or .Designer.vb if you prefer). The auto-generated designer code is the descendant of VB6's .frm file 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. The 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 APIHere 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's roughly eighty lines. Two structures and ten P/Invoke signatures. You manage the delegate's lifetime by hand to keep it from being garbage-collected while USER32 still holds a function pointer to it. And you get 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.Run is the message pump. Internally it is a GetMessageW, TranslateMessage, DispatchMessageW loop, 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, TextBox each call CreateWindowExW. Look at Control.CreateHandle in the WinForms source if you want to see it directly. The runtime registers a window class per control type, calls CreateWindowExW, stores the resulting HWND in the managed Control.Handle property, 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 by HWND, calls Control.WndProc, and that method demultiplexes the message into OnPaint, which raises the Paint event. The whole .NET event model on top of WinForms is a switch statement on Win32 messages. The designer's .Designer.cs file is just calls to set those Win32 properties. When the designer writes button1.Text = "OK";, that property setter eventually calls SendMessageW(hWnd, WM_SETTEXT, ...). When it writes button1.Location = new Point(10, 10);, that's SetWindowPos. 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.