Showing posts with label GridView. Show all posts
Showing posts with label GridView. Show all posts

Tuesday, February 10, 2009

ASP.NET GridView With Simple Collapse and Expand Functionality

I recently needed an expandable and collapsible GridView in a project I was working on, but couldn't find any simple solutions that met my criteria. Fortunately, it was fairly easy to write up. I have included the basic source code below, so it should be fairly easy to implement into other projects. The only thing you will need to do is find a collapse and expand icon and then update the image names accordingly in the code behind. (For SharePoint, you can use "_layouts\images\TPMin2.gif" for collapse and "_layouts\images\TPMax2.gif" for expand.)

Note: I have not included code for paging. This will require a little more work.

Requirements

  • Use a standard ASP.NET GridView control.
  • Ability to collapse or expand the whole GridView or selective parts of it.
  • Use client side Javascript to ensure better performance and no browser refresh.
  • Only show expand and collapse buttons and grouping value ("Company" in my case) if the next row has the same grouping value.
  • Provide the ability to have the GridView collapsed initially. This is handled in the OnRowDataBound event handler.

Smoke Tests

  • Collapse / Expand link works as expected and the text is updated accordingly. - PASS
  • Collapse and expand button icons are updated properly when clicked and when using the Collapse / Expand link. - PASS
  • Rows without expand and collapse behave properly, especially the first and last rows. - PASS

Screenshots

All Expanded

image

All Collapsed

image

Some Expanded and Some Collapsed

image

ASPX Code

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title></title>

    <script type="text/javascript">
        function ExpandCollapse(index, imageName) {
            var continueLoop = new Boolean(true);
            var imageMax = "TPMax2.gif";
            var imageMin = "TPMin2.gif";
            var rows = document.getElementById('<%= GridView1.ClientID %>').getElementsByTagName('tbody')[0].getElementsByTagName('tr');
            var image = document.getElementsByName(imageName);
            var imageSrc = image[0].src;
            if (imageSrc.match(imageMax) != null) {
                image[0].src = imageMin;
                index++;
                while (continueLoop) {
                    if (rows[index].cells[1].innerHTML == "") {
                        rows[index].style.display = "block";
                        index++;
                        if (index >= rows.length) {
                            continueLoop = false;
                        }
                    }
                    else {
                        continueLoop = false;
                    }
                }
            }
            else {
                image[0].src = imageMax;
                index++
                while (continueLoop) {
                    if (rows[index].cells[1].innerHTML == "") {
                        rows[index].style.display = "none";
                        index++;
                        if (index >= rows.length) {
                            continueLoop = false;
                        }
                    }
                    else {
                        continueLoop = false;
                    }
                }
            }
        }

        function ExpandCollapseAll(linkId) {
            var link = document.getElementById(linkId);
            var imageMax = "TPMax2.gif";
            var imageMin = "TPMin2.gif";
            var rows = document.getElementById('<%= GridView1.ClientID %>').getElementsByTagName('tbody')[0].getElementsByTagName('tr');
            if (link.innerHTML == 'Expand All') {
                link.innerHTML = 'Collapse All';
                for (var i = 1; i < rows.length; i++) {
                    if (rows[i].cells[1].innerHTML != "") {
                        if (rows[i].cells[0].childNodes[0] != undefined) {
                            var img = rows[i].cells[0].childNodes[0];
                            img.src = imageMin;
                        }
                    }
                    else {
                        rows[i].style.display = "block";
                    }
                }
            }
            else {
                link.innerHTML = 'Expand All';
                for (var i = 1; i < rows.length; i++) {
                    if (rows[i].cells[1].innerHTML != "") {
                        if (rows[i].cells[0].childNodes[0] != undefined) {
                            var img = rows[i].cells[0].childNodes[0];
                            img.src = imageMax;
                        }
                    }
                    else {
                        rows[i].style.display = "none";
                    }
                }
            }
        }
    </script>

</head>
<body>
    <form id="form1" runat="server">
    <div>
        <div onclick="javascript:ExpandCollapseAll(this.id);" id="CollapseExpandAllLink"
            style="color: Blue; text-decoration: underline; cursor: pointer" runat="server">
            Collapse All</div>
        <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="false" OnRowDataBound="GridView1_OnRowDataBound"
            OnDataBound="GridView1_DataBound">
            <Columns>
                <asp:ImageField DataImageUrlField="ImageUrl">
                </asp:ImageField>
                <asp:BoundField DataField="Company" HeaderText="Company" />
                <asp:BoundField DataField="FirstName" HeaderText="First Name" />
                <asp:BoundField DataField="LastName" HeaderText="Last Name" />
            </Columns>
        </asp:GridView>
    </div>
    </form>
</body>
</html>

ASPX Code Behind

using System;
using System.Collections.Generic;
using System.Data;
using System.Web.UI.WebControls;

public partial class _Default : System.Web.UI.Page
{
    public string PreviousGroupingValue { get; set; }
    public string ImageMin = "TPMin2.gif";
    public string ImageMax = "TPMax2.gif";
    protected void Page_Load(object sender, EventArgs e)
    {       
        DataTable dt = new DataTable();
        dt.Columns.Add("ImageUrl");
        dt.Columns.Add("Company");
        dt.Columns.Add("FirstName");
        dt.Columns.Add("LastName");
        DataRow dr = dt.NewRow();
        foreach (Employee employee in GetEmployees())
        {
            dr = dt.NewRow();
            dr["ImageUrl"] = ImageMin;
            dr["Company"] = employee.Company;
            dr["FirstName"] = employee.FirstName;
            dr["LastName"] = employee.LastName;
            dt.Rows.Add(dr);
        }
        GridView1.DataSource = dt;
        GridView1.DataBind();
    }
    protected void GridView1_OnRowDataBound(object sender, GridViewRowEventArgs e)
    {
        string imageName = string.Empty;
        bool collapsedInitially = true;
        bool populateImageCtrl = false;
        if (collapsedInitially)
        {
            CollapseExpandAllLink.InnerText = "Expand All";
        }
        else
        {
            CollapseExpandAllLink.InnerText = "Collapse All";
        }
        if (e.Row.RowType == DataControlRowType.DataRow)
        {
            imageName = "ImageExpand" + e.Row.RowIndex.ToString();
            if (e.Row.RowIndex != 0)
            {
                if (GridView1.Rows[e.Row.RowIndex - 1].RowType == DataControlRowType.DataRow)
                {
                    if (e.Row.Cells[1].Text == PreviousGroupingValue)
                    {
                        PreviousGroupingValue = e.Row.Cells[1].Text;
                        e.Row.Cells[1].Text = "";
                        ((Image)e.Row.Cells[0].Controls[0]).Visible = false;
                        if (collapsedInitially)
                        {
                            e.Row.Style.Add("display", "none");
                        }
                    }
                    else
                    {
                        ((Image)GridView1.Rows[e.Row.RowIndex - 1].Cells[0].Controls[0]).Visible = false;
                        populateImageCtrl = true;
                    }
                }
            }
            else
            {
                populateImageCtrl = true;
            }
            if (populateImageCtrl)
            {
                ((Image)e.Row.Cells[0].Controls[0]).Attributes.Add("name", imageName);
                ((Image)e.Row.Cells[0].Controls[0]).Attributes.Add("onclick", "javascript:ExpandCollapse(" + (Convert.ToInt32(e.Row.RowIndex + 1)).ToString() + ", '" + imageName + "');");
                ((Image)e.Row.Cells[0].Controls[0]).ImageUrl = ImageMax;
                PreviousGroupingValue = e.Row.Cells[1].Text;
            }
        }
    }

    protected void GridView1_DataBound(object sender, EventArgs e)
    {
        if (GridView1.Rows.Count > 1)
        {
            if (GridView1.Rows[GridView1.Rows.Count - 1].Cells[1].Text != GridView1.Rows[GridView1.Rows.Count - 2].Cells[1].Text)
            {
                ((Image)GridView1.Rows[GridView1.Rows.Count - 1].Cells[0].Controls[0]).Visible = false;
            }
        }
    }
    public List<Employee> GetEmployees()
    {
        List<Employee> employees = new List<Employee>();
        employees.Add(new Employee { Company = "C Company", FirstName = "Helen", LastName = "Hamaguchi" });
        employees.Add(new Employee { Company = "A Company", FirstName = "Ira", LastName = "Ivanovich" });
        employees.Add(new Employee { Company = "D Company", FirstName = "Greg", LastName = "Good" });
        employees.Add(new Employee { Company = "F Company", FirstName = "Adam", LastName = "Apple" });       
        employees.Add(new Employee { Company = "F Company", FirstName = "Carl", LastName = "Castle" });
        employees.Add(new Employee { Company = "D Company", FirstName = "Delia", LastName = "Dunken" });      
        employees.Add(new Employee { Company = "D Company", FirstName = "Francis", LastName = "Frantz" });      
        employees.Add(new Employee { Company = "E Company", FirstName = "Katsuhiro", LastName = "Kawasaki" });
        employees.Add(new Employee { Company = "D Company", FirstName = "Evan", LastName = "Erlich" });
        employees.Add(new Employee { Company = "E Company", FirstName = "Leslie", LastName = "Lang" });
        employees.Add(new Employee { Company = "F Company", FirstName = "Beverly", LastName = "Beard" });
        employees.Add(new Employee { Company = "E Company", FirstName = "Marvin", LastName = "Marx" });
        employees.Add(new Employee { Company = "Z Company", FirstName = "Nelson", LastName = "Nellis" });
        employees.Add(new Employee { Company = "E Company", FirstName = "Jasmine", LastName = "Jobs" });
        employees.Sort(delegate(Employee e1, Employee e2) { return e1.Company.CompareTo(e2.Company); });
        return employees;
    }
}

public class Employee
{
    public string Company { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Wednesday, February 27, 2008

GridView + DetailsView + LinqDataSource + ModalPopupExtender - Hello World Example

I recently found a limitation when using a GridView, DetailsView, LinqDataSource, and ModalPopupExtender that prevented any records other than the first to be displayed in the DetailsView shown in the ModalPopupExtender. Although I am told that this can be alleviated by using an UpdatePanel, I was working in an extranet environment using Juniper VPN which doesn't like UpdatePanels or at least rewrites the HTML in such a way that prevents an UpdatePanel from working properly. In order to get this to work, all you need to do is hook up the RowCommand and RowDataBound events of the GridView so that a CommandArgument is created and then subsequently used in a Linq query when the item is clicked. You will need SQLExpress with the Northwind sample database and a Linq to SQL Classes (dbml) file that point to the Products table to get this to work.

For amusement, you can read about my struggles in the ASP.NET Forum here: http://forums.asp.net/p/1224034/2195642.aspx Also, a special thanks from the tiredblogger with his post which got me on the right track. http://tiredblogger.wordpress.com/2007/09/16/using-a-modal-popup-to-modify-linq-gridviews/

Without further ado, the code:

ASPX

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Test.aspx.cs" Inherits="LinqTester.Test" EnableEventValidation="false" %>

<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="cc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
<title>Untitled Page</title>
<style type="text/css">
.modalBackground
{
background-color: Black;
filter: alpha(opacity=80);
opacity: 0.8;
}
</style>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<div style="text-align: center;">
<table>
<tr>
<td style="text-align: left;">
<asp:GridView ID="GridView1" runat="server" AllowPaging="True" DataKeyNames="ProductID"
AllowSorting="True" AutoGenerateColumns="False" DataSourceID="LinqDataSource1"
OnRowDataBound="GridView1_RowDataBound"
onrowcommand="GridView1_RowCommand">
<Columns>
<asp:BoundField DataField="ProductName" HeaderText="ProductName" ReadOnly="True"
SortExpression="ProductName" />
<asp:BoundField DataField="QuantityPerUnit" HeaderText="QuantityPerUnit" ReadOnly="True"
SortExpression="QuantityPerUnit" />
<asp:BoundField DataField="ProductID" HeaderText="ProductID" ReadOnly="True" SortExpression="ProductID" />
<asp:TemplateField>
<ItemTemplate>
<asp:LinkButton ID="Details" runat="server" CommandName="ShowDetails" Text="Details"></asp:LinkButton>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
</td>
</tr>
</table>
</div>
<asp:Panel ID="UpdateRecordPanel" runat="server" CssClass="" Style="display:none">
<asp:DetailsView ID="DetailsView1" runat="server" AutoGenerateRows="False" DataSourceID="LinqDataSource2"
Height="50px" Width="125px" BackColor="White">
<Fields>
<asp:BoundField DataField="ProductName" HeaderText="ProductName" ReadOnly="True"
SortExpression="ProductName" />
<asp:BoundField DataField="QuantityPerUnit" HeaderText="QuantityPerUnit" ReadOnly="True"
SortExpression="QuantityPerUnit" />
<asp:BoundField DataField="UnitPrice" HeaderText="UnitPrice" ReadOnly="True" SortExpression="UnitPrice" />
<asp:BoundField DataField="UnitsInStock" HeaderText="UnitsInStock" ReadOnly="True"
SortExpression="UnitsInStock" />
<asp:BoundField DataField="UnitsOnOrder" HeaderText="UnitsOnOrder" ReadOnly="True"
SortExpression="UnitsOnOrder" />
<asp:BoundField DataField="ReorderLevel" HeaderText="ReorderLevel" ReadOnly="True"
SortExpression="ReorderLevel" />
<asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued" ReadOnly="True"
SortExpression="Discontinued" />
</Fields>
</asp:DetailsView>
<asp:Button ID="CancelPopupButton" runat="server" Text="Cancel" />
</asp:Panel>

<cc1:ModalPopupExtender ID="ModalPopupExtender1" runat="server"
BackgroundCssClass="modalBackground"
CancelControlID="CancelPopupButton"
PopupControlID="UpdateRecordPanel"
TargetControlID="HiddenButton" >
</cc1:ModalPopupExtender>

<asp:Button ID="HiddenButton" runat="server" Style="display:none" />

<asp:LinqDataSource ID="LinqDataSource1" runat="server" ContextTypeName="LinqTester.Data.NorthwindDataContext"
Select="new (ProductName, QuantityPerUnit, ProductID)" TableName="Products">
</asp:LinqDataSource>
<asp:LinqDataSource ID="LinqDataSource2" runat="server" ContextTypeName="LinqTester.Data.NorthwindDataContext"
Select="new (ProductName, QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued)"
TableName="Products" Where="ProductID == @ProductID">
<WhereParameters>
<asp:ControlParameter ControlID="GridView1" Name="ProductID" PropertyName="SelectedValue"
DefaultValue="1" Type="Int32" />
</WhereParameters>
</asp:LinqDataSource>
</form>
</body>
</html>


ASPX.CS



using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using AjaxControlToolkit;

namespace LinqTester
{
public partial class Test : System.Web.UI.Page
{
protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType != DataControlRowType.Header && e.Row.RowType != DataControlRowType.Footer && e.Row.RowType != DataControlRowType.Pager)
{
LinkButton lb = e.Row.FindControl("Details") as LinkButton;
if (lb != null)
{
lb.CommandArgument = ((TableCell)e.Row.Controls[2]).Text;
}
}
}

protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
if (e.CommandName == "ShowDetails")
{
ProductDetails productDetails = GetProductDetails(Convert.ToInt32(e.CommandArgument));
DetailsView1.Rows[0].Cells[1].Text = productDetails.ProductName;
DetailsView1.Rows[1].Cells[1].Text = productDetails.Quantity;
DetailsView1.Rows[2].Cells[1].Text = productDetails.UnitPrice.ToString();
DetailsView1.Rows[3].Cells[1].Text = productDetails.UnitsInStock.ToString();
DetailsView1.Rows[4].Cells[1].Text = productDetails.UnitsOnOrder.ToString();
DetailsView1.Rows[5].Cells[1].Text = productDetails.ReorderLevel.ToString();
DetailsView1.Rows[6].Cells[1].Text = productDetails.Discontinued.ToString();
ModalPopupExtender1.Show();
}
}

private ProductDetails GetProductDetails(int productId)
{
LinqTester.Data.NorthwindDataContext northwind = new LinqTester.Data.NorthwindDataContext();
List productDetails = (from p in northwind.Products
where p.ProductID == productId
select new ProductDetails
{
ProductName = p.ProductName,
Quantity = p.QuantityPerUnit,
UnitPrice = p.UnitPrice.HasValue == true ? 0 : (int)p.UnitPrice,
UnitsInStock = p.UnitsInStock.HasValue == true ? 0 : (int)p.UnitsInStock,
UnitsOnOrder = p.UnitsOnOrder.HasValue == true ? 0 : (int)p.UnitsOnOrder,
ReorderLevel = p.ReorderLevel.HasValue == true ? 0 : (int)p.ReorderLevel,
Discontinued = p.Discontinued
}).ToList();
return productDetails[0];
}
}

public class ProductDetails
{
public string ProductName { get; set; }
public string Quantity { get; set; }
public decimal UnitPrice { get; set; }
public int UnitsInStock { get; set; }
public int UnitsOnOrder { get; set; }
public int ReorderLevel { get; set; }
public bool Discontinued { get; set; }
}

}