I discovered an “new” but “old” way (Old but still the dominant way of web tech) of doing ASP.NET Web Forms, not with more complex server/user control with ViewState, or inline C# frontpage syntax. It refers as “new” because it’s not the convention of how ASP.NET Web Forms does things. It’s done by using jsut native HTML, JavaScript and CSS to build the application, which is an old school but it is far from outdated and it the most advanced technology that ACTUALLY build the web and ACTUALLY runs the world web tech at the core foundation level. I still using the master pages though. Using pure HTML, JavaScript and CSS you got the most powerful tool to do whatever you want to do with the web, that’s the vanilla, right? Since there is not much mentioned and discussed in traditional Web Forms community. So, I named it after that…. thus ladies and gentlemen’s, I humbly hereby present you….
The Vanilla ASP.NET Web Forms Architecture (July 23rd, 2025)
A thousand thanks to Microsoft for creating such an awesome platform, ASP.NET (Web Forms, MVC, .NET Core) as the backbone to power the web backend for the Vanilla Web.
This article starts the series of demonstrating the concept of “Vanilla ASP.NET Web Forms Architecture” and this page will serve as the main content menu with links to detailed articles explaining each concept.
Main Menu:
- CRUD (Create, Read, Update, Delete) with Fetch API in Vanilla ASP.NET Web Forms
- JavaScript Portable Object – Reusable Component
- RESTful Endpoints vs Page Methods/Web Methods
- Building a Complex Progress Reporting UI in Vanilla ASP.NET Web Forms Style
– Part 2: Progress Reporting in Web Application using HTTP Request/API Endpoint
– Part 3: Progress Reporting in Web Application using Web Socket
– Part 4: Progress Reporting in Web Application using Server-Sent Events (SSE)
– Part 5: Building a Portable JavaScript Object for MySqlBackup.NET Progress Reporting Widget
– (old doc) Progress Reporting with MySqlBackup.NET - Automatic Route All Pages in ASP.NET Web Forms
- Building a Table with Freezing Columns and Rows without using Custom User Control (Pure CSS) and Use It in ASP.NET Web Forms
- Lightning Fast Page Caching Strategy for High Traffic Performance Vanilla ASP.NET Web Forms
- Single Page Application (SPA) and Client-Side Routing (CSR) in Vanilla ASP.NET Web Forms
- Printing Invoice, Bill, Ticket, Reports in Vanilla ASP.NET Web Forms
- More to come, stay tuned…
Definition of “Vanilla ASP.NET Web Forms Architecture”:
- Pure HTML, JavaScript, CSS
- Zero ViewState
- No Server Control
- No Custom User Control
- No GridView
- No PostBack
- No UpdatePanel
- Routed Path, no Static File+Folder Path (*.aspx)
- No Server Theme/Skin Files
The replacement of traditional components with Vanilla components:
Vanilla Components | Traditional Web Forms Components |
---|---|
HTML <input> | Server Controls (TextBox , DropDownList , etc.) |
Dynamic HTML Table | GridView , DataList , Repeater |
FetchAPI | Server Control PostBack |
FetchAPI + DOM Manipulation | UpdatePanel |
Portable JavaScript Object | Custom User Control |
URL Routing | Static Folder + File Path (*.aspx) |
JSON Response | Server Control Databinding |
RESTful Endpoints | Page Methods/Web Methods |
Pure CSS Styling | Theme/Skin Files |
Event Listeners | Server Events (Page_Load, Button_Click) |
Let’s introduce the first concept: Replacing GridView with Dynamic HTML Table.
The elephant in the room: GridView, one of the most essential server controls in traditional Web Forms development.
Dynamic HTML Table
Method 1: Frontend JSON Rendering
Backend: userlist.aspx.cs
public partial class userlist : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
string action = (Request["action"] ?? "").ToLower();
if (action == "getusers")
{
GetUsersAsJson();
}
}
void GetUsersAsJson()
{
try
{
var users = GetAllUsers(); // Returns List<User>
string json = JsonSerializer.Serialize(users);
Response.Clear();
Response.ContentType = "application/json";
Response.Write(json);
}
catch (Exception ex)
{
Response.Write($"{{\"error\":\"{ex.Message}\"}}");
}
}
}
Frontend: Table Rendering
async function loadUsersTable() {
try {
const formData = new FormData();
formData.append('action', 'getusers');
const response = await fetch('/userlist.aspx', {
method: 'POST',
body: formData
});
const users = await response.json();
const tableHtml = `
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Phone</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${users.map(user => `
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.tel}</td>
<td>${user.email}</td>
<td>
<button onclick="editUser(${user.id})">Edit</button>
<button onclick="deleteUser(${user.id})">Delete</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
document.getElementById('user-table-container').innerHTML = tableHtml;
} catch (error) {
console.error('Error loading users:', error);
}
}
Method 2: Card Layout Rendering
async function loadUsersCards() {
try {
const formData = new FormData();
formData.append('action', 'getusers');
const response = await fetch('/userlist.aspx', {
method: 'POST',
body: formData
});
const users = await response.json();
const cardsHtml = users.map(user => `
<div class="user-card">
<div class="user-card-header">
<h3>${user.name}</h3>
<span class="user-id">#${user.id}</span>
</div>
<div class="user-card-body">
<p><strong>Phone:</strong> ${user.tel}</p>
<p><strong>Email:</strong> ${user.email}</p>
</div>
<div class="user-card-actions">
<button onclick="editUser(${user.id})">Edit</button>
<button onclick="deleteUser(${user.id})">Delete</button>
</div>
</div>
`).join('');
document.getElementById('user-cards-container').innerHTML = cardsHtml;
} catch (error) {
console.error('Error loading users:', error);
}
}
Card CSS
.user-card {
background: white;
border-radius: 10px;
padding: 20px;
margin: 15px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
display: inline-block;
width: 300px;
vertical-align: top;
}
.user-card:hover {
transform: translateY(-5px);
}
.user-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
border-bottom: 2px solid #ecf0f1;
padding-bottom: 10px;
}
.user-id {
background: #3498db;
color: white;
padding: 5px 10px;
border-radius: 15px;
font-size: 12px;
}
.user-card-actions {
margin-top: 15px;
text-align: center;
}
Method 3: Backend Pre-rendered HTML
Backend: userlist.aspx.cs (Pre-rendered)
// C# - Backend Pre-rendered HTML
public partial class userlist : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
string action = (Request["action"] ?? "").ToLower();
if (action == "getuserstable")
{
GetUsersAsHtmlTable();
}
}
void GetUsersAsHtmlTable()
{
try
{
var users = GetAllUsers();
var sb = new StringBuilder();
sb.AppendLine(@"
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Phone</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>");
foreach (var user in users)
{
sb.AppendLine($@"
<tr>
<td>{user.Id}</td>
<td>{HttpUtility.HtmlEncode(user.Name)}</td>
<td>{HttpUtility.HtmlEncode(user.Tel)}</td>
<td>{HttpUtility.HtmlEncode(user.Email)}</td>
<td>
<button onclick=""editUser({user.Id})"">Edit</button>
<button onclick=""deleteUser({user.Id})"">Delete</button>
</td>
</tr>");
}
sb.AppendLine(@"
</tbody>
</table>");
Response.Clear();
Response.ContentType = "text/html";
Response.Write(sb.ToString());
}
catch (Exception ex)
{
Response.Write($"<div class='error'>Error: {HttpUtility.HtmlEncode(ex.Message)}</div>");
}
}
}
Frontend: Direct HTML Insertion
async function loadPreRenderedTable() {
try {
const formData = new FormData();
formData.append('action', 'getuserstable');
const response = await fetch('/userlist.aspx', {
method: 'POST',
body: formData
});
const htmlContent = await response.text();
// Direct insertion - zero frontend rendering work
document.getElementById('pre-rendered-container').innerHTML = htmlContent;
} catch (error) {
console.error('Error loading pre-rendered table:', error);
document.getElementById('pre-rendered-container').innerHTML =
'<div class="error">Failed to load data</div>';
}
}
Complete Usage Example
HTML Page
<!DOCTYPE html>
<html>
<head>
<title>Vanilla Web Forms Demo</title>
<link rel="stylesheet" href="vanilla-styles.css">
</head>
<body>
<div class="container">
<h1>Vanilla ASP.NET Web Forms Architecture Demo</h1>
<!-- User Management Widget -->
<div id="user-widget-container"></div>
<!-- Dynamic Tables -->
<h2>Users Table (JSON + Frontend Rendering)</h2>
<button onclick="loadUsersTable()">Load Table</button>
<div id="user-table-container"></div>
<h2>Users Cards (Flexible Layout)</h2>
<button onclick="loadUsersCards()">Load Cards</button>
<div id="user-cards-container"></div>
<h2>Pre-rendered Table (Backend HTML)</h2>
<button onclick="loadPreRenderedTable()">Load Pre-rendered</button>
<div id="pre-rendered-container"></div>
</div>
<script src="UserManager.js"></script>
<script>
// Initialize user widget
const userWidget = UserManager.create();
userWidget.init('user-widget-container');
// Load initial data
loadUsersTable();
</script>
</body>
</html>
This architecture demonstrates the power of treating ASP.NET Web Forms as a pure API backend while leveraging modern JavaScript for dynamic, interactive frontends. The flexibility far exceeds traditional server controls while maintaining the familiar Web Forms development environment.
Feature image credit: Photo by RhondaK Native Florida Folk Artist on Unsplash