This is part of the series of introducing Vanilla ASP.NET Web Forms Architecture. Read more about this at the main menu:
Main Article: [Introducing Vanilla ASP.NET Web Forms Architecture]
Why JavaScript Portable Object?
ASP.NET user controls (or custom user controls) are strictly tied to the backend server and involve their own complex lifecycle that introduces ViewState and abstraction of how native web development behaves.
There is a complex mechanism to properly manipulate and control the behavior of these things to work as expected and be predictable. In the end, the user control is actually still rendered as a portable JavaScript object in the frontend.
But if you create a JavaScript object directly, you are actually spending the same amount of time engineering the component, but with the proper native foundation of web development. You can have direct control over every behavior and predict its behavior exactly as it should be. You will be bypassing the abstraction layer and working with the core of web development.
If you can spend time learning the complexity of creating a user control with various backend PostBack handling, you might as well spend the same effort rebuilding it directly as a portable JavaScript object.
The Pattern in Action
Instead of creating a UserManagement.ascx control that’s tightly coupled to Web Forms’ lifecycle, you create a UserManager JavaScript object that can be instantiated anywhere:
// Traditional approach - tied to Web Forms
<%@ Register Src="~/Controls/UserManagement.ascx" TagName="UserMgmt" TagPrefix="uc" %>
<uc:UserMgmt ID="UserMgmt1" runat="server" />
// Vanilla approach - pure flexibility
const userWidget = UserManager.create();
userWidget.init('my-container');The following code demonstrates a complete User Management widget that replaces what would traditionally require a Custom User Control, server-side data binding, postback handling, and ViewState management – all with clean, modern JavaScript that communicates via JSON APIs.
JavaScript Portable Object:
const UserManager = {
create() {
const UserWidget = {
config: {
container: null,
apiUrl: '/api/users.aspx'
},
data: {
id: 0,
name: '',
tel: '',
email: ''
},
init(containerId) {
this.config.container = document.getElementById(containerId);
if (!this.config.container) {
console.error('Container not found:', containerId);
return false;
}
this.render();
this.bindEvents();
return this;
},
render() {
const html = `
<div class="user-widget">
<h3>User Management</h3>
<div class="form-group">
<label>ID:</label>
<span class="user-id">--</span>
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" class="user-name" placeholder="Enter name">
</div>
<div class="form-group">
<label>Phone:</label>
<input type="text" class="user-tel" placeholder="Enter phone">
</div>
<div class="form-group">
<label>Email:</label>
<input type="text" class="user-email" placeholder="Enter email">
</div>
<div class="button-group">
<button type="button" class="btn-save">Save</button>
<button type="button" class="btn-load">Load User</button>
<button type="button" class="btn-delete">Delete</button>
<button type="button" class="btn-reset">Reset</button>
</div>
<input type="number" class="user-id-input" placeholder="User ID to load" min="1">
</div>
`;
this.config.container.innerHTML = html;
},
bindEvents() {
const container = this.config.container;
container.querySelector('.btn-save').addEventListener('click', () => this.save());
container.querySelector('.btn-load').addEventListener('click', () => {
const userId = container.querySelector('.user-id-input').value;
if (userId) this.loadData(parseInt(userId));
});
container.querySelector('.btn-delete').addEventListener('click', () => this.delete());
container.querySelector('.btn-reset').addEventListener('click', () => this.resetUI());
},
async fetchAPI(action, data = {}) {
try {
const formData = new FormData();
formData.append('action', action);
Object.keys(data).forEach(key => {
formData.append(key, data[key]);
});
const response = await fetch(this.config.apiUrl, {
method: 'POST',
body: formData,
credentials: 'include'
});
if (response.ok) {
const result = await response.text();
if (result.startsWith('0|')) {
return { success: false, error: result.substring(2) };
}
return { success: true, data: result };
} else {
return { success: false, error: 'Network error' };
}
} catch (error) {
return { success: false, error: error.message };
}
},
async save() {
const container = this.config.container;
const userData = {
id: this.data.id,
name: container.querySelector('.user-name').value,
tel: container.querySelector('.user-tel').value,
email: container.querySelector('.user-email').value
};
const result = await this.fetchAPI('save', userData);
if (result.success) {
alert('User saved successfully!');
if (userData.id === 0) {
this.data.id = parseInt(result.data);
container.querySelector('.user-id').textContent = this.data.id;
}
} else {
alert('Error: ' + result.error);
}
},
async loadData(userId) {
const result = await this.fetchAPI('load', { id: userId });
if (result.success) {
try {
const user = JSON.parse(result.data);
this.data = user;
this.updateUI(user);
} catch (e) {
alert('Error parsing user data');
}
} else {
alert('Error: ' + result.error);
}
},
async delete() {
if (this.data.id === 0) {
alert('No user selected to delete');
return;
}
if (confirm('Are you sure you want to delete this user?')) {
const result = await this.fetchAPI('delete', { id: this.data.id });
if (result.success) {
alert('User deleted successfully!');
this.resetUI();
} else {
alert('Error: ' + result.error);
}
}
},
updateUI(user) {
const container = this.config.container;
container.querySelector('.user-id').textContent = user.id;
container.querySelector('.user-name').value = user.name;
container.querySelector('.user-tel').value = user.tel;
container.querySelector('.user-email').value = user.email;
},
resetUI() {
const container = this.config.container;
this.data = { id: 0, name: '', tel: '', email: '' };
container.querySelector('.user-id').textContent = '--';
container.querySelector('.user-name').value = '';
container.querySelector('.user-tel').value = '';
container.querySelector('.user-email').value = '';
container.querySelector('.user-id-input').value = '';
}
};
return UserWidget;
}
};
// Usage:
// const userWidget = UserManager.create();
// userWidget.init('user-container');C# Backend: users.aspx.cs
public partial class users : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
string action = (Request["action"] ?? "").ToLower();
switch (action)
{
case "save":
SaveUser();
break;
case "load":
LoadUser();
break;
case "delete":
DeleteUser();
break;
default:
Response.Write("0|Invalid action");
break;
}
}
void SaveUser()
{
try
{
int id = int.Parse(Request["id"] ?? "0");
string name = Request["name"] ?? "";
string tel = Request["tel"] ?? "";
string email = Request["email"] ?? "";
// Database operations here
if (id == 0)
{
// Insert new user, return new ID
int newId = InsertUser(name, tel, email);
Response.Write(newId.ToString());
}
else
{
// Update existing user
UpdateUser(id, name, tel, email);
Response.Write("1");
}
}
catch (Exception ex)
{
Response.Write("0|" + ex.Message);
}
}
void LoadUser()
{
try
{
int id = int.Parse(Request["id"] ?? "0");
var user = GetUser(id);
if (user != null)
{
string json = JsonSerializer.Serialize(user);
Response.Write(json);
}
else
{
Response.Write("0|User not found");
}
}
catch (Exception ex)
{
Response.Write("0|" + ex.Message);
}
}
void DeleteUser()
{
try
{
int id = int.Parse(Request["id"] ?? "0");
bool success = RemoveUser(id);
Response.Write(success ? "1" : "0|Delete failed");
}
catch (Exception ex)
{
Response.Write("0|" + ex.Message);
}
}
}Feature image: Photo by Nina Mercado on Unsplash
