Introducing Vanilla ASP.NET Web Forms Architecture: The Craftmanship of ASP.NET Web Forms

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:

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 ComponentsTraditional Web Forms Components
HTML <input>Server Controls (TextBox, DropDownList, etc.)
Dynamic HTML TableGridView, DataList, Repeater
FetchAPIServer Control PostBack
FetchAPI + DOM ManipulationUpdatePanel
Portable JavaScript ObjectCustom User Control
URL RoutingStatic Folder + File Path (*.aspx)
JSON ResponseServer Control Databinding
RESTful EndpointsPage Methods/Web Methods
Pure CSS StylingTheme/Skin Files
Event ListenersServer 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