Building Report Grids with EXT.NET Complex Layouts

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 Type attributes for numeric/date fields to avoid JavaScript undefined.
  • Column header groups can be created server-side and added to the GroupingView. They suport nested rows.
  • The BottomBar can host another GridPanel for split data display (e.g., weekly breakdowns).
  • Use Server.HtmlEncode when header text contains special characters like & to prevent rendering issues.
  • JavaScript renderers help conditionally style cell values (e.g., hide zeros).

Tags: ext.net GridPanel GroupingView HeaderGroupRow ASP.NET

Posted on Sun, 09 Aug 2026 16:50:11 +0000 by methodman