Few days back, we got into a very interesting performance problem -- To display around 7K data grid entries over a wide spectrum of internet connections. We had to solve the following --
- Display of humungous data set in the Grid View in the fastest time possible
- To reduce the time it takes a user to review these entries. Thus, we couldn't load the data from the persistent store on every demand.
The conceptual solution that manifested itself was --
- To create effective data segmentation and to present the aggregates first;Our aggregates are only two levels deep.
- To Pre-fetch and build the server side cache for the detail line items in the aggregates while the user consumes the header level aggregates
- Use Atlas to ashynchronously retrieve the data from the server side when the user expands one of the aggregates -- The server side returns the data if it has been cached by the background worker thread. If not, it makes a synchronous call to the persistent storage to retrieve the data, caches the entry and then returns the data back to the client.
Of course, the actual solution will be not to use the background worker threads because they are neither scalable nor robust. But, I just wanted to validate the concept and it works! I will be posting the real production design in the coming days.
Here is the technical breakdown of the concept --
1. Data Segmentation & Atlas Code --
Have a GridView within GridView(the nested GridView is hosted within a table, which sits inside a ItemTemplate). Nesting of child GridView within a table gives it a more cleaner look -- My colleague Sajay came up with this idea as he just couldn't bear the screen design that I initially had :)
Host the parent GridView under an UpdatePanel
<atlas:UpdatePanel runat="server" ID="up_GridView">
<ContentTemplate>
<asp:GridView ID="gridParent" runat="server" AutoGenerateColumns="False" OnRowCommand="GridView1_RowCommand"
OnSelectedIndexChanged="gridParent_SelectedIndexChanged" Width="100%">
<Columns>
<asp:TemplateField ShowHeader="true" HeaderText="Select">
<ItemTemplate >
<asp:LinkButton ID="LinkButton1" runat="server" CausesValidation="False" CommandName="Select"
Text="+"></asp:LinkButton>
</ItemTemplate>
<ItemStyle VerticalAlign="Top" />
</asp:TemplateField>
<asp:BoundField DataField="Name" HeaderText="Name">
<ItemStyle Width="20%"></ItemStyle>
</asp:BoundField>
<asp:TemplateField ControlStyle-Width="80%">
<ItemTemplate>
<table width="100%">
<tr>
<td width="100%">
<asp:GridView ID="gridActivities" runat="server" AutoGenerateColumns="False" Width="100%" CellPadding="3" GridLines="Horizontal">
<
Columns>
//Child Columns here
</
Columns>
</
asp:GridView>
</td>
</tr>
</table>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
</ContentTemplate>
</atlas:UpdatePanel>
2. Prefetching of data --
/// This class is a singleton
public class TEManager
{
private static TEManager _instance = null;
private static object _lockObject = new Object();
private List<Parent> _parentList = new List<Parent>();
/// <summary>
/// This class provides the only instance of the TEManager to the caller
/// </summary>
/// <returns></returns>
public static TEManager GetInstance()
{
lock (_lockObject )
{
if (_instance == null)
{
_instance =
new TEManager();
}
}
return _instance;
}
public List<Parent> GetParentList()
{
//Build the parent list from the persistent storage
//Queue the thread so that we can start building activities for the loaded resources
ThreadPool.QueueUserWorkItem(new WaitCallback(BuildChildCache));
return _parentList;
}
protected void BuildChildCache(object state)
{
if ( _parentList != null && _parentList.Count > 0)
{
foreach (Parent parent in _parentList)
{
if ( parent.Children == null )
{
List<Children> children = GetChildrenFromPersistentStorage( parent.ID );
//Lock the parent object so that the synchronous thread can't get to it
lock( parent )
{
parent.Children = children;
}
}
}
}
}
public
List<Child> GetChildren(string parentID)
{
List<Child> children = GetChildrenFromCache( parentID );
if (children == null)
{
List<Child> children = GetChildrenFromPersistentStorage( parentID );
Parent parent = GetParentFromCache(parentID);
//lock the parent so that the background thread can't get to it
lock( parent )
{
parent.Children = children;
}
}
return children;
}
private TEManager()
{
}
}
Now, all that needs to be done is to pass the parentID of the selected row on the SelectedIndexChanged event to the TEManager class to retrieve its children. The UpdatePanel takes care of creating a XMLHttpRequest object and loading the children asynchronously.
Happy Programming!