Skip to main content
Published / updated

Web Forms Structure and Project Setup

Before you start

You need: C# (Track 03) or VB.NET (Track 04) — Web Forms code-behind is written in one of them.

You need installed: Visual Studio with the ASP.NET and web development workload. Web Forms is .NET Framework only, so you also need the .NET Framework 4.x targeting pack that the project asks for.

Time: about 45 minutes, plus the practice.

Learning objective

Open an unfamiliar Web Forms application, identify every file type it contains, and explain what the server produces from an .aspx page.

Topics

  • Where Web Forms fits, and why you will meet it
  • Project layout: .aspx, .aspx.vb / .aspx.cs, .aspx.designer
  • Web.config and what it controls
  • runat="server" and the server-control model
  • Page directives
  • Running a legacy project in Visual Studio and IIS Express
  • What the browser actually receives

Where this fits

ASP.NET Web Forms shipped in 2002 and was the default way to build Microsoft web applications for roughly a decade. It is no longer used for new work — ASP.NET Core MVC and Razor Pages replaced it — but a large amount of internal business software still runs on it.

This track is about maintaining those applications: understanding the page lifecycle, fixing state and event problems, and changing a CRUD page without breaking it. It is not about choosing Web Forms for anything new.

Web Forms runs on .NET Framework only. It was never ported to modern .NET, which is why these applications stay on Windows servers and IIS.

The design idea, and its cost

Web Forms tried to make the web feel like desktop development. You drag a button onto a page, double-click it, and write a click handler. The framework hides HTTP entirely.

Desktop: user clicks button → event fires → code runs → screen updates
Web Forms: user clicks button → POST to the same URL → page rebuilds
→ event fires → whole page re-renders

That abstraction is the source of nearly every Web Forms defect you will fix. HTTP is stateless; the framework pretends otherwise, using ViewState and postbacks to fake continuity. When something behaves strangely, the leak in that abstraction is almost always the cause.

Project layout

StudentPortal/
├── Default.aspx markup
├── Default.aspx.vb code-behind — your logic
├── Default.aspx.designer.vb generated control fields — never edit
├── Students/
│ ├── List.aspx
│ ├── List.aspx.vb
│ └── List.aspx.designer.vb
├── Site.master master page — shared layout
├── Site.master.vb
├── Controls/
│ └── StudentCard.ascx user control
├── App_Code/ loose classes, auto-compiled
├── App_Data/ local database files
├── Web.config configuration
├── Global.asax application-level events
└── Bin/ compiled assemblies
ExtensionPurpose
.aspxA page — has a URL
.ascxA user control — reusable fragment, no URL
.masterA master page — the shared layout
.asmxA legacy SOAP web service
.ashxA generic handler — raw output, no page lifecycle
.aspx.vb / .aspx.csCode-behind
.aspx.designer.vbGenerated field declarations

The .designer file is regenerated by Visual Studio whenever the markup changes. Never edit it by hand — your changes are lost the next time someone opens the designer. When a control exists in the markup but the code-behind cannot see it, the designer file is out of sync; opening the page in the designer view regenerates it.

App_Code is unusual: files placed there are compiled automatically at run time in a Website project, without being listed in a project file. It exists only in Website projects, not Web Application projects — knowing which you have determines whether adding a .vb file is enough.

Web.config

<configuration>
<connectionStrings>
<add name="SchoolDb"
connectionString="Server=.;Database=NexCodingSchool;Integrated Security=True;"
providerName="System.Data.SqlClient" />
</connectionStrings>

<appSettings>
<add key="EnableFeeModule" value="true" />
</appSettings>

<system.web>
<compilation debug="true" targetFramework="4.8" />
<customErrors mode="RemoteOnly" defaultRedirect="~/Error.aspx" />
<sessionState mode="InProc" timeout="20" />
<authentication mode="Forms">
<forms loginUrl="~/Login.aspx" timeout="30" />
</authentication>
</system.web>
</configuration>

Five settings worth checking on any legacy application:

  • connectionStrings — how many databases, and whether credentials are in the file
  • compilation debugtrue in production disables optimisations and timeouts; it is a real performance defect
  • customErrorsOff shows full stack traces to users, which is an information leak
  • sessionStateInProc means session dies on every app-pool recycle, which explains "I keep getting logged out"
  • authentication — Forms, Windows, or none

A Web.config in a subfolder overrides the root for that folder. When a setting seems wrong, check for a nested config before concluding anything.

runat="server"

This one attribute is the whole model.

<!-- Plain HTML — sent to the browser as-is, invisible to the server -->
<input type="text" id="txtPlain" />

<!-- HTML server control — the server can read and modify it -->
<input type="text" id="txtHtml" runat="server" />

<!-- Web server control — richest, renders its own HTML -->
<asp:TextBox ID="txtSearch" runat="server" CssClass="form-control" />

runat="server" tells ASP.NET to create a server-side object for the element. Without it, the element is literal text that the server passes through and your code-behind cannot touch.

The first thing to check when code-behind cannot see a control is whether the markup has runat="server". It is the most common cause of "the name txtSearch is not declared".

Web server controls (<asp:...>) generate their own HTML and manage their own state. The rendered id is not what you wrote:

<!-- You write -->
<asp:TextBox ID="txtSearch" runat="server" />

<!-- The browser receives, inside a master page -->
<input type="text" name="ctl00$MainContent$txtSearch" id="MainContent_txtSearch" />

That id mangling breaks hand-written JavaScript and CSS selectors. Use ClientID from the server side rather than guessing:

var box = document.getElementById('<%= txtSearch.ClientID %>');

Newer projects can set ClientIDMode="Static" to keep the id you wrote, per control or application-wide.

Page directives

The first line of every .aspx:

<%@ Page Language="VB"
AutoEventWireup="false"
CodeBehind="List.aspx.vb"
Inherits="StudentPortal.Students.List"
MasterPageFile="~/Site.master"
Title="Students" %>
AttributeMeaning
LanguageVB or C#
CodeBehindThe paired file (Web Application projects)
CodeFileThe paired file (Website projects)
InheritsThe class the page becomes
AutoEventWireupWhether Page_Load is wired by name alone
MasterPageFileThe layout this page sits inside
EnableViewStateWhether state is round-tripped

AutoEventWireup matters when hunting for handlers. When True, a method named Page_Load runs with no Handles clause and no explicit wiring — searching for Handles Me.Load finds nothing, and the method still runs. C# projects default to True; VB projects usually use False plus Handles.

Other directives you will meet:

<%@ Register Src="~/Controls/StudentCard.ascx" TagPrefix="uc" TagName="StudentCard" %>
<%@ Import Namespace="StudentPortal.Data" %>
<%@ OutputCache Duration="60" VaryByParam="classId" %>

Running a legacy project

  1. Open the .sln in Visual Studio. Web Forms needs the ASP.NET and web development workload installed.
  2. Check the target framework in project properties. 4.5 or below may need the matching targeting pack.
  3. Confirm the start page and the port. IIS Express settings live in .vs/config/applicationhost.config, which is often gitignored — a fresh clone may not have a port assigned.
  4. Set the connection string to a database you can reach.
  5. Press F5.

Common first-run failures:

SymptomCause
"Could not load file or assembly"A Bin/ DLL is missing, or a NuGet restore has not run
"Unrecognized configuration section"Framework version mismatch, or a missing config section handler
Port already in useAnother IIS Express site claims it; change it in project properties
Blank page, no errorcustomErrors is on and hiding the real exception — set mode="Off" locally
Designer will not openA markup error, or a control the designer cannot resolve

Set customErrors mode="Off" in your local config before debugging anything. Without it you get a generic error page and no information.

What the browser receives

Web Forms renders a single <form> element wrapping the whole page, posting back to itself:

<form method="post" action="./List.aspx" id="form1">
<input type="hidden" name="__VIEWSTATE" value="/wEPDwUKMTU4Nzk4..." />
<input type="hidden" name="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" value="" />
...
</form>

Three consequences that explain a great deal of legacy behaviour:

  • One form per page. Nested <form runat="server"> elements are not allowed, which is why every button on the page submits everything.
  • __VIEWSTATE carries the encoded control state on every request, in both directions. A large grid produces a very large hidden field.
  • __EVENTTARGET names which control caused the postback. This is how the framework decides which event handler to run.

Open any Web Forms page in browser DevTools and look at the POST body. Seeing __VIEWSTATE and __EVENTTARGET on the wire makes the next article's lifecycle far easier to follow.

Errors you will hit

MessageCauseFix
Project shows as unavailable in Solution ExplorerThe ASP.NET and web development workload is missingTools → Get Tools and Features → add it
"The project requires .NET Framework 4.x"Targeting pack not installedInstaller → Individual components → tick it
HTTP Error 500.19A malformed Web.configCheck the XML is well-formed
Port already in useAnother IIS Express site claims itProject Properties → Web → change the port
Yellow error page with no detailcustomErrors is OnSet it to RemoteOnly in Web.config while developing
Control exists in markup but not in code-behindThe .designer file is out of syncOpen the page in Design view to regenerate it

Turn customErrors to RemoteOnly on day one. With it On you get a friendly page and no information, and every diagnosis takes ten times longer.

Common mistakes

  • Editing a .aspx.designer file by hand
  • Forgetting runat="server", so the code-behind cannot see the control
  • Hard-coding a rendered control id in JavaScript instead of using ClientID
  • Leaving compilation debug="true" in a deployed configuration
  • Debugging with customErrors on and seeing no real error
  • Assuming Page_Load is unwired because there is no Handles clause, when AutoEventWireup is True
  • Missing a nested Web.config that overrides the root
  • Assuming App_Code works in a Web Application project

Practice

Open an existing Web Forms application. Produce a one-page map: how many .aspx pages, how many .ascx controls, whether there is a master page, which project type it is, its target framework, and every connection string. Note whether AutoEventWireup is True or False, and whether any subfolder has its own Web.config.

Then open one page in the browser with DevTools on the Network tab, click a button, and inspect the POST body. Find __VIEWSTATE and __EVENTTARGET, and record the size of the ViewState field. Compare it to a page with a populated GridView — the difference is what the next articles explain.

You can now

  • Open a Web Forms solution in Visual Studio and run it on IIS Express
  • Name every file type in the project and say what it does
  • Say what runat="server" actually changes
  • Read Web.config and find the connection strings and customErrors
  • Regenerate a .designer file that has gone out of sync
  • Get real errors visible instead of the yellow page

Review questions

  1. What does runat="server" do, and what is the symptom when it is missing?
  2. Why must you never edit a .designer file by hand?
  3. What does AutoEventWireup="True" change about finding event handlers?
  4. Why does a Web Forms page contain exactly one <form> element?

Next: The page lifecycle