Introduction
This article discusses how to create dynamic columns in listview
control.
Background
Creating a GridView
with dynamic columns generated is easy, it even comes without much effort by simply configuring the AutoGenerateColumns
property.
Using the Code
First, we create a Employee-vs-Vendor table, using the Common Table Expressions(CTE) and PIVOT. Check it from the source attached.
On the listview
layout template, we have to declare a placeholder:
<LayoutTemplate>
<div class="listviewGrid">
<table id="tblViewHdr" class="CollapsableExpandable" cellpadding="0"
cellspacing="0" style="word-break:break-all;word-wrap:break-word">
<tr class="head">
<asp:PlaceHolder runat="server" ID="phDynamicHdr" />
</tr>
<tr id="itemPlaceholder" runat="server" />
</table>
</div>
</LayoutTemplate>
The CSS style used is to break the words if you specify too many columns.
Then in the ItemTemplate
, we do not need to add extra dynamic columns we want, since it's going to be done programmatically.
<ItemTemplate>
<tr id="row" runat="server" class="item">
<td class="first">
<img src="Images/minus.png" />
</td>
</tr>
<asp:ListView runat="server"
onitemdatabound="lst1stLevel_ItemDataBound" ID="lst1stLevel">
<%-- more contents here--%>
</asp:ListView>
</ItemTemplate>
Depending on the level of the query, you have to create a nested ItemTemplate
accordingly. I believe this can be done through AJAX. However, I leave this to the future.
Then when we start binding the query to the listview
, we bind it level-by-level from the SQL retrieved from CTE & PIVOT above.
public void bindListView(DataTable dtEmpID)
{
DataTable dt = new DataTable();
dt = ((DataTable)ViewState["cachedTable"]).Clone();
if (((DataTable)ViewState["cachedTable"]).Rows.Count > 0)
{
DataRow[] drResults = ((DataTable)ViewState
["cachedTable"]).Select("GENERATION = 0");
foreach (DataRow dr in drResults)
{
object[] row = dr.ItemArray;
dt.Rows.Add(row);
}
}
lsvHighLvlFormAccess.DataSource = dt;
lsvHighLvlFormAccess.DataBind();
}
When highest level binding is done, we can populate the header caption. This is the part when you set the column-span & generate the column header dynamically.
protected void lsvHighLvlFormAccess_DataBound
(object sender, EventArgs e)
{
PlaceHolder phDynamicHdr =
(PlaceHolder)lsvHighLvlFormAccess.FindControl("phDynamicHdr");
if (phDynamicHdr != null )
{
Literal ltrl = new Literal();
DataTable dt = ((DataTable)ViewState["cachedTable"]);
if (dt != null && dt.Rows.Count > 0)
{
foreach (DataColumn dc in dt.Rows[0].Table.Columns)
{
if (!(dc.ColumnName.Equals("GENERATION") ||
dc.ColumnName.Equals("hierarchy") ||
dc.ColumnName.Equals("rowNo") ||
dc.ColumnName.Equals("EmployeeID")))
{
if (dc.ColumnName.Equals("LoginID"))
{
ltrl.Text += "<th colspan=" + (I_COLSPAN + 1) +
">" + dc.ColumnName + "</th>";
}
else
ltrl.Text += "<th>" + dc.ColumnName + "</th>";
}
}
}
if (phDynamicHdr.Controls.Count > 0)
{
if (!((Literal)phDynamicHdr.Controls[0]).Text.Equals(ltrl.Text))
{
phDynamicHdr.Controls.Remove(phDynamicHdr.Controls[0]);
phDynamicHdr.Controls.Add(ltrl);
}
}
else
phDynamicHdr.Controls.Add(ltrl);
}
HtmlTableCell td =
(HtmlTableCell)lsvHighLvlFormAccess.FindControl("imgCollapseExpand");
if (td != null)
{
DataTable dt = ((DataTable)ViewState["cachedTable"]);
if (dt != null && dt.Rows.Count > 0)
{
td.ColSpan = dt.Rows[0].Table.Columns.Count +
I_COLSPAN - 1; }
}
}
On each level row is bound, we need to fire the event to populate its descendant. Here, checkboxes are used. You can replace with any other control too.
protected void lsvHighLvlFormAccess_ItemDataBound
(object sender, ListViewItemEventArgs e)
{
HtmlTableRow row = (HtmlTableRow)e.Item.FindControl("row");
ListViewDataItem item = (ListViewDataItem)e.Item;
System.Data.DataRowView drv = (System.Data.DataRowView)item.DataItem;
dynamicPopulateRow(row, drv, 0);
}
where dynamicPopulateRow()
is:
private void dynamicPopulateRow(HtmlTableRow row,
System.Data.DataRowView drv, int iGeneration)
{
if (row != null)
{
foreach (DataColumn dc in drv.Row.Table.Columns)
{
string sEmployeeID = drv["LoginID"].ToString();
if (dc.ColumnName.Equals("LoginID"))
{
HtmlTableCell cell = new HtmlTableCell("td");
cell.Controls.Add(new LiteralControl
(Convert.ToString(drv[dc.ColumnName])));
cell.ColSpan = dc.ColumnName.Equals
("LoginID") ? I_COLSPAN - iGeneration : 1;
row.Cells.Add(cell);
}
else if (!(dc.ColumnName.Equals("GENERATION") ||
dc.ColumnName.Equals("hierarchy") ||
dc.ColumnName.Equals("rowNo") ||
dc.ColumnName.Equals("EmployeeID")))
{
HtmlTableCell cell = new HtmlTableCell("td");
bool bIsNull = drv[dc.ColumnName] is System.DBNull;
Literal ltrl = new Literal();
ltrl.Text += "<input type=\"checkbox\"
name=\"" + dc.ColumnName + "\"" +
(bIsNull ? "" : " value=" +
drv[dc.ColumnName].ToString()) +
" id=\"" + sEmployeeID + "~" +
dc.ColumnName.Replace(" ", "_") +
"\"" + " onclick=\"didModify(this)\" " +
(bIsNull ? " disabled" : "") +
(!bIsNull && ((int)drv[dc.ColumnName]) > 0 ?
" checked>" : ">");
cell.Controls.Add(ltrl);
row.Cells.Add(cell);
}
else
{
}
}
}
}
If you more nested levels, just add these codes at the end of ItemDataBound
event:
var lst1stLevel = (ListView)e.Item.FindControl("lst1stLevel");
populateLV(lst1stLevel, 1, (string)drv["hierarchy"], Convert.ToInt32(drv["rowNo"]));
where populateLV()
is:
private void populateLV(ListView lv,
int iNextGeneration, string sHierarchy, int iCurrRowNo)
{
if (lv != null)
{
DataTable dt = new DataTable();
dt = ((DataTable)ViewState["cachedTable"]).Clone();
List<int /> levels = ((DataTable)ViewState["cachedTable"]).
Select("GENERATION = " +
(iNextGeneration > 0 ? iNextGeneration - 1 : 0) +
" AND hierarchy LIKE '" + sHierarchy + "%'"). AsEnumerable().
Select(al => Convert.ToInt32
(al.Field<object>("rowNo"))).Distinct().ToList();
if (levels.Count > 0 && levels.Min() == iCurrRowNo)
{
DataRow[] drResults = ((DataTable)ViewState["cachedTable"]).
Select("GENERATION = " +
iNextGeneration + " AND hierarchy LIKE '" +
sHierarchy + " %'");
foreach (DataRow dr in drResults)
{
object[] obRow = dr.ItemArray;
dt.Rows.Add(obRow);
}
lv.DataSource = dt;
lv.DataBind();
}
}
}
Repeat this for second-level, third-level, etc.
protected void lst2ndLevel_ItemDataBound(object sender, ListViewItemEventArgs e)
{
HtmlTableRow row = (HtmlTableRow)e.Item.FindControl("row");
ListViewDataItem item = (ListViewDataItem)e.Item;
System.Data.DataRowView drv = (System.Data.DataRowView)item.DataItem;
int iCurrLvl = 2;
dynamicPopulateRow(row, drv, iCurrLvl);
var lst3rdLevel = (ListView)e.Item.FindControl("lst3rdLevel");
populateLV(lst3rdLevel, iCurrLvl + 1, (string)drv["hierarchy"],
Convert.ToInt32(drv["rowNo"]));
}
Done. That's it.
Points of Interest
By using AJAX, we can dynamically populate the nested level as well.
History
- 5th June, 2013: Initial post