DropDownListField

Add Foreign Key Editing Capability to Your GridView and DetailsView Controls without Writing a Single Line of Code

asp:Feature

LANGUAGES: C# | VB.NET

ASP.NET VERSIONS: 2.0

 

DropDownListField

Add Foreign Key Editing Capability to Your GridView and DetailsView Controls without Writing a Single Line of Code

 

By Dr. Shahram Khosravi

 

The foreign/primary key pairs establish relationship among database tables. The value of a foreign key field in a given record is one of the existing values of its corresponding primary key field. ASP.NET page developers must take extra steps to allow users to display and edit the foreign key fields of a row in data-bound controls such as GridView and DetailsView.

 

The state of the row that contains a foreign key field determines what steps must be taken. The following steps are normally taken when the containing row is not in the Edit or Insert state:

  • The current foreign key value is one of the values of its corresponding primary key field. Most database tables automatically generate the primary key value of a record when the record is added to its table. Therefore, the actual foreign key value is an auto-generated integer that does not mean anything to users. However, the table that contains the primary key field normally exposes another field with more meaningful values to users. For instance, consider a database that contains tables named Products and Categories. The Products table has a foreign key field named CategoryID. The Categories table contains the corresponding primary key field, i.e. CategoryID. The Categories table also exposes a field named CategoryName, which is more meaningful to users. Page developers extract the current value of the field that makes more sense to users, e.g. CategoryName.
  • Display the current value of the user-friendly field (e.g. CategoryName) as simple text.

 

The following steps are taken when the containing row is in the Edit or Insert state where users are allowed to edit the current value of the foreign key field:

  • Write code to access the database and extract all legal values of the foreign key field and the field with more meaningful values to users, e.g. CategoryName in the previous example.
  • Instantiate a DropDownList server control and bind it to the data extracted in the first step.
  • Set the DataValueField property of the DropDownList control to the name of the corresponding primary key field.
  • Set the DataTextField property of the DropDownList control to the name of the user-friendly field (e.g. CategoryName).
  • Extract the current value of the foreign key field.
  • Set the SelectedIndex of the DropDownList control to the index of the current value of the user-friendly field (e.g. CategoryName).
  • Allow users to select a null value for the foreign key field because most database tables allow null values for their foreign key fields.

 

Because none of the existing fields (i.e. BoundField, HyperLinkField, ImageField, CheckBoxField, CommandField, ButtonField, and TemplateField fields) of data-bound controls such as GridView and DetailsView provide built-in support for the above nine steps, page developers have no choice but to take care of all the steps themselves. This article will design and implement a new field named DropDownListField that will automatically take care of all the above nine steps. Page developers will be able to declaratively use the DropDownListField instances without writing a single line of code!

 

DropDownListField

Both GridView and DetailsView controls consist of fields. They are called fields because they mostly display the values of their respective database fields. GridView and DetailsView controls render their fields as columns and rows, respectively. All fields are the instances of the descendants of the DataControlField class. The existing descendants are the BoundField, ImageField, HyperLinkField, CheckBoxField, CommandField, ButtonField, and TemplateField classes.

 

Most of these field classes internally use server controls to display the values of their respective database fields. For example, ImageField and CheckBoxField objects internally use Image and CheckBox server controls, respectively, to display their field values. The data type of the field and the state of its containing row determine the type of server control used to display the value of the field. For instance, an ImageField object uses an Image server control to display its field value when its containing row is in normal state, and a TextBox server control when its containing row is in the Edit or Insert state.

 

This article will design and implement a new field named DropDownListField that will use a DropDownList server control to display all the legal values of its field when its containing row is in the Edit or Insert state. The DropDownListField field will display the current value of its field as simple text when its containing row is not in the Edit or Insert state. This article will derive the DropDownListField class from the BoundField class because the BoundField class provides all the necessary base functionality when the containing row is not in the Edit or Insert state, such as:

  • Extracting the current value of the field whose name is the value of the DataField property. The DropDownListField overrides this property and defines a new property named DataTextField to replace it because DataTextField is a more appropriate name than DataField.
  • Displaying the current value as simple text if the current value is not null.
  • Displaying the value of the NullDisplayText property if the current value is null.
  • Displaying the value of the HeaderText property as a simple text if sorting is disabled and as a hyperlink if sorting is enabled.
  • Raising the GridView.Sorting event when AllowSorting is enabled and the header hyperlink is clicked.

 

The main shortcoming of the BoundField class is that it displays the current value of the field in a TextBox control when the containing row is in the Edit or Insert state. The TextBox control is not the appropriate server control for editing foreign key fields because it allows users to enter any value and does not restrict the values to the legal ones. The DropDownListField class overrides the InitializeDataCell, OnDataBindField, and ExtractValuesFromCell methods of the BoundField class to add the supports needed when the containing row is in the Edit or Insert state. Figure 1 shows all the properties and methods of the DropDownListField class. The following sections will present the implementation of these properties and methods.

 

public class DropDownListField : BoundField

{

 public override string DataField {get; set; }

 public virtual string DataTextField {get; set; }

 public virtual string SkinID {get; set;}

 public virtual bool EnableTheming {get; set;}

 public virtual string DataValueField {get; set;}

 public virtual string DataSourceID {get; set;}

 protected override void OnDataBindField(Object sender,

                                         EventArgs e);

 protected override void InitializeDataCell(DataControlFieldCell cell,

                                            DataControlRowState rowState);

 public override void ExtractValuesFromCell(IOrderedDictionary dictionary,

                                            DataControlFieldCell cell,

                                            DataControlRowState rowState,

                                            bool includeReadOnly);

}

Figure 1: The properties and methods of the DropDownListField class.

 

Overriding the InitializeDataCell Method

The BoundField class exposes a method named InitializeDataCell that contains the code that generates the appropriate HTML markup text for the data cell. The InitializeDataCell method takes two arguments. The first argument is the DataControlFieldCell instance being initialized. The second argument is the state of the containing row.

 

Which HTML markup text the BoundField class implementation of the InitializeDataCell method emits depends on the state of its containing row. If the containing row is not in the Edit or Insert state, the method simply registers the OnDataBindField method as the callback for the DataBinding event of the respective DataControlFieldCell instance. When the DataBinding event of the cell is raised, the OnDataBindField Method extracts the current value of the respective field (the name of the field is the value of the DataField property). If the current value is null, the value of the NullDisplayText property is displayed. Otherwise, the current value is displayed as simple text.

 

The BoundField class implementation of the InitializeDataCell method in normal state is exactly what we need. However, the BoundField class implementation of the method when the containing row is in the Edit or Insert state is not acceptable because the method instantiates an instance of the TextBox control. We need an implementation that instantiates an instance of the DropDownList control. This is why the DropDownListField class overrides the InitializeDataCell method. The DropDownListField class calls the base version of the InitializeDataCell method when the containing row is not in the Edit or Insert state because the behavior of the base version is exactly what we need. However, the DropDownListField class provides its own implementation when the containing row is in the Edit or Insert state.

 

The DropDownListField class implementation of the InitializeDataCell method instantiates an instance of the DropDownList control and sets its DataSourceID property to the value of the DataSourceID property of the DropDownListField instance. It is the responsibility of page developers to set the DataSourceID property of the DropDownListField instance to the value of the ID property of the appropriate data source control in the containing page. Page developers must also set the DataTextField and DataValueField properties of the DropDownListField class to the names of the appropriate database fields. This allows the DropDownListField instance to automatically populate its DropDownList control with the valid values of the foreign key field:

 

DropDownList ddl = new DropDownList();

ddl.DataSourceID = DataSourceID;

ddl.DataTextField = DataTextField;

ddl.DataValueField = DataValueField;

 

As discussed in the introduction, one of the requirements for the DropDownListField class is that it must set the selected value of the DropDownList control to the current value of the respective foreign key field. This is done in a callback registered for the DataBound event of the DropDownList control. The DropDownList control inherits the DataBound event from the BaseDataBoundControl class. There is a difference between the DataBound event that the BaseDataBoundControl class exposes and the DataBinding event that the Control class exposes.

 

The DataBinding event is raised before the data is actually bound, and the DataBound event is raised after the data binding process finishes. Because the selected value of the DropDownList control must be set after the control is bound to its data source, it is set within the callback for the DataBound event. The InitializeDataCell method registers the OnDataBindField method as the callback for the DataBound event of the DropDownList control:

 

if (DataTextField.Length != 0 && DataValueField.Length != 0)

   ddl.DataBound += new EventHandler(OnDataBindField);

 

Figure 2 illustrates the code for the InitializeDataCell method.

 

protected override void InitializeDataCell(DataControlFieldCell cell,

                                          DataControlRowState rowState)

{

 if ((rowState & DataControlRowState.Edit) != 0 ||

      (rowState & DataControlRowState.Insert) != 0)

 {

    DropDownList ddl = new DropDownList();

    ddl.SkinID = SkinID;

    ddl.EnableTheming = EnableTheming;

    ddl.DataSourceID = DataSourceID;

    ddl.DataTextField = DataTextField;

    ddl.DataValueField = DataValueField;

    if (DataTextField.Length != 0 && DataValueField.Length != 0)

        ddl.DataBound += new EventHandler(OnDataBindField);

      cell.Controls.Add(ddl);

 }

 else

    base.InitializeDataCell(cell,rowState);

}

Figure 2: The InitializeDataCell method initializes the current data cell object.

 

Handling the DataBound Event

When the DataBinding event of the cell is raised, the OnDataBindField method is called to display the current value in display mode, i.e. as simple text. When the DataBound event of the DropDownList control is raised, the OnDataBindField method is called to display the current value in edit mode, i.e. as the selected item of the DropDownList control.

 

The reason the OnDataBindField method is called when the DataBinding event of the cell is raised is that the BoundField version of the InitializeDataCell method registers the OnDataBindField method as the callback for the DataBinding event of the cell. The reason the OnDataBindField method is called when the DataBound event of the DropDownList control is raised is that the DropDownListField version of the InitializeDataCell method registers the OnDataBindField method as the callback for the DataBound event of the DropDownList control.

 

Before the OnDataBindField method can display the current value in the edit or insert mode, it must extract the value. The OnDataBindField method uses the parent control of the cell to access the value:

 

IDataItemContainer container =

  (IDataItemContainer)cell.Parent;

object dataItem = container.DataItem;

 

The parent of the cell is a row of type GridViewRow in GridView controls and of type DetailsViewRow in DetailsView controls. Both these types implement the IDataItemContainer interface. IDataItemContainer exposes a single property named DataItem of type Object. The dataItem object represents the record of the database to which the row is bound (e.g. the GridViewRow or DetailsViewRow object). After we access the dataItem object we can use the DataBinder class to extract the current value of the field whose name is the value of the DataValueField property

 

object dataValueField = DataBinder.Eval(dataItem,

                                       DataValueField);

 

The value of the DataValueField property is the name of the foreign key field.

 

After the OnDataBindField method extracts the dataValueField value it takes two additional actions. Because most database tables allow null values for foreign key fields, the OnDataBindField method inserts a new item into the DropDownList control and sets its Text property to the value of the NullDisplayText property:

 

if (NullDisplayText.Length > 0)

 ddl.Items.Insert(0, new ListItem(NullDisplayText, "-1"));

 

The next section will delve into the details of the ExtractValuesFromCell method. This method extracts the SelectedValue of the DropDownList control and inserts it into an IDictionary container that is passed in as its first argument. The OnDataBindField method sets the value of the Value property of the newly added item to the string -1 to signal the ExtractValuesFromCell method to insert a null value in the IDictionary container.

 

The OnDataBindField method then sets the SelectedIndex of the DropDownList control to the respective index of dataValueField if dataValueField is not equal to DBNull. Otherwise, it configures the DropDownList control to display the newly added item as its selected item:

 

if (dataValueField.Equals(DBNull.Value))

    ddl.SelectedIndex = 0;

 else

    ddl.SelectedIndex = ddl.Items.IndexOf(

     ddl.Items.FindByValue(dataValueField.ToString()));

 

Figure 3 shows the code for the OnDataBindField method.

 

protected override void OnDataBindField(Object sender,

                                       EventArgs e)

{

 DropDownList ddl2;

 DataControlFieldCell cell;

 ddl2 = sender as DropDownList;

 if (ddl2 == null)

 {

   base.OnDataBindField(sender, e);

   return;

 }

   cell = (DataControlFieldCell)ddl2.Parent;

 IDataItemContainer container =

   (IDataItemContainer)cell.Parent;

 object dataItem = container.DataItem;

 object dataValueField = DataBinder.Eval(dataItem,

                                         DataValueField);

 DropDownList ddl = cell.Controls[0] as DropDownList;

 if (ddl != null)

 {

   if (NullDisplayText.Length > 0)

       ddl.Items.Insert(0, new ListItem(

                        NullDisplayText, "-1"));

    if (dataValueField.Equals(DBNull.Value))

        ddl.SelectedIndex = 0;

    else

        ddl.SelectedIndex = ddl.Items.IndexOf(

         ddl.Items.FindByValue(dataValueField.ToString()));

 }

}

Figure 3: The OnDataBindField method is called when either the cell s DataBinding event or the DropDownList s DataBound event is raised.

 

Extracting Values from Cells

GridView and DetailsView controls allow users to edit database fields. Users click on the Update button after they make the desired changes. GridView and DetailsView controls are equipped with internal handlers to handle the Update event. The handlers call the ExtractRowValues method of the GridView or DetailsView control, which in turn calls the ExtractValuesFromCell methods of its cells. The ExtractRowValues method provides each ExtractValuesFromCell method with a container of type IOrderedDictionary. Each ExtractValuesFromCell method extracts the value of its cell and inserts the value into the container. The internal handler for the Update event then uses these values in its internal data access code to update the underlying database fields.

 

The ExtractValuesFromCell method of the DropDownListField class extracts the selected value of the DropDownList control and inserts it into the IDictionary container passed in as its first input argument. Recall that the OnDataBindField method inserted a new item with the Value property of -1 into the DropDownList control. This item allows users to select a null value for the foreign key field. The ExtractValuesFromCell method checks the SelectedValue property of the DropDownList control before it inserts the item into the IDictionary container. If the value is -1 , the method inserts a null value instead:

 

if (ddl.SelectedValue == "-1")

 

Figure 4 shows the code for the ExtractValuesFromCell method.

 

public override void ExtractValuesFromCell(IOrderedDictionary dictionary,

                                          DataControlFieldCell cell,

                                          DataControlRowState rowState,

                                          bool includeReadOnly)

{

 if (cell.Controls.Count > 0)

 {

    DropDownList ddl = cell.Controls[0] as DropDownList;

    if (ddl == null)

        throw new InvalidOperationException("DropDownListField could

                                            not extract control.");

    string dataValueField = ddl.SelectedValue;

    if (dictionary.Contains(DataValueField))

    {

       if (dataValueField == "-1")

           dictionary[DataValueField] = DBNull.Value;

       else

           dictionary[DataValueField] = int.Parse(dataValueField);

    }

    else

    {

       if (dataValueField == "-1")

           dictionary.Add(DataValueField, DBNull.Value);

        else

           dictionary.Add(DataValueField, int.Parse(dataValueField));

    }

 }

}

Figure 4: ExtractValuesFromCell extracts the selected value of the DropDownList control and inserts the value into the IDictionary container.

 

Appearance Properties

The DataControlField class exposes a property of type Style named ControlStyle. The DataControlField class internally uses the value of the ControlStyle property to set the style properties of the server control that the DataControlField instance renders. In the case of the DropDownListField class, the ControlStyle property is applied to the DropDownList control that the class contains.

 

The ControlStyle property is not the only styling option available. ASP.NET 2.0 comes with a new feature named Themes. A theme is implemented as a subfolder under the Themes folder (the name will change to app_themes in the Beta 2 version). The subfolder must have the same name as the theme. A theme subfolder consists of one or more skin files and their respective image and cascading stylesheet files. Because ASP.NET 2.0 merges all the skin files of a theme into a single skin file page, developers can use as many skin files as necessary to organize the theme folder. Themes are assigned to the containing page, not the individual controls.

 

The @Page directive in ASP.NET 2.0 exposes a new attribute named Theme, which is set to the name of the desired theme. Because all themes are subfolders of the Themes folder, the ASP.NET framework knows where to find the assigned theme. A skin file includes one or more control skins. A control skin defines the appearance properties of a class of server controls. The definition of a control skin is very similar to the declaration of an instance of the control in a page. This does not mean that all properties of a server control can be set in its skin. In general, only the appearance properties can be included and set in a control skin.

 

If the SkinID property of a control skin is not set, the control skin is treated as the default skin. A default skin is automatically applied to the control instances whose SkinID properties are not set. If the SkinID property of a control skin is set, it will be applied only to the control instances whose SkinID property is set to the same value.

 

The DataControlField class exposes two new properties, SkinID and EnableTheming. It is the responsibility of page developers to set the SkinID property of the DropDownListField object to the value of the SkinID property of the desired DropDownList control skin. The EnableTheming property of the DropDownListField object allows page developers to enable/disable theming for the DropDownList control server.

 

The InitializeDataCell method sets the SkinID and EnableTheming properties of the DropDownList control to the values of the SkinID and EnableTheming properties of the DropDownListField object. Themes give page developers full control over the appearance properties of the DropDownList control that the DropDownListField object renders.

 

Declarative Programming without Writing a Single Line of Code

Page developers can use the DropDownListField class declaratively without writing a single line of code! Figure 5 shows a page that declaratively uses an instance of the DropDownListField.

 

<%@ Page Language="C#" Theme="YellowTheme" %>

<%@ Register TagPrefix="custom" Namespace="CustomFields" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"

"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<script runat="server">

</script>

<html xmlns="http://www.w3.org/1999/xhtml" >

<head id="Head1" runat="server">

 <title>Untitled Page</title>

</head>

<body>

 <form id="form1" runat="server">

   <asp:GridView ID="gv2" Runat="Server"

    AutoGenerateColumns="false" AllowSorting="true"

    DataSourceID="GridViewSource" AutoGenerateEditButton="true"

    DataKeyNames="ProductID">

      <Columns>

         <asp:BoundField DataField="ProductName"

          HeaderText="Product Name" SortExpression="ProductName">

         </asp:BoundField>

         <custom:DropDownListField SkinID="SkinID1" EnableTheming="true"

          DataValueField="CategoryID" DataTextField="CategoryName"

          DataSourceID="DropDownListSource" SortExpression="CategoryName"

          HeaderText="Category Name" NullDisplayText="Unknown" />

      </Columns>

    </asp:GridView>

   <asp:ObjectDataSource ID="GridViewSource"

    Runat="Server" SortParameterName="sortExpression"

    TypeName="Product" SelectMethod="Select" UpdateMethod="Update" />

   <asp:SqlDataSource ID="DropDownListSource" Runat="Server"

    ConnectionString="<%$ ConnectionStrings:MyConnectionString %>"

    SelectCommand="Select * From Categories" />

 </form>

</body>

</html>

Figure 5: Page developers use the instances of the DropDownListField class declaratively without writing a single line of code.

 

Notice the page doesn t include a single line of C# or VB.NET code; it s all done declaratively. Theming is enabled for the internal DropDownList instance by setting its SkinID property to the SkinID of the control skin defined in the theme named YellowTheme. The YellowTheme is very simple. It sets the background and foreground colors of the internal DropDownList control to blue and yellow, respectively. However, page developers can apply their own complex themes to the DropDownList control.

 

Any type of data source control that presents tabular views of its underlying data store can be bound to the internal DropDownList control. Page developers do not have to bind both the GridView and the internal DropDownList controls to the same type of data source control. Figure 5 binds the GridView control to an instance of the ObjectDataSource class and the internal DropDownList control to an instance of the SqlDataSource control. The Product class used in the ObjectDataSource control is a custom class that provides basic database operations, such as Select and Update.

 

Conclusion

In this article you learned how to design and implement a new class named DropDownListField that inherits from the BoundField class. The DropDownListField class provides built-in support for the following features when the containing row is not in the Edit or Insert state:

  • Extracts the current value of the fields whose names are the values of the DataValueField (i.e. foreign key field) and DataTextField (i.e. the field with more useful values to users) properties.
  • Displays the current value of the user-friendly field as simple text.
  • Raises sort event if sorting is enabled.

 

The DropDownListField class inherits the above three features from the BoundField class. The DropDownListField class provides its own built-in support for the following features when the containing row is in the Edit or Insert state:

  • Extracts all the legal values of the fields whose names are the values of the DataValueField (i.e. foreign key field) and DataTextField (i.e. the field with more useful values to users) properties.
  • Instantiates an instance of the DropDownList control.
  • Populates the corresponding DropDownList control with the legal values.
  • Configures the DropDownList control to display the current value of the user-friendly field.
  • Extracts the selected value of the DropDownList and inserts the value into the appropriate IDictionary object. The extracted value is used in update data operations.
  • Allows users to select a null value for its respective foreign key field.
  • Supports new ASP.NET 2.0 themes.

 

The sample code accompanying this article is available for download.

 

Dr. Shahram Khosravi is a Senior Software Engineer with Schlumberger Information Solutions (SIS), the leading provider of software solutions for the oil and gas industry. He specializes in ASP.NET, XML Web services, .NET technologies, 3D Computer Graphics, HI/Usability, and Design Patterns. Shahram has extensive expertise in writing ASP.NET custom server controls. He has more than 10 years of experience in object-oriented programming. Reach him at mailto:[email protected].

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish