GridView control to show master-child or master-slave data, written in c#, asp.net, and javascript.
PRE-REQUISITE:
I previously made a master slave datagrid using Visual Studio 2003 .Net (original article) that would allow users to show data to clients using collapse and expand features. These features however did not carry over to gridview for many users. This tutorial will explaing how to create a collapsable/expandable gridview in frameworks 2.0 and higher. If you have never seen the previous article, then just read the comments throughout to understand the code flow. To use the provided source code, you will need the Northwind database. If you do not have it, you can get it from here.
SCREENSHOTS:
Default screen of how grid view looks:
Expanding rows of the gridview:

Raising an event ouside of the gridview keeps expanded and collapse states:

Sample paging of the gridview:

Sample of expanding a row in the new page:

CHANGES:The datagrid and gridview have many different methods. When we used the datagrid, we had modified the ItemDataBound method using the ItemIndex. This will be modified to use the RowDataBound method with the RowIndex objects. We all dealt with columns that would have the primary keys of our Northwind Database tables. When using ItemDataBound with the datagrid, the cells would have access to these contents, however with the gridview this is not achievable if the columns are marked with Visible = false;
We will also modify the paging and sorting with respect to our gridview.
WHAT STEPS ARE NEEDED TO MAKE A MASTER/SLAVE GRIDVIEW (HGRID):
Steps:
1. Add a gridview
2. Configure gridview and make sure first column has Hyperlink field with text as [+].
3. Configure other column along with the ID fields you need for your child/slave data.
4. Add link to the js file provided by this example using the standar ");
}
}
}
}
}
Next we will define the events of what is in our hidden textbox, which contains all the expanded rows information.
This information is delimeted by a comma, which we can later parse.
protected void ButtonSample_Click(object sender, System.EventArgs e)
{
LabelPostBack.ForeColor = System.Drawing.Color.DarkRed;
if (txtExpandedDivs.Text.Length == 0)
{
LabelPostBack.Text = "A Postback has occurred. txtExpandedDivs has no content!";
}
else
{
LabelPostBack.Text = "A Postback has occurred. txtExpandedDivs has contents:
" +
this.txtExpandedDivs.Text + "";
}
}
The following below will define all our database methods. Here we will see how to bind and query from our database.
Here we will do a simple query to fetch data from our Northwin database. A stored procedure can also be called here, but for
simplicity, we will keep it here.
#region Database Methods
private void BindData()
{
//========Query For Master Rows===
string QueryString = "SELECT OrderID, CustomerID, EmployeeID, ShipName as Name, " +
"ShipAddress as Address, " +
"ShipCity as City, " +
"ShipRegion as Region, ShipPostalCode as Postal, ShipCountry as Country FROM Orders";
if (ViewState["sortby"] != null)
{
QueryString = QueryString + " order by " +
ViewState["sortby"].ToString();
}
//==============================
System.Data.SqlClient.SqlConnection conn = new
System.Data.SqlClient.SqlConnection();
try
{
conn.ConnectionString = connectionstring;
if (conn.State == System.Data.ConnectionState.Closed)
{
conn.Open();
}
System.Data.SqlClient.SqlDataAdapter adapter =
new System.Data.SqlClient.SqlDataAdapter(QueryString, conn);
DataSet ds = new DataSet();
adapter.Fill(ds);
GridView1.DataSource = ds;
GridView1.DataBind();
}
catch (Exception ex1)
{
Response.Write("An error has occurred: ");
Response.Write(ex1.Message.ToString());
Response.End();
}
finally
{
if (conn.State == System.Data.ConnectionState.Open)
{
conn.Close();
}
}
}
The RunQuery method will simpley open a connection to the database, perform a query, and return the result to a dataset.
If you are using a database other than MSSQL, this method will need to change. As you can see we are using teh SqlClient
which will not work with MySQL or Oracle databases.
private DataSet RunQuery(string QueryString)
{
System.Data.SqlClient.SqlConnection conn = new
System.Data.SqlClient.SqlConnection();
try
{
conn.ConnectionString = connectionstring;
if (conn.State == System.Data.ConnectionState.Closed)
{
conn.Open();
}
System.Data.SqlClient.SqlDataAdapter adapter = new
System.Data.SqlClient.SqlDataAdapter(QueryString, conn);
DataSet ds = new DataSet();
adapter.Fill(ds);
return ds;
}
catch (Exception ex1)
{
Response.Write("An Error has occurred.
");
Response.Write(ex1.Message.ToString());
Response.End();
//This line below will never execute.
return null;
}
finally
{
if (conn.State == System.Data.ConnectionState.Open)
{
conn.Close();
}
}
}
#endregion
#region GridView Methods
The RowDataBound method will perform the details query from the Northwind Database. First we will check if the current row type is a data row. If it is a data
row, we will look up the id columns using the e.Row.Cell[#].Text and pass it in for our subquery. You can think of it in simpler terms. Imagine we have two
tables ( User and Address)
The first table is a user table, and another is an Address table for all the addresses a user has lived in. The main query in the BindData would handle all the records
from the User table. The RowDataBound method fetch all the addresses from the Address table based on the user.
protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
//If your page size is 10,
//only 10 sub queries will be done.
if (e.Row.RowType == DataControlRowType.DataRow)
{
string DetailsQuery = "SELECT C.CompanyName, OD.UnitPrice, OD.Quantity, OD.Discount, E.FirstName, E.LastName " +
"FROM [Order Details] as OD, [Customers] as C, [Orders] as O, [Employees] as E " +
"where OD.OrderID = O.OrderID and O.CustomerID = C.CustomerID and O.EmployeeID = E.EmployeeID and " +
"O.OrderID = '" + e.Row.Cells[1].Text + "' and " +
"C.CustomerID = '" + e.Row.Cells[2].Text + "' and " +
"E.EmployeeID = '" + e.Row.Cells[3].Text + "'";
//Here I am grabbing the additional data and putting it
//into mini datagrids...
//If you wish to just use labels, or other controls,
//just bind the data as you
//wish, and render to html as I did.
DataSet ds = this.RunQuery(DetailsQuery);
GridView NewDg = new GridView();
NewDg.AutoGenerateColumns = true;
NewDg.Width = Unit.Percentage(100.00);
NewDg.Font.Size = 8;
NewDg.Font.Name = "Tahoma";
NewDg.HeaderStyle.HorizontalAlign = HorizontalAlign.Left;
NewDg.BorderColor = System.Drawing.Color.Black;
if (ds == null || ds.Tables.Count == 0 || ds.Tables[0].Rows.Count == 0)
{
NewDg.ShowFooter = true;
}
NewDg.DataSource = ds;
NewDg.DataBind();
System.IO.StringWriter sw = new System.IO.StringWriter();
System.Web.UI.HtmlTextWriter htw = new System.Web.UI.HtmlTextWriter(sw);
NewDg.RenderControl(htw);
string DivStart = "
e.Row.RowIndex.ToString() +
"' style='DISPLAY: none; HEIGHT: 1px;'>";
string DivBody = sw.ToString();
string DivEnd = "
";
string FullDIV = DivStart + DivBody + DivEnd;
int LastCellPosition = e.Row.Cells.Count - 1;
int NewCellPosition = e.Row.Cells.Count - 2;
e.Row.Cells[0].ID = "CellInfo" +
e.Row.RowIndex.ToString();
//Match color of div which we will expand base on row
if (e.Row.RowIndex % 2 == 0)
{
//set to regular row style
e.Row.Cells[LastCellPosition].Text = e.Row.Cells[LastCellPosition].Text +
"" + FullDIV;
}
else
{
//set to alternative row style
e.Row.Cells[LastCellPosition].Text = e.Row.Cells[LastCellPosition].Text +
"" + FullDIV;
}
e.Row.Cells[0].Attributes["onclick"] = "HideShowPanel('uniquename" +
e.Row.RowIndex.ToString() + "'); ChangePlusMinusText('" +
e.Row.Cells[0].ClientID + "'); SetExpandedDIVInfo('" +
e.Row.Cells[0].ClientID + "','" + this.txtExpandedDivs.ClientID +
"', 'uniquename" + e.Row.RowIndex.ToString() + "');";
e.Row.Cells[0].Attributes["onmouseover"] = "this.style.cursor='pointer'";
e.Row.Cells[0].Attributes["onmouseout"] = "this.style.cursor='pointer'";
}
}
The RowCreated method will be used to hide our columns which have data we do no with the user to see.
In this example, Row 1, Row 2, and Row 3 hold id fields which we do no want to display. Here we just set the Visible property to false.
If we had set the visible property to false when using the wizard, our RowDataBound method would not be able to see the id's of the fields.
protected void GridView1_RowCreated(object sender, GridViewRowEventArgs e)
{
//Hide ID columns from user
if (e.Row.RowType == DataControlRowType.Header ||
e.Row.RowType == DataControlRowType.DataRow)
{
e.Row.Cells[1].Visible = false;
e.Row.Cells[2].Visible = false;
e.Row.Cells[3].Visible = false;
}
}
When a new page is chosen, we will clear our textbox which holds all the expanded rows. Next we will simply point to the new PageIndex,
and bind the data.
protected void GridView1_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
//clear expanded rows, this is a new page
txtExpandedDivs.Text = "";
BindData();
GridView1.PageIndex = e.NewPageIndex;
GridView1.DataBind();
}
#endregion
JAVASCRIPT METHODS:
Below I will show the javascript methods used to collapse and expand the the rows.
//This method will switch the plus to a minus and vice versa on the client side
function ChangePlusMinusText(PlusMinusCellID)
{
try
{
var PlusMinusCellObj = document.all(PlusMinusCellID);
if ( PlusMinusCellObj != null )
{
if ( PlusMinusCellObj.innerHTML == "[+]")
{
PlusMinusCellObj.innerHTML = "[-]";
}
else
{
PlusMinusCellObj.innerHTML = "[+]";
}
}
}
catch(e)
{
alert( "Error in ChangePlusMinusText Method: " + e);
}
}
//After a postback occurs, this method will re-expand old data.
function ShowExpandedDivInfo(TextBoxIDOfExpandedDiv, DataGridID)
{
try
{
var DIVTB = document.all(TextBoxIDOfExpandedDiv);
if ( DIVTB != null )
{
var data = DIVTB.value;
if ( data != null && data.length > 0 && data != ",")
{
var SplitData = data.split(",");
//Temp variables
var SplitDataOfArrayItem = "";
var DivIdentifier = "";
var PlusMinusCellIdentifier = "";
for ( var i = 0; i < SplitData.length; i++)
{
//Format of textbox data
//NameOfDIV + "@" + PlusMinusCellID
SplitDataOfArrayItem = SplitData[i].split("@");
//Get the ID of the DIV section
DivIdentifier = SplitDataOfArrayItem[0];
//Get the ID of the Cell with the Plus or Minus showing collapsed or expanded state.
PlusMinusCellIdentifier = SplitDataOfArrayItem[1];
if ( DivIdentifier != null && DivIdentifier.length > 0 )
{
//Expand DIV Section
ShowPanel( DivIdentifier );
}
if ( PlusMinusCellIdentifier != null && PlusMinusCellIdentifier.length > 0 )
{
var PlusMinusCellObj = document.all(PlusMinusCellIdentifier);
if ( PlusMinusCellObj != null )
{
//We found the cell to expand
PlusMinusCellObj.innerHTML = "[-]";
}
else
{
//We DID NOT FIND the cell to expand
//May Have Been Lost After Postback.
//Invoke Method to get id of cell using partial data which will always
//be unique...
var NewPlusMinusCellObjID = getItem(PlusMinusCellIdentifier, DataGridID);
if ( NewPlusMinusCellObjID != null )
{
var NewCellObject = document.all(NewPlusMinusCellObjID);
if ( NewCellObject != null )
{
var rows = NewCellObject.getElementsByTagName("td");
if ( rows != null && rows.length >= 0)
{
//the initial row has to be set to expanded....
try
{
rows[0].innerHTML = "[-]";
}
catch(e)
{
//alert("Error in setting row
//to expanded in ShowExpandedDivInfo Method.
//Error 101: " + e);
}
}//end of rows if statement
}//end of NewCellObject != null
}//end of NewPlusMinusCellObjID != null
}//end of PlusMinusCellObj != null if else
}//end of PlusMinusCellIdentifier != null
}//end of for loop
}
}
}
catch(e)
{
alert( "Error in ShowExpandedDivInfo Method: " + e);
}
}
//This will keep the data in the hidden textbox field.
function SetExpandedDIVInfo(PlusMinusCellID, HiddenTextBoxToHoldDivInfo, DIVInfoID)
{
try
{
var IsExpanded = false;
var HTBObj = document.all(HiddenTextBoxToHoldDivInfo);
var HLObj = document.all(PlusMinusCellID);
if ( HLObj != null && HTBObj != null)
{
if ( HLObj.innerHTML == "[+]")
{
IsExpanded = false;
}
else
{
IsExpanded = true;
}
//What is in the hidden DIV Textbox field?
var ExpandedData = HTBObj.value;
if ( ExpandedData == null || ExpandedData == "," )
{
ExpandedData = "";
}
//===============================================================
if ( ExpandedData.length < 1 && IsExpanded == true )
{
//No Previous Expanded Data.
//Add new Expanded Field.
ExpandedData = DIVInfoID + "@" + PlusMinusCellID;
HTBObj.value = ExpandedData;
}
else if ( ExpandedData.length < 1 && IsExpanded == false )
{
//No Previous Expanded Data.
//Clean up old Expanded Field.
//ExpandedData is empty, so no work is needed.
}
else if ( ExpandedData.length > 0 && ExpandedData.indexOf(DIVInfoID) == -1 && IsExpanded == true )
{
//Expanded data has data from before.
//No existing record exists for this field.
//We can add new Expanded field.
//We can use a comma as a delimeter.
ExpandedData = ExpandedData + "," + DIVInfoID + "@" + PlusMinusCellID;
ExpandedData = ExpandedData.replace( ",," , ",");
HTBObj.value = ExpandedData;
}
else if ( ExpandedData.indexOf(DIVInfoID) > -1 && IsExpanded == true )
{
//Expanded data has data from before.
//Existing record exists for this field.
//We do not need to perform any updates.
}
else if ( ExpandedData.indexOf(DIVInfoID) > -1 && IsExpanded == false )
{
//Expanded data has data from before.
//Existing record exists for this field
//We remove it as it is not expanded any longer.
ExpandedData = ExpandedData.replace(DIVInfoID + "@" + PlusMinusCellID ,"");
//Make sure we don't have a double delimeter
ExpandedData = ExpandedData.replace( ",," , ",");
HTBObj.value = ExpandedData;
}
else if ( ExpandedData.indexOf(DIVInfoID) == -1 && IsExpanded == false )
{
//Expanded data has data from before.
//Existing record does not exists for this field
//No work is needed.
}
//Recheck we don't have any garbage data.
//Added because deleting items, causes a comma
//to stay sometimes.
if ( HTBObj != null && HTBObj.value == "," )
{
HTBObj.value = "";
}
//===============================================================
}
}
catch(e)
{
alert( "Error in SetExpandedDIVInfo Method: " + e);
}
}
//This method will hide the panel.
function HidePanel(Panel)
{
try
{
var ChosenPanel = document.all(Panel);
if ( ChosenPanel != null )
{
document.all(Panel).style.display = "none";
}
}
catch(e)
{
alert( "Error in HidePanel Method: " + e);
}
}
//This method will show the panel.
function ShowPanel(Panel)
{
try
{
var ChosenPanel = document.all(Panel);
if ( ChosenPanel != null )
{
document.all(Panel).style.display = "block";
}
}
catch(e)
{
alert( "Error in ShowPanel Method: " + e);
}
}
//This method will hide and show the panel.
function HideShowPanel(Panel)
{
try
{
var ChosenPanel = document.all(Panel);
if ( ChosenPanel != null )
{
var currentdisplay = document.all(Panel).style.display;
if ( currentdisplay != "block")
{
document.all(Panel).style.display = "block";
}
else
{
document.all(Panel).style.display = "none";
}
}
}
catch(e)
{
alert( "Error in HideShowPanel Method: " + e);
}
}
//This method will get the object using partial id match.
function getItem(IDSearchCriteria, DataGridID)
{
try
{
if ( IDSearchCriteria == null || IDSearchCriteria.length <= 0 )
{
return null;
}
else if ( DataGridID == null || DataGridID.length <= 0 )
{
return null;
}
if( document.getElementsByTagName )
{
var table = document.getElementById(DataGridID);
if ( table != null )
{
var rows = table.getElementsByTagName("tr");
for ( var i = 0; i < rows.length; i++ )
{
var Identity = rows[i].id;
if ( Identity != null && Identity.length > 0 )
{
var cellid = IDSearchCriteria.substring( IDSearchCriteria.lastIndexOf("_") + 1 );
if ( Identity.match(cellid) )
{
//alert("Found: " + Identity);
return Identity;
}
}
}
}
return null;
}
else
{
return null;
}
}
catch(e)
{
alert( "Error in getItem Method: " + e);
return null;
}
}
Leave some feedback.
The gridview has much more flexible methods than the datagrid. Though some methods are a bit different, it is just a simple html table once rendered. Keeping this in mind, we can achieve any type of visual structure we desire.
Was this article helpful? Don't forget to rate it. Ratings helps community members identify top & useful articles.