Report 1: Shop Status Grid with Multi-Level Column Headers
The first report uses a GridPanel with grouped column headers. The code manually defines every column, but the structrue is straightforward.
<%@ Register Assembly="Ext.Net" Namespace="Ext.Net" TagPrefix="ext" %>
<style>
.total-field { background:#fff; font-weight:bold; border:1px solid silver; padding:2px; }
.headRows1 { color:green; font-weight:bolder; }
.headRows2 { color:blue; font-weight:bolder; }
</style>
<script>
function getYear() { return <%=txtYear.ClientID %>.getValue(); }
function getMonth() { return <%=cboMonth.ClientID %>.getValue(); }
</script>
<ext:Viewport runat="server" Layout="Fit">
<Items>
<ext:GridPanel ID="GP1" Width="400" Height="230" StripeRows="true" Title="Shop Status" AutoExpandColumn="RptId" Frame="true">
<Store>
<ext:Store ID="store1" AutoLoad="false" runat="server">
<Proxy>
<ext:HttpProxy Method="POST" Url="/Ajax/Reports.ashx" />
</Proxy>
<Reader>
<ext:JsonReader IDProperty="RptId" TotalProperty="total" Root="data">
<Fields>
<ext:RecordField Name="RptId" />
<ext:RecordField Name="RptYear" />
<ext:RecordField Name="RptMonth" />
<ext:RecordField Name="TargetNewEntrant" />
<ext:RecordField Name="TargetOM" />
<ext:RecordField Name="TargetC4" />
<ext:RecordField Name="TargetTotal" />
<ext:RecordField Name="NewStoreNewEntrant" />
<ext:RecordField Name="NewStoreOM" />
<ext:RecordField Name="NewStoreC4" />
<ext:RecordField Name="NewStoreNCSTotal" />
<ext:RecordField Name="NewStoreHB" />
<ext:RecordField Name="NewStoreTotal" />
<ext:RecordField Name="ContractSignedNCS" />
<ext:RecordField Name="ContractSignedHB" />
<ext:RecordField Name="ContractSignedTotal" />
<ext:RecordField Name="UnderDecoNCS" />
<ext:RecordField Name="UnderDecoHB" />
<ext:RecordField Name="UnderDecoTotal" />
<ext:RecordField Name="UpgradingTarget" />
<ext:RecordField Name="UpgradingInOperation" />
<ext:RecordField Name="UpgradingContractSigned" />
<ext:RecordField Name="UpgradingUnderDeco" />
<ext:RecordField Name="ClosureNonPerform" />
<ext:RecordField Name="ClosureForcedAttrition" />
<ext:RecordField Name="ClosureTotal" />
<ext:RecordField Name="StoreStatusNetStore" />
<ext:RecordField Name="StoreStatusStoreNoNCS" />
<ext:RecordField Name="StoreStatusStoreNoHB" />
<ext:RecordField Name="StoreStatusStoreNoOld" />
<ext:RecordField Name="StoreStatusStoreNoTotal" />
<ext:RecordField Name="DisplayMonth" />
</Fields>
</ext:JsonReader>
</Reader>
<BaseParams>
<ext:Parameter Name="type" Value="ShopStatus" Mode="Value" />
<ext:Parameter Name="Year" Value="getYear()" Mode="Raw" />
<ext:Parameter Name="Month" Value="getMonth()" Mode="Raw" />
</BaseParams>
</ext:Store>
</Store>
<ColumnModel>
<Columns>
<ext:Column Header="Year" DataIndex="DisplayMonth" Locked="true" />
<ext:Column Header="ID" ColumnID="RptId" Hidden="true" DataIndex="RptId" />
<!-- remaining columns omitted for brevity -->
</Columns>
</ColumnModel>
<View>
<ext:GroupingView>
<HeaderGroupRows>
<ext:HeaderGroupRow>
<Columns>
<ext:HeaderGroupColumn Header="<font class='headRows1'>National</font>" Align="Center" />
<ext:HeaderGroupColumn Header="<font class='headRows1'>New Store</font>" ColSpan="17" />
<ext:HeaderGroupColumn Header="<font class='headRows1'>Upgrading</font>" ColSpan="4" />
<ext:HeaderGroupColumn Header="<font class='headRows1'>Closure</font>" ColSpan="3" />
<ext:HeaderGroupColumn Header="<font class='headRows1'>Store Status</font>" ColSpan="5" />
<ext:HeaderGroupColumn Align="Center" />
</Columns>
</ext:HeaderGroupRow>
<ext:HeaderGroupRow>
<Columns>
<ext:HeaderGroupColumn Header="<font class='headRows2'>Year/Month</font>" />
<ext:HeaderGroupColumn Header="<font class='headRows2'>Target</font>" ColSpan="5" />
<ext:HeaderGroupColumn Header="<font class='headRows2'>Actual Store opened</font>" ColSpan="6" />
<ext:HeaderGroupColumn Header="<font class='headRows2'>Contract Signed</font>" ColSpan="3" />
<ext:HeaderGroupColumn Header="<font class='headRows2'>Under Deco</font>" ColSpan="3" />
<ext:HeaderGroupColumn ColSpan="4" />
<ext:HeaderGroupColumn Header="<font class='headRows2'>Drop-Out</font>" ColSpan="3" />
<ext:HeaderGroupColumn />
<ext:HeaderGroupColumn Header="<font class='headRows2'>Store No.</font>" ColSpan="4" />
<ext:HeaderGroupColumn />
</Columns>
</ext:HeaderGroupRow>
</HeaderGroupRows>
</ext:GroupingView>
</View>
<TopBar>
<ext:Toolbar>
<Items>
<ext:NumberField ID="txtYear" LabelWidth="74" FieldLabel="Year" />
<ext:ComboBox ID="cboMonth" LabelWidth="74" FieldLabel="Month" Editable="false">
<Items>
<ext:ListItem Text="01" Value="1"/> ... <ext:ListItem Text="12" Value="12"/>
<ext:ListItem Text="ALL" Value=" "/>
</Items>
</ext:ComboBox>
<ext:Button ID="btnSearch" Text="Search" Icon="PageRefresh">
<Listeners>
<Click Handler="if(#{txtYear}.getValue()==''){Ext.MessageBox.alert('Info','Enter year!');}else{#{store1}.reload();}" />
</Listeners>
</ext:Button>
<ext:Button ID="btnExport" Text="Export Excel" Icon="PageExcel">
<Listeners>
<Click Handler="window.open('/FormServerTemplates/DownLoad.aspx?year='+#{txtYear}.getValue()+'&month='+#{cboMonth}.getValue(),'_newtab');" />
</Listeners>
</ext:Button>
</Items>
</ext:Toolbar>
</TopBar>
</ext:GridPanel>
</Items>
</ext:Viewport>
The server-side HTTP handler returns JSON according to the parameters. The column group rows are nested to produce two levels of headers. This approach requires careful mapping of ColSpan values.
Report 2: Decoration Status with Row Totals and Conditional Formatting
The second report includes a bottom toolbar that displays column totals. It also uses a custom renderer to hide zero values (showing blue numbers for posiitve values).
<style>
.total-field { background:#fff; font-weight:bold; padding:2px; }
.headRows1 { color:green; font-weight:bolder; }
.headRows2 { color:blue; font-weight:bolder; }
</style>
<script>
var templ = '<b style="color:blue;">{0}</b>';
var hideZero = function(val) {
return val > 0 ? String.format(templ, val) : '';
};
var refreshTotals = function(grid) {
var tb = grid.getBottomToolbar(),
cols = grid.view.getColumnData(),
sums = {};
for (var j = 0, len = grid.store.getCount(); j < len; j++) {
var rec = grid.store.getAt(j);
for (var i = 0; i < cols.length; i++) {
var c = cols[i];
if (j == 0) sums[c.name] = 0;
if (c.name == "Region") sums[c.name] = "Total:";
else sums[c.name] += rec.get(c.name);
}
}
var cm = grid.getColumnModel();
for (var i = 0; i < cm.columns.length; i++) {
var col = cm.columns[i];
if (col.dataIndex != grid.store.groupField) {
var fld = tb.findBy(function(item) { return item.dataIndex === col.dataIndex; })[0];
tb.remove(fld, false);
tb.insert(i, fld);
var w = cm.getColumnWidth(i);
fld.setWidth(w - 5);
var renderer = col.summaryRenderer || c.renderer;
fld.setValue(renderer(sums[col.dataIndex], {}, {}, 0, i, grid.store));
}
}
tb.doLayout();
};
</script>
<ext:GridPanel ID="GP2" AutoScroll="true" StripeRows="true" AutoHeight="true" Title="Decoration Status" Icon="Bookmark">
<Store>
<ext:Store ID="store2" AutoLoad="true" runat="server">
<Reader>
<ext:JsonReader>
<Fields>
<ext:RecordField Name="Region" />
<ext:RecordField Name="ncsnew" Type="Int"/>
<ext:RecordField Name="h_bnew" Type="Int"/>
<!-- remaining fields omitted -->
</Fields>
</ext:JsonReader>
</Reader>
</ext:Store>
</Store>
<ColumnModel>
<Columns>
<ext:Column DataIndex="Region" Header="Region" />
<ext:Column DataIndex="ncsnew" Header="New NCS">
<Renderer Fn="hideZero" Args="value,metadata,record,rowIndex,colIndex,store" />
</ext:Column>
<!-- other columns with same renderer -->
</Columns>
</ColumnModel>
<Listeners>
<ColumnResize Handler="refreshTotals(this);" />
<AfterRender Handler="refreshTotals(this);" Delay="100" />
</Listeners>
<View>
<ext:GroupingView />
</View>
<BottomBar>
<ext:Toolbar>
<Items>
<ext:DisplayField Cls="total-field" DataIndex="Region" Text="-" />
<ext:DisplayField Cls="total-field" DataIndex="ncsnew" Text="-" />
<!-- one DisplayField per data column -->
</Items>
</ext:Toolbar>
</BottomBar>
</ext:GridPanel>
The store is populated server-side by binding a DataTable to Store1.DataSource and calling DataBind(). Notice the ColumnResize listener recalculates footer values. Column header groups can be added dynamically via HeaderGroupRow objects created in the code-behind, as shown in the third report.
Report 3: Fully Dynamic Grid with Server-Side Generation
The third approach generates columns, fields, and header groups entirely in C#. The ASPX page is minimal:
<ext:GridPanel ID="GP3" AutoScroll="true" StripeRows="true" AutoHeight="true" Title="ShopStatus" Icon="Date">
<Store>
<ext:Store ID="store3" AutoLoad="true" runat="server" />
</Store>
<ColumnModel ID="cm3" />
<View>
<ext:GroupingView ID="gv3" />
</View>
<BottomBar>
<ext:Toolbar>
<Items>
<ext:GridPanel ID="gpItems" AutoScroll="true" HideHeaders="true" Title="Sub">
<Store>
<ext:Store ID="storeItems" />
</Store>
<ColumnModel />
</ext:GridPanel>
</Items>
</ext:Toolbar>
</BottomBar>
</ext:GridPanel>
The code-behind (Page_Load) builds everything:
protected void Page_Load(object sender, EventArgs e)
{
if (IsPostBack) return;
// Bind dropdowns, get area/year/month from query string
DataTable dt = _cm.SelectReportByStoredName("SelectMonthDataAndWeekData", parameters);
if (dt == null || dt.Rows.Count == 0) { X.Msg.Alert("Info", "No data").Show(); return; }
// Filter data: main grid gets rows without "-", "MTD", etc.; sub grid gets the rest
var mainData = dt.AsEnumerable().Where(r => !r[0].ToString().Contains("-") && r[0].ToString() != "MTD").AsDataView();
BindData(mainData, GP3, store3);
// ... similar for sub grid
// Set column headers manually after binding
GP3.ColumnModel.Columns[0].Header = areaName;
// ... etc.
// Build header group rows
var hgr = new HeaderGroupRow();
hgr.Columns.Add(new HeaderGroupColumn { Header = "<font class='headRows1'>In operation</font>", ColSpan = 4 });
hgr.Columns.Add(new HeaderGroupColumn { Header = "<font class='headRows2'>New NCS</font>", ColSpan = 3 });
// ... add more
gv3.HeaderGroupRows.Add(hgr);
}
private void BindData(DataView dv, GridPanel gp, Store store)
{
var reader = new JsonReader();
foreach (DataColumn dc in dv.Table.Columns)
{
reader.Fields.Add(new RecordField(dc.ColumnName));
var col = new Column { Header = dc.ColumnName, DataIndex = dc.ColumnName };
if (dc.ColumnName != "timecol")
{
col.Renderer.Fn = "hideZero";
col.Renderer.Args = new[] { "value" };
}
gp.ColumnModel.Columns.Add(col);
}
store.Reader.Add(reader);
store.DataSource = dv;
store.DataBind();
}
Key takeaways:
- When using dynamic generation, set
Typeattributes for numeric/date fields to avoid JavaScriptundefined. - Column header groups can be created server-side and added to the
GroupingView. They suport nested rows. - The
BottomBarcan host anotherGridPanelfor split data display (e.g., weekly breakdowns). - Use
Server.HtmlEncodewhen header text contains special characters like&to prevent rendering issues. - JavaScript renderers help conditionally style cell values (e.g., hide zeros).