Introduction
This question has been asked many times in various technical forums (such as in http://forums.asp.net) and there definitely are already many solutions available. Most of the examples from the web use DataSource controls (for example SqlDataSource
, ObjectDataSource
, and so on) to implement a cascading DropDownList in a GridView and I can't seem to find a formal example that shows how to implement it without using DataSource controls.
Using the code
Note
Before you proceed, make sure you already understand the basics of using the GridView control and how to bind it with data since I will not include the details of doing edit, update or delete scenarios for this exercise.
Scenario
As a recap, a Cascading DropDownList
enables a common scenario in which the contents of one list depends on the selection of another list. In this demo we will show a list of Product
orders in a GridView
and when the user edits the row, we will present the user with a DropDownList
containing the list of available makes of products. Once they have made their selection, we will populate the second DropDownList
in the next column with the available Models for the specific make they've chosen. And once they have chosen the model from the second DropDownList
we will then update the Price field for that corresponding model.
Let's Get Started!
This article will have a look at how to possibly do this. To get started let's setup our HTML
markup (.aspx). For the simplicity of this demo, I just set it up like this.
<asp:GridView ID="gvProducts" runat="server" AutoGenerateColumns="false"
DataKeyNames="ProductOrderID"
onrowcancelingedit="gvProducts_RowCancelingEdit"
onrowediting="gvProducts_RowEditing"
onrowupdating="gvProducts_RowUpdating"
onrowdatabound="gvProducts_RowDataBound">
<Columns>
<asp:BoundField DataField="ProductOrderID" ReadOnly="true" />
<asp:TemplateField HeaderText="Make">
<ItemTemplate>
<asp:Label ID="lblMake" runat="server" Text='<%# Bind("Make") %>' />
</ItemTemplate>
<EditItemTemplate>
<asp:DropDownList ID="ddlProductMake" runat="server"
OnSelectedIndexChanged="ddlProductMake_SelectedIndexChanged"
AutoPostBack="true">
</asp:DropDownList>
</EditItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Model">
<ItemTemplate>
<asp:Label ID="lblModel" runat="server" Text='<%# Bind("Model") %>' />
</ItemTemplate>
<EditItemTemplate>
<asp:DropDownList ID="ddlProductModel" runat="server"
OnSelectedIndexChanged="ddlProductModel_SelectedIndexChanged"
AutoPostBack="true">
</asp:DropDownList>
</EditItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="Price" HeaderText="Price" ReadOnly="True"
DataFormatString="{0:c}" />
<asp:BoundField DataField="Quantity" HeaderText="Quantity" />
<asp:CommandField ShowEditButton="True" />
</Columns>
</asp:GridView>
The markup above is composed of 6 columns of various types of control fields. The first column is a BoundField
that holds the ProductOrderID
field. You may notice that we set this field as the DataKeyName
in the GridView so we can easily reference the ID
of the row later on. The 2nd and 3rd columns are TemplateFields
that contain a Label
control within an <ItemTemplate>
and a DropDownList
control within an <EditItemTemplate>
. This means that on a read-only state the Label will be displayed, and on an edit-state the DropDownList
will be displayed to allow users to modify the selection of their choice. Note that you need to set AutoPostBack
to TRUE for the DropDownList
to trigger the SelectedIndexChanged
event. The 4th and 5th columns are BoundFields
that hold the Price
and Quantity
fields. You may notice that the Price
field has an attribute DataFormatString="{0:c}"
set to it. This will transform the value to a currency format when displayed in the browser. Finally, the last column is a CommandField
with ShowEditButton
enabled. This will generate the Edit
link when the GridView
is bound to data.
The DataSource
Just for the simplicity of this exercise, I didn't use a database and instead I've just created some classes that will hold some properties on it. Here are the class definitions.
public class ProductMake
{
public int ProductMakeID { get; set; }
public string Make { get; set; }
}
public class ProductModel
{
public int ProductModelID { get; set; }
public int ProductMakeID { get; set; }
public string Model { get; set; }
public decimal Price { get; set; }
}
public class ProductOrder
{
public int ProductOrderID { get; set; }
public int ProductMakeID { get; set; }
public int ProductModelID { get; set; }
public string Make { get; set; }
public string Model { get; set; }
public short Quantity { get; set; }
public decimal Price { get; set; }
}
Note that this is just an example to make this exercise work. You can always replace this with your DataTable, Entity objects, Custom classes and so on that holds your actual source of data from database.
The ProductMake
class holds two main properties. This is where we store the list of makes of the product. The ProductModel
class is where we store all the models for each make. The ProductOrder
class is where we store the list of orders the user has chosen. Now let's go ahead and provide this class with some sample data. The following is the code block.
private List<ProductOrder> GetUserProductOrder()
{
List<ProductOrder> orders = new List<ProductOrder>();
ProductOrder po = new ProductOrder();
po.ProductOrderID = 1;
po.ProductMakeID = 1;
po.ProductModelID = 1;
po.Make = "Apple";
po.Model = "iPhone 4";
po.Quantity = 2;
po.Price = 499;
orders.Add(po);
po = new ProductOrder();
po.ProductOrderID = 2;
po.ProductMakeID = 2;
po.ProductModelID = 4;
po.Make = "Samsung";
po.Model = "Galaxy S2";
po.Quantity = 1;
po.Price = 449;
orders.Add(po);
po = new ProductOrder();
po.ProductOrderID = 3;
po.ProductMakeID = 3;
po.ProductModelID = 7;
po.Make = "Nokia";
po.Model = "Lumia";
po.Quantity = 1;
po.Price = 549;
orders.Add(po);
return orders;
}
private List<ProductMake> GetProductMakes()
{
List<ProductMake> products = new List<ProductMake>();
ProductMake p = new ProductMake();
p.ProductMakeID = 1;
p.Make = "Apple";
products.Add(p);
p = new ProductMake();
p.ProductMakeID = 2;
p.Make = "Samsung";
products.Add(p);
p = new ProductMake();
p.ProductMakeID = 3;
p.Make = "Nokia";
products.Add(p);
return products;
}
private List<ProductModel> GetProductModels()
{
List<ProductModel> productModels = new List<ProductModel>();
ProductModel pm = new ProductModel();
pm.ProductMakeID = 1;
pm.ProductModelID = 1;
pm.Model = "iPhone 4";
pm.Price = 499;
productModels.Add(pm);
pm = new ProductModel();
pm.ProductMakeID = 1;
pm.ProductModelID = 2;
pm.Model = "iPhone 4s";
pm.Price = 599;
productModels.Add(pm);
pm = new ProductModel();
pm.ProductMakeID = 1;
pm.ProductModelID = 3;
pm.Model = "iPhone 5";
pm.Price = 699;
productModels.Add(pm);
pm = new ProductModel();
pm.ProductMakeID = 2;
pm.ProductModelID = 4;
pm.Model = "Galaxy S2";
pm.Price = 449;
productModels.Add(pm);
pm = new ProductModel();
pm.ProductMakeID = 2;
pm.ProductModelID = 5;
pm.Model = "Galaxy S3";
pm.Price = 549;
productModels.Add(pm);
pm = new ProductModel();
pm.ProductMakeID = 2;
pm.ProductModelID = 6;
pm.Model = "Galaxy Note2";
pm.Price = 619;
productModels.Add(pm);
pm = new ProductModel();
pm.ProductMakeID = 3;
pm.ProductModelID = 7;
pm.Model = "Nokia Lumia";
pm.Price = 659;
productModels.Add(pm);
return productModels;
}
private List<ProductModel> GetProductModelByMake(int productMakeID)
{
var models = (from p in GetProductModels()
where p.ProductMakeID == productMakeID
select p);
return models.ToList();
}
The GetUserProductOrder()
fetches the list of orders. We will use this as our DataSource
in the GridView
later. The GetProductMakes()
method gets all the available makes that, in this case, we just added 3 main items to. The GetProductModel()
method gets all the available models for each make. The GetProductModelByMake()
method gets the specific model item and its details based on the ProductMakeID
. This method uses the LINQ syntax to query the DataSource
based on the parameter that was passed to it.
The Implementation
Now it looks as if we already have some sample source of data to work with. Now let's go ahead and do the highlight of this exercise (which is the implementation of the cascading dropdownlist). Here are the code blocks below.
private void BindGrid()
{
gvProducts.DataSource = GetUserProductOrder();
gvProducts.DataBind();
}
protected void gvProducts_RowEditing(object sender, GridViewEditEventArgs e)
{
gvProducts.EditIndex = e.NewEditIndex;
BindGrid();
}
protected void gvProducts_RowCancelingEdit(object sender, GridViewCancelEditEventArgs e)
{
gvProducts.EditIndex = -1;
BindGrid();
}
protected void gvProducts_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
if ((e.Row.RowState & DataControlRowState.Edit) > 0)
{
DropDownList ddlMake = (DropDownList)e.Row.FindControl("ddlProductMake");
ddlMake.DataSource = GetProductMakes();
ddlMake.DataValueField = "ProductMakeID";
ddlMake.DataTextField = "Make";
ddlMake.DataBind();
ddlMake.SelectedValue = gvProducts.DataKeys[e.Row.RowIndex].Value.ToString();
DropDownList ddlModel = (DropDownList)e.Row.FindControl("ddlProductModel");
ddlModel.DataSource = GetProductModelByMake(Convert.ToInt32(gvProducts.DataKeys[e.Row.RowIndex].Value));
ddlModel.DataValueField = "ProductModelID";
ddlModel.DataTextField = "Model";
ddlModel.DataBind();
ddlModel.SelectedValue = GetProductModelByMake(Convert.ToInt32(gvProducts.DataKeys[e.Row.RowIndex].Value))
.FirstOrDefault().ProductModelID.ToString();
}
}
}
protected void ddlProductMake_SelectedIndexChanged(object sender, EventArgs e)
{
DropDownList ddlMake = (DropDownList)sender;
GridViewRow row = (GridViewRow)ddlMake.NamingContainer;
if (row != null)
{
if ((row.RowState & DataControlRowState.Edit) > 0)
{
DropDownList ddlModel = (DropDownList)row.FindControl("ddlProductModel");
ddlModel.DataSource = GetProductModelByMake(Convert.ToInt32(ddlMake.SelectedValue));
ddlModel.DataValueField = "ProductModelID";
ddlModel.DataTextField = "Model";
ddlModel.DataBind();
}
}
}
protected void ddlProductModel_SelectedIndexChanged(object sender, EventArgs e)
{
DropDownList ddlModel = (DropDownList)sender;
GridViewRow row = (GridViewRow)ddlModel.NamingContainer;
if (row != null)
{
if ((row.RowState & DataControlRowState.Edit) > 0)
{
row.Cells[3].Text = string.Format("{0:C}", GetProductModels()
.Where(o => o.ProductModelID == Convert.ToInt32(ddlModel.SelectedValue))
.FirstOrDefault().Price);
}
}
}
The gvProducts_RowDataBound
event is where we bind the DropDownList with the data from our DataSource
. First we check the RowType
to ensure that we are only manipulating the rows of type DataRow
. Please note that a GridView is composed of several row types such as Header
, DataRow
, EmptyDataRow
, Footer
, Pager
and Separator
. The next line in code block above is the critical part of the code and that is to determine the Edit state.
Accessing the controls from within <EditItemTemplate>
is a bit tricky, especially if you are not really familiar with how the stuff works within a GridView. Equating the RowState
to DataControlState.Edit
isn't really accurate and you might get an exception when doing so. The RowState
property is a bitwise combination. Therefore, the RowState
could indicate that you are in the Edit state and the Alternate state. Hence, you cannot do a simple equality check when you are in Edit mode. Instead you must do something like this.
if ((e.Row.RowState & DataControlRowState.Edit) > 0)
{
}
We use the bitwise “&” operator to determine if the GridView
is in Edit
mode and check the result if its greater than zero. For details about the Bitwise operator see: Bitwise Operators in C#.
Once we've manage to determine the edit-state then we can now begin accessing the controls using the FindControl() method and bind it with the corresponding DataSources. If you notice, I've set the SelectedValue
for the ddlMake and ddlModel DropDownLists
so that by the time the user clicks edit, the DropDownList
will be pre-selected with the previous item the user has chosen.
The ddlProductMake_SelectedIndexChanged
event is where we actually do the cascading feature by populating the second DropDownList
based from the value selected from the first DropDownList
. But before that we need to cast the sender of the object triggering the event to determine which DropDownList
is triggered within the GridView
row. We then cast the NamingContainer of the sender to a type of GridViewRow
to determine the actual row the user is editing.
The ddlProductModel_SelectedIndexChanged
event is where we update the Price
value based on the selected Model
from the second DropDownList
. It basically uses LINQ
syntax to query the DataSource
and to get the Price
based on the selected value of the ddlModel.
Binding the GridView
Finally let's call the BindGrid()
method to populate the GridView
. Here's the code block below.
protected void Page_Load(object sender, EventArgs e){
if (!IsPostBack)
BindGrid();
}
Output
Running the code will produce the following output below:
Summary
In this exercise, we've learned how to implement a horizontal-wise cascading DropDownlist
in GridView
on edit mode.