Table of Contents

INTRODUCTION
During a recent engagement with a client in the logistics and supply chain sector, our team was tasked with stabilizing a mission-critical legacy dashboard. The system was a high-traffic ASP.NET WebForms application used by dispatchers to monitor thousands of active shipments. The core of this dashboard was a data-heavy grid that grouped shipments by status, region, and carrier.
The problem surfaced during peak operational hours. Dispatchers reported that simply clicking to expand a group (e.g., opening the “In Transit” group) caused the interface to freeze for several seconds. We realized that what should have been a lightweight client-side interaction was triggering a full server-side lifecycle event.
Instead of just displaying pre-loaded data, the application was re-executing heavy SQL queries against the production database for every single UI interaction. This article details how we diagnosed this lifecycle conflict and implemented a caching layer to prevent unnecessary data reloads.
PROBLEM CONTEXT
The application relied heavily on the DevExpress ASPxGridView control to present tabular data. The business requirement allowed users to group records dynamically to view aggregated data. Under the hood, the architecture was relatively standard for its time:
- Frontend: ASP.NET WebForms with DevExpress UI controls.
- Backend: SQL Server.
- Data Access: ADO.NET with raw SQL queries constructed in the code-behind.
The grid was configured to load data inside the Page_Init event. The developers’ original intent was to ensure data was always available for the grid to render. However, this implementation failed to account for the way DevExpress handles callbacks. When a user expanded a group row, the grid performed a callback to the server to render the new rows. Because the data loading logic was unconditional in the initialization phase, the server re-fetched the entire dataset from SQL Server before determining which specific rows to render.
WHAT WENT WRONG
The performance bottleneck was caused by a misunderstanding of the ASP.NET page lifecycle in conjunction with DevExpress callbacks.
The original implementation looked similar to this:
protected void Page_Init(object sender, EventArgs e) {
// This executes on every PostBack AND Callback
LoadShipmentData();
}
private void LoadShipmentData() {
// Expensive SQL operation running 50+ times per session
DataTable dt = ExecuteHeavyQuery();
gvShipments.DataSource = dt;
gvShipments.DataBind();
}Even though the grid had EnableRowsCache="True", it was rendered ineffective. By explicitly calling DataBind() inside Page_Init on every request, we were forcing the grid to discard its cached structure and accept “fresh” data, which triggered the expensive SQL query every time a user clicked a plus sign.
From a database perspective, this generated an I/O storm. A simple UI navigation action by 100 concurrent users resulted in thousands of redundant execution plans being generated per minute.
HOW WE APPROACHED THE SOLUTION
To resolve this, we needed to separate the Data Retrieval (getting data from SQL) from the Data Binding (linking data to the Grid).
1. Caching Strategy
We needed a temporary storage mechanism to hold the data set across callbacks. Since this was a WebForms application with moderate concurrency, we opted to use the server-side Session object (or Cache object depending on scope) to store the DataTable. This ensures the SQL query runs only once per user session or until the data is explicitly refreshed.
2. Conditional Binding
We had to refactor the Page_Init logic. Instead of blindly loading data, we shifted the responsibility to the Grid’s own DataBinding event. This ensures that data is only fetched when the Grid explicitly requests it, rather than forcing it during page initialization.
3. Handling Callbacks
We utilized the IsCallback and IsPostBack properties to determine if a full reload was actually necessary.
FINAL IMPLEMENTATION
The solution involved modifying the code-behind to prioritize cached data. We implemented a pattern where the application first checks for existing data in the Session before attempting to contact the database.
Here is the sanitized, generic version of the corrected implementation:
protected void Page_Init(object sender, EventArgs e) {
// Only bind if we are not causing a full reload, or let the grid handle it internally
// We attach the event handler here, but we do NOT call DataBind() explicitly
gvGenericList.DataBinding += GvGenericList_DataBinding;
}
protected void Page_Load(object sender, EventArgs e) {
if (!IsPostBack && !IsCallback) {
// Initial load only
gvGenericList.DataBind();
}
}
protected void GvGenericList_DataBinding(object sender, EventArgs e) {
// Assign the data source from our caching method
gvGenericList.DataSource = GetCachedData();
}
private DataTable GetCachedData() {
// Check if data exists in Session to avoid SQL trip
if (Session["SessionDataKey"] == null) {
// Data not in cache, execute SQL
DataTable dt = FetchDataFromDatabase();
Session["SessionDataKey"] = dt;
}
// Return cached data
return (DataTable)Session["SessionDataKey"];
}
private DataTable FetchDataFromDatabase() {
// Standard ADO.NET fetching logic
// This now runs ONLY once per session or on explicit refresh
DataTable dt = new DataTable();
using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["AppDb"].ConnectionString)) {
using (SqlCommand cmd = new SqlCommand("SELECT * FROM Shipments WHERE Status = @Status", conn)) {
conn.Open();
using (SqlDataAdapter da = new SqlDataAdapter(cmd)) {
da.Fill(dt);
}
}
}
return dt;
}
Key Architectural Changes:
- Event-Driven Binding: Moving logic to
OnDataBindingallows the Grid to control when it needs data. - Session Caching: The SQL query executes once. Subsequent grouping, sorting, and paging operations retrieve the
DataTablefrom memory, which is effectively instant. - Reduced Latency: Group expansion time dropped from ~4 seconds to <200ms.
LESSONS FOR ENGINEERING TEAMS
This optimization provided several key takeaways for engineering teams handling legacy modernization or data-intensive web applications:
- Understand the Lifecycle: In ASP.NET (and even modern frameworks like React), understanding exactly when data is requested is as important as how it is requested.
- Don’t Fight the Framework: DevExpress and Telerik controls have built-in caching mechanisms. Manually forcing data binds often overrides these optimizations.
- Cache Near the Consumption: For read-heavy views where real-time data isn’t critical (second-by-second), caching the result set in memory drastically reduces database costs.
- Instrument Your Queries: We only found this because we monitored SQL profilers during UI interactions. Always verify that a UI click doesn’t trigger a backend avalanche.
- Legacy Code Requires Specialized Care: Modernizing WebForms requires specific expertise. It is often more cost-effective to optimize the existing logic than to rewrite the entire platform.
WRAP UP
By shifting from unconditional data loading to a cached, event-driven model, we eliminated 95% of the database load generated by the dashboard. This ensured that dispatchers could group and filter data instantly, restoring faith in the system’s reliability. Whether you are maintaining legacy systems or building new cloud-native architectures, understanding data flow efficiency is critical.
If you need assistance optimizing high-performance dashboards or modernizing legacy .NET applications, contact us to discuss your engineering needs.
Frequently Asked Questions
The GridView reloads data if the data binding logic is placed unconditionally in Page_Init or Page_Load. During a callback (like sorting or grouping), the server recreates the page lifecycle. If you explicitly call DataBind() without checking for IsCallback or using a caching mechanism, the database is queried again.
For purely client-side grouping without any server interaction, you must use "Batch" mode or load all data into the client model. However, for large datasets, this is often impractical. The best hybrid approach is to use server-side binding with aggressive server-side caching (as described above) so the callback is lightweight.
For WebForms applications with moderate user counts, Session state is effective. However, for scalable, distributed systems, you should use a distributed cache like Redis. This allows you to hire dotnet developers for enterprise modernization who can implement out-of-process caching strategies that survive application pool recycles.
If your internal team is struggling with legacy WebForms performance or lacks deep knowledge of third-party component lifecycles (DevExpress/Telerik), it is time to bring in experts. You can hire ASP.NET developers for legacy optimization to refactor the data layer without risking system stability.














