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.configand what it controlsrunat="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
| Extension | Purpose |
|---|---|
.aspx | A page — has a URL |
.ascx | A user control — reusable fragment, no URL |
.master | A master page — the shared layout |
.asmx | A legacy SOAP web service |
.ashx | A generic handler — raw output, no page lifecycle |
.aspx.vb / .aspx.cs | Code-behind |
.aspx.designer.vb | Generated 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 filecompilation debug—truein production disables optimisations and timeouts; it is a real performance defectcustomErrors—Offshows full stack traces to users, which is an information leaksessionState—InProcmeans 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" %>
| Attribute | Meaning |
|---|---|
Language | VB or C# |
CodeBehind | The paired file (Web Application projects) |
CodeFile | The paired file (Website projects) |
Inherits | The class the page becomes |
AutoEventWireup | Whether Page_Load is wired by name alone |
MasterPageFile | The layout this page sits inside |
EnableViewState | Whether 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
- Open the
.slnin Visual Studio. Web Forms needs the ASP.NET and web development workload installed. - Check the target framework in project properties. 4.5 or below may need the matching targeting pack.
- 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. - Set the connection string to a database you can reach.
- Press F5.
Common first-run failures:
| Symptom | Cause |
|---|---|
| "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 use | Another IIS Express site claims it; change it in project properties |
| Blank page, no error | customErrors is on and hiding the real exception — set mode="Off" locally |
| Designer will not open | A 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. __VIEWSTATEcarries the encoded control state on every request, in both directions. A large grid produces a very large hidden field.__EVENTTARGETnames 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
| Message | Cause | Fix |
|---|---|---|
| Project shows as unavailable in Solution Explorer | The ASP.NET and web development workload is missing | Tools → Get Tools and Features → add it |
| "The project requires .NET Framework 4.x" | Targeting pack not installed | Installer → Individual components → tick it |
HTTP Error 500.19 | A malformed Web.config | Check the XML is well-formed |
| Port already in use | Another IIS Express site claims it | Project Properties → Web → change the port |
| Yellow error page with no detail | customErrors is On | Set it to RemoteOnly in Web.config while developing |
| Control exists in markup but not in code-behind | The .designer file is out of sync | Open 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.designerfile 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
customErrorson and seeing no real error - Assuming
Page_Loadis unwired because there is noHandlesclause, whenAutoEventWireupisTrue - Missing a nested
Web.configthat overrides the root - Assuming
App_Codeworks 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.configand find the connection strings andcustomErrors - Regenerate a
.designerfile that has gone out of sync - Get real errors visible instead of the yellow page
Review questions
- What does
runat="server"do, and what is the symptom when it is missing? - Why must you never edit a
.designerfile by hand? - What does
AutoEventWireup="True"change about finding event handlers? - Why does a Web Forms page contain exactly one
<form>element?
Next: The page lifecycle