Skip to content
← All posts
· Delta1 Labs WinFormsGlass

Decompiling a WinForms app: reconstructing the Designer's InitializeComponent and resources

A compiled WinForms app has no .Designer.cs and no .resx — the designer code was merged into one type as InitializeComponent(), and the UI strings and images live in an embedded .resources stream. Here is how Glass.NET reconstructs a faithful InitializeComponent() from the binary, surfaces the ComponentResourceManager values, and lets you export the embedded resources — all static, all free.

Open a WinForms project in Visual Studio and every form is two files. There is MainForm.cs, where your code lives — the constructor, the button-click handlers, the logic you actually wrote — and there is MainForm.Designer.cs, which the designer owns. That second file holds a single generated method, InitializeComponent(), plus the private fields for every control on the form. You rarely open it; the designer rewrites it whenever you drag something on the surface.

Then you ship the app, lose the source, and years later need it back. You open the .exe in a decompiler and the two files are gone. There is no MainForm.Designer.cs, no .resx, just one MainForm type with everything in it. That is not a decompiler failing — it is how WinForms was always compiled. This post walks through what the compiler did to your designer code, and how Glass.NET — the free, static-only .NET decompiler — reconstructs it into something you can read and rebuild from.

The two-file split is a source-only illusion

The split between MainForm.cs and MainForm.Designer.cs is a C# partial class. Both files declare partial class MainForm, and the compiler merges them into one type before it ever emits IL. The split exists purely so the designer has a file it can regenerate without touching your code. The CLR has no concept of it.

Say you built a tiny form with a label and a button. Your half looks like this:

// MainForm.cs — the part you write
public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
    }

    private void greetButton_Click(object sender, EventArgs e)
    {
        greetingLabel.Text = $"Hello at {DateTime.Now:T}";
    }
}

The designer’s half — the file you almost never open — looks like this:

// MainForm.Designer.cs — the part the designer owns
partial class MainForm
{
    private System.Windows.Forms.Label greetingLabel;
    private System.Windows.Forms.Button greetButton;

    private void InitializeComponent()
    {
        this.greetingLabel = new System.Windows.Forms.Label();
        this.greetButton = new System.Windows.Forms.Button();
        this.SuspendLayout();
        //
        // greetingLabel
        //
        this.greetingLabel.AutoSize = true;
        this.greetingLabel.Location = new System.Drawing.Point(24, 24);
        this.greetingLabel.Name = "greetingLabel";
        this.greetingLabel.Size = new System.Drawing.Size(120, 15);
        this.greetingLabel.TabIndex = 0;
        this.greetingLabel.Text = "Not greeted yet";
        //
        // greetButton
        //
        this.greetButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom;
        this.greetButton.Location = new System.Drawing.Point(24, 64);
        this.greetButton.Name = "greetButton";
        this.greetButton.Size = new System.Drawing.Size(120, 32);
        this.greetButton.TabIndex = 1;
        this.greetButton.Text = "Greet me";
        this.greetButton.Click += new System.EventHandler(this.greetButton_Click);
        //
        // MainForm
        //
        this.ClientSize = new System.Drawing.Size(300, 160);
        this.Controls.Add(this.greetButton);
        this.Controls.Add(this.greetingLabel);
        this.Name = "MainForm";
        this.Text = "Greeter";
        this.ResumeLayout(false);
        this.PerformLayout();
    }
}

Two files at the keyboard, one type in the assembly. The compiler takes the field declarations from the designer file, the methods from both files, and emits a single MainForm type. InitializeComponent() becomes an ordinary private instance method, indistinguishable in the metadata from any method you wrote by hand.

What the binary actually holds

AT SOURCE (two files)MainForm.csyour logic,event handlersMainForm.Designer.csInitializeComponent()control fieldsIN THE BINARY (one type)type MainFormprivate Label greetingLabelprivate Button greetButtonInitializeComponent()greetButton_Click().ctor()MainForm.resourcesstrings · images · icons(compiled from .resx)GLASS RECONSTRUCTSMainForm (one .cs)faithful InitializeComponent()SuspendLayout / ResumeLayoutLocation · Size · Text · AnchorControls.Add(...)Click += greetButton_Clickrecovered resource valuesreadable text, exported images

When Glass opens the assembly, the MainForm type shows up with its members laid out plainly: the greetingLabel and greetButton fields, the constructor, your greetButton_Click handler, and InitializeComponent(). The private control fields that lived in the designer file are right there as type members — private Label greetingLabel; and private Button greetButton; — because that is exactly where the compiler put them.

Reconstructing InitializeComponent()

InitializeComponent() is just a method, so Glass decompiles it the way it decompiles any method: by reading the IL and lifting it back to C#. The designer emits very regular, almost mechanical IL — construct a control, set property after property, add it to the parent’s Controls collection — which is exactly the kind of straight-line code a decompiler reconstructs cleanly. What Glass produces reads essentially like the designer file you lost:

private void InitializeComponent()
{
    this.greetingLabel = new Label();
    this.greetButton = new Button();
    this.SuspendLayout();
    this.greetingLabel.AutoSize = true;
    this.greetingLabel.Location = new Point(24, 24);
    this.greetingLabel.Name = "greetingLabel";
    this.greetingLabel.Size = new Size(120, 15);
    this.greetingLabel.TabIndex = 0;
    this.greetingLabel.Text = "Not greeted yet";
    this.greetButton.Anchor = AnchorStyles.Bottom;
    this.greetButton.Location = new Point(24, 64);
    this.greetButton.Name = "greetButton";
    this.greetButton.Size = new Size(120, 32);
    this.greetButton.TabIndex = 1;
    this.greetButton.Text = "Greet me";
    this.greetButton.Click += this.greetButton_Click;
    this.ClientSize = new Size(300, 160);
    this.Controls.Add(this.greetButton);
    this.Controls.Add(this.greetingLabel);
    this.Name = "MainForm";
    this.Text = "Greeter";
    this.ResumeLayout(false);
    this.PerformLayout();
}

Everything that mattered is back. The SuspendLayout()/ResumeLayout() bracketing that stops the form thrashing as controls are added; the Location, Size, Text and Anchor property sets; the Controls.Add(...) calls in their original order (which is TabIndex-independent z-order); and the event wiring — this.greetButton.Click += this.greetButton_Click; — pointing at the handler method that sits in the same type. The handler body itself, greetButton_Click, is decompiled as a normal method; nothing about it was ever special. Drop this into a file with your reconstructed handlers and you have a compiling form.

The one cosmetic difference is the // comment banners the designer writes above each control block. Those are source comments; they are not compiled into IL, so they are gone. The code is equivalent, just uncommented.

Designer resources and the ComponentResourceManager

The form above set its text with string literals baked straight into the IL (this.greetButton.Text = "Greet me";). Localizable forms work differently. When you set a form’s Localizable property to true, the designer stops writing literals and instead routes every property through a ComponentResourceManager:

private void InitializeComponent()
{
    ComponentResourceManager resources =
        new ComponentResourceManager(typeof(MainForm));
    this.greetButton = new Button();
    this.SuspendLayout();
    resources.ApplyResources(this.greetButton, "greetButton");
    this.greetButton.Name = "greetButton";
    this.greetButton.Click += this.greetButton_Click;
    resources.ApplyResources(this, "$this");
    this.Controls.Add(this.greetButton);
    this.ResumeLayout(false);
}

Now greetButton.Text, its Location, Size, and even an image or icon are not in the code at all. ApplyResources(this.greetButton, "greetButton") looks up every entry prefixed greetButton. — greetButton.Text, greetButton.Size, greetButton.Location — in a resource table and applies them. That table is the embedded .resources stream, named <Namespace>.<Form>.resources — for a form Greeter.MainForm in namespace Greeter, the stream is Greeter.MainForm.resources. It was compiled from the MainForm.resx you edited at design time, and the culture-specific variants (MainForm.fr.resx, MainForm.de.resx) were compiled into satellite assemblies under fr/ and de/ folders.

Glass decompiles the ApplyResources calls faithfully — you can see which controls are resource-driven and under what key. But the interesting part is that it also surfaces the embedded resource stream itself. Expand the assembly’s resources node and select Greeter.MainForm.resources, and Glass lists the entries that ApplyResources pulls at runtime: greetButton.Text = "Greet me", greetButton.Size = a serialized Size, $this.Text = "Greeter", and any image or icon as a binary value you can view and export. That is how you read a localized form’s actual strings — including the French and German ones in the satellite assemblies — without running it, and how you recover the images and icons that ApplyResources was wiring in.

Running it from the CLI

Everything above is scriptable. The glass decompile subcommand reconstructs the type headlessly, which is what you want over a folder of recovered binaries:

$ glass decompile ./Greeter.exe --type Greeter.MainForm -o ./recovered/MainForm.cs

That writes the reconstructed MainForm — fields, InitializeComponent(), handlers and all — to a single file. To pull the designer resources out alongside it, Glass exports the embedded .resources stream and its contents, so the strings, images and icons land on disk where you can inspect or re-author them into a .resx. Every one of these reads the binary and reconstructs from metadata and IL — none of them run the assembly, so a WinForms .exe you would never launch is still safe to decompile and export.

What you get back, and what you do not

Be clear-eyed about the limits, because they follow directly from what the compiler keeps:

  • You get a faithful InitializeComponent() — the control creation, SuspendLayout/ResumeLayout, every property set, the Controls.Add order and the event wiring — plus the control fields, the handler bodies, and the resource values from the embedded stream.
  • You do not get the original two-file split. There was never a MainForm.Designer.cs in the binary; there was one MainForm type. Glass gives you one file. If you want the designer to reopen the form, re-split it by hand into a partial .Designer.cs — it is a mechanical move.
  • You do not get the original .resx XML. That authoring format is compiled away into the binary .resources stream and never shipped. But because the values are all recoverable from the stream, you can regenerate a .resx from them if you need the authoring format again.

In short: the designer’s intent survives compilation completely — the layout, the wiring, the strings and images are all there — even though the two files and the .resx XML that expressed it do not. Glass reads what the binary kept and hands it back as readable C# and recoverable resource data.

This is exactly the workflow for recovering your own lost WinForms source, or for understanding a dependency whose UI you need to reason about. Grab Glass.NET from the product page and point it at a form. It is free — no licence, no seats, no sign-up — and it never runs the app it reads.

Try Nebula.NET

Harden your .NET code in minutes — start with the free edition.