Develop Custom Data-bound Controls in ASP.NET 2.0: Part II

Automate Delete, Update, Select, Insert, and Sorting Operations to Allow Page Developers to Use Your Custom Controls without Writinga Single Line of Code!

asp:Feature

LANGUAGES: C# | VB.NET

ASP.NET VERSIONS: 2.0 (Beta 2)

 

Develop Custom Data-bound Controls in ASP.NET 2.0: Part II

Automate Delete, Update, Select, Insert, and Sorting Operations to Allow Page Developers to Use Your Custom Controls without Writing a Single Line of Code!

 

By Dr. Shahram Khosravi

 

This two-part article provides the implementation of a custom composite data-bound control named MasterDetailsForm that will use a step by step approach to show how to develop custom data-bound controls in ASP.NET 2.0 (Beta 2) with minimal efforts. The two articles show how control developers can automate all tasks of their custom controls, such as Delete, Update, Insert, and Sort, as well as display updates to allow page developers to use their custom controls. Please review Part I before continuing.

 

MasterDetailsForm

In Part I we implemented the important methods and properties of the BaseDataBoundControl, DataBoundControl, and CompositeDataBoundControl base classes to help developers get a better understanding of the ASP.NET 2.0 implementation of these methods and properties. Because this implementation of the methods and properties of these base classes is fully functional, we have the option of using either this article s or ASP.NET s implementation of these methods and properties. This section of the article will show how we can use or extend the ASP.NET 2.0 (or this article s) implementation of these methods and properties to write our own custom data-bound controls. This section will implement a custom data-bound control named MasterDetailsForm that extends the functionality of the CompositeDataBoundControl base class.

 

Control developers also have the option of extending the functionality of the ASP.NET classes that derive from the CompositeDataBoundControl class; e.g., the GridView and DetailsView classes.

 

The MasterDetailsForm control renders two tables. The top table presents all the relevant database records. The MasterDetailsForm exposes a property named MasterFields that allows page developers to specify which database fields should be displayed in the top table. Each row of the top table comes with a link button that allows users to select the row. When a row is selected, the details of the corresponding database record are automatically displayed in the bottom table. The bottom table also allows users to insert a new record and edit, update, and delete the selected record. Therefore, the top and bottom tables create a master/details form (hence the name MasterDetailsForm). The top and bottom tables will be referred to as master and details tables, respectively.

 

The MasterDetailsForm data-bound control derives from the CompositeDataBoundControl base class where it implements the CreateChildControls method (see Figure 1).

 

protected override int CreateChildControls(

 IEnumerable dataSource, bool useDataSource)

{

 this.useDataSource = useDataSource;

 if (dataSource != null)

 {

     CreateMasterDetailsTables();

     renderHeader = true;

     currentRowIndex = 0;

     IEnumerator iter = dataSource.GetEnumerator();

     while (iter.MoveNext())

     {

         this.currentDataItem = iter.Current;

         masterCurrentDataRow = new TableRow();

         if (currentRowIndex % 2 == 1)

             masterCurrentDataRow.SkinID =

              MasterAlternatingRowSkinID;

         else

             masterCurrentDataRow.SkinID =

              MasterRowSkinID;

         masterTable.Rows.Add(masterCurrentDataRow);

         SetParameters();

         for (int i = 0; i < columnCount; i++)

         {

             currentFieldIndex = i;

             AddDetailsRow();

             AddMasterCell();

         }

          AddMasterSelectButton();

         renderHeader = false;

         currentRowIndex++;

     }

     AddDetailsCommandBar();

     AddErrorMessageLabel();

    if (useDataSource)

         ViewState["ColumnCount"] = columnCount;

 }

 return currentRowIndex;

}

Figure 1: The CreateChildControls method enumerates the data and creates the control hierarchy.

 

The method enumerates the records in the dataSource argument to create the control hierarchy from the data source if useDataSource is true (i.e., the records are real database records) and from the saved viewstate otherwise (i.e., the records are dummy records for enumeration purposes). The for loop enumerates all the relevant fields or columns of each enumerated record and calls the AddDetailsRow and AddMasterCell methods to render each enumerated field in the master and details tables, respectively. The master table displays each enumerated field as a cell; the details table displays it as a row.

 

Recall the details table renders the details of the record that the user selects from the master table. Therefore, the details table displays a single record at a time, contrary to the master table that displays multiple records. The details table can be in one of the following three states: ReadOnly, Edit, or Insert. The current implementation of the AddDetailsRow method allows users to update all fields except the primary key field. However, it can easily be extended to allow page developers to decide which fields should be editable.

 

The CreateChildControls method uses the TypeDescriptor class to access the fields of an enumerated record in generic fashion. The GetProperties method of the TypeDescriptor class uses reflection to access all the available information about each field. The method uses an instance of the PropertyDescriptor class to represent each field and returns an instance of the PropertyDescriptorCollection class that contains all the PropertyDescriptor instances:

 

PropertyDescriptorCollection tempDataItemFields =

 TypeDescriptor.GetProperties(currentDataItem);

 

The current implementation of the AddDetailsRow and AddMasterCell methods display fields whose values can be converted to string values. The following code excludes the fields whose values cannot be converted to string values:

 

ArrayList list = new ArrayList();

 foreach (PropertyDescriptor pd in tempDataItemFields)

 {

     if (pd.Converter.CanConvertTo(Type.GetType(

         "System.String")))

         list.Add(pd);

 }

 currentDataItemFields =

  new PropertyDescriptor[list.Count];

 list.CopyTo(currentDataItemFields);

 

The PropertyDescriptor class exposes a property named Converter that returns an instance of type TypeConverter. The instance exposes a method named CanConvertTo that can be used to determine whether a field can be converted to a string value.

 

The AddDetailsRow method renders an instance of the TextBox control for each enumerated field when the details table is in the Insert or Edit state. The method then stores the name of the enumerated field in the Attributes collection of the TextBox instance for future references:

 

    TextBox tbx = new TextBox();

PropertyDescriptor pd =

 currentDataItemFields[currentFieldIndex];

 tbx.Attributes["FieldName"] = pd.Name;

 

The above code uses the value of the Name property of the respective PropertyDescriptor object to access the name of the field in generic fashion. The AddDetailsRow method also displays the actual value of the enumerated field in the TextBox instance when the details table is in the Edit state:

 

tbx.Text = pd.Converter.ConvertTo(

 pd.GetValue(currentDataItem),

 Type.GetType("System.String")).ToString();

 

The above code uses the GetValue method of the respective PropertyDescriptor object to access the value of the enumerated field, then uses the ConvertTo method of the Converter property of the PropertyDescriptor object to convert the value to a string. Recall the current implementation of the MasterDetailsForm control only displays fields whose values can be converted to string values. Also notice the method sets the value of the DataKeyFieldValue property to the primary key field value of the selected record. This property will be used in data operations such as Delete, Update, Insert, and Sort (discussed later).

 

After displaying all the fields of the enumerated record, the CreateChildControls method calls the AddMasterSelectButton method to display a select link button for the record and registers the CommandCallback method as the callback for the Command event of the button. The AddMasterSelectButton method sets the CommandArgument of the link button to the index of the enumerated record:

 

LinkButton lbtn = new LinkButton();

Lbtn.CommandName = "Select";

   lbtn.CommandArgument = currentRowIndex.ToString();

 

The CommandCallback method calls the SelectCallback method:

 

protected void SelectCallback(int index)

{

 SelectedIndex = index;

 MasterDetailsFormMode = MasterDetailsFormMode.ReadOnly;

 RequiresDataBinding = true;

}

 

The SelectCallback method switches the details table back to the ReadOnly mode and sets the RequiresDataBinding property to true.

 

There are numerous places where a custom control must extract fresh data from the data store and refresh its display. The SelectCallback method is one of these cases. When the user selects a record from the master table, the MasterDetailsForm must extract the details of the selected record from the data store and display them in the details table. The BaseDataBoundControl, DataBoundControl, and CompositeDataBoundControl base classes have made life easy for custom-control developers. Thanks to these base classes, every time a custom control needs to refresh its display with fresh data, it can simply set the RequiresDataBinding property to true. A single line of code, like setting the property value, does the trick. The previous sections of this article described how this magic works. As a reminder, let s briefly review what happens when a custom control sets the RequiresDataBinding property to true. The setter method of the RequiresDataBinding calls the EnsureDataBound method of the BaseDataBoundControl class. The EnsureDataBound method checks the value of the RequiresDataBinding property. Because the custom control has set the value to true, the EnsureDataBound method automatically calls the following overload of the DataBind method of the BaseDataBoundControl class:

 

public override void DataBind;

 

The DataBind method automatically calls the PerformSelect method of the DataBoundControl class. The PerformSelect method calls the GetData method of the DataBoundControl class to access the default tabular DataSourceView object. The PerformSelect method registers the PerformDataBinding method of the DataBoundControl class as the callback for the Select operation. The PerformSelect method then calls the Select method of the DataSourceView object. The Select method extracts the fresh data from the underlying data store. The Select method then automatically calls the PerformDataBinding method of the DataBoundControl and passes the data as its only argument. The PerformDataBinding method calls the following overload of the DataBind method of the CompositeDataBoundControl class:

 

protected override void DataBind(bool raiseOnDataBinding)

 

The DataBind method calls the following overload of the CreateChildControls method of the custom control (e.g., MasterDetailsForm) class:

 

protected override int CreateChildControls(

 IEnumerable dataSource, bool useDataSource)

 

The CreateChildControls method enumerates the data and creates the control hierarchy. The CreateChildControls method of the MasterDetailsForm calls the AddDetailsCommandBar to add the standard New, Edit, Update, Cancel, Delete, and Insert command buttons to the details table. The type of command buttons depends on the mode of the details table:

  • The three New, Edit, and Delete standard command buttons are used when the details table is in the ReadOnly mode. When the user clicks the New button, the details table switches to Insert mode, allowing the user to add a new record. When the user clicks the Edit button, the details table switches to Edit mode, allowing the user to edit the existing record.
  • The two Update and Cancel command buttons are used when the details table is in Edit mode.
  • The two Insert and Cancel command buttons are used when the details table is in Insert mode.

 

When the user clicks any of the above command buttons, the button raises the Command event and calls the CommandCallback method. As the CommandCallback method shows, switching the details table from one mode to another takes two steps:

1)     Set the MasterDetailsFormMode property to the new mode. The possible values are ReadOnly, Edit, and Insert.

2)     Set the RequiresDataBinding property to true.

 

As previously discussed, setting the RequiresDataBinding property automatically refreshes the display of the details table.

 

The following sections will show how we can use the ASP.NET 2.0 (or this article s) implementation of the methods and properties of the BaseDataBoundControl, DataBoundControl, and CompositeDataBoundControl to automate the Delete, Update, Insert, and Sort data operations where page developers will be able to use the MasterDetailsForm control without writing a single line of code!

 

The Delete, Update, and Insert operations change the underlying data store. Therefore, the MasterDetailsForm control must extract fresh data from the underlying data store and refresh its display after these operations. However, thanks to the DataBoundControl class, that is no longer necessary. As discussed in the previous sections, the DataBoundControl class takes the following two actions:

1)     Exposes a new method named OnDataSourceViewChanged that sets the RequiresDataBinding property to true.

2)     Its GetData method registers the OnDataSourceViewChanged as the callback for the DataSourceViewChanged event of the default view object.

 

Therefore, the view object automatically calls the OnDataSourceViewChanged method right after deleting, updating, or inserting a record. Because the OnDataSourceViewChanged method sets the RequiresDataBinding property to true, everything else automatically falls through.

 

However, it is important that the details table switches back to its ReadOnly mode after deleting, updating, or inserting a record. That is why the MasterDetailsForm class overrides the OnDataSourceViewChanged method:

 

protected override void OnDataSourceViewChanged(

 object sender, EventArgs e)

{

 MasterDetailsFormMode = MasterDetailsFormMode.ReadOnly;

   base.OnDataSourceViewChanged(sender, e);

}

 

Delete Data Operation

The CommandCallback calls the Delete method to handle the Delete event:

 

private void Delete()

{

 DataSourceView dv = GetData();

 if (dv.CanDelete)

 {

   Hashtable keys = new Hashtable();

   keys.Add(DataKeyField, DataKeyFieldValue);

   Hashtable oldValues = new Hashtable();

   oldValues.Add(DataKeyField, DataKeyFieldValue);

   dv.Delete(keys, oldValues,

    new DataSourceViewOperationCallback(DeleteCallback));

 }

}

 

As the Delete method shows, control developers must take the following steps to automate the delete data operation:

  • Call the GetData method of the DataBoundControl class to access the default tabular view object.
  • Check the value of the CanDelete property of the view object to make sure it supports the Delete data operation.
  • Create an instance of the Hashtable class and populate it with the primary key field values. The MasterDetailsForm control exposes two properties, named DataKeyField and DataKeyFieldValue, whose values are the name of the primary key field and its value, respectively.
  • Create another instance of the Hashtable class and populate it with the old field values.
  • Call the Delete method of the view object.
  • Pass the above two instances of the Hashtable class as the first two arguments of the Delete method.
  • Because the Delete operation is asynchronous, register a callback.
  • The first two arguments of the Delete method take objects of type IDictionary. Therefore, we can use objects of any class that implements IDictionary, such as Hashtable, SortedList, etc.

 

Update Data Operation

The CommandCallback method calls the Update method to handle the Update event, as shown in Figure 2.

 

private void Update()

{

 DataSourceView dv = GetData();

 if (dv.CanUpdate)

 {

   Hashtable keys = new Hashtable();

   keys.Add(DataKeyField, DataKeyFieldValue);

   Hashtable values = new Hashtable();

   Hashtable oldValues = new Hashtable();

   foreach (TableRow row in detailsTable.Rows)

   {

       foreach (TableCell cell in row.Cells)

       {

           if (cell.Controls.Count > 0)

           {

               TextBox tbx = cell.Controls[0] as TextBox;

               if (tbx != null)

               {

                 values.Add(tbx.Attributes["FieldName"],

                            tbx.Text);

                 oldValues.Add(tbx.Attributes["FieldName"],

                               tbx.Text);

               }

           }

       }

   }

   dv.Update(keys, values, oldValues,

    new DataSourceViewOperationCallback(

    UpdateInsertCallback));

 }

}

Figure 2: The Update method handles the Update event.

 

As the Update method shows, control developers must take the following steps to automate the Update data operation:

1)     Call the GetData method of the DataBoundControl class to access the default tabular view object.

2)     Check the value of the CanUpdate property of the view object to make sure it supports the Update data operation.

3)     Create an instance of the Hashtable class and populate it with the primary key field values.

4)     Create two more instances of the Hashtable class.

5)     Enumerate all the cells that contain textboxes and extract the names of the respective fields and their values. Recall that the AddDetailsRow method added the name of the respective field to the Attributes property of the corresponding TextBox instance.

6)     Populate the above two Hashtable instances with the above field values.

7)     Call the Delete method of the view object.

8)     Pass the above three Hashtable instances as the first three arguments of the Update method.

9)     Because the Update operation is asynchronous, register a callback.

 

In step 5, custom controls that use server controls other than textboxes must extract the names and values of the fields from whatever server control is being used. The Insert operation is very similar to the Update operation. The only difference is that the Insert method only takes a single instance of the Hashtable class because inserting a record does not involve primary key field values and old values.

 

Sort Data Operation

The MasterDetailsForm control exposes a property named AllowSorting that allows page developers to turn sorting on/off. When the value of this property is true, the AddMasterHeaderCell method renders the header text as a link button, allowing users to sort the records. The method sets the CommandArgument of the link button to the name of the field and registers the CommandCallback as the callback for the Command event of the link button:

 

   LinkButton hbtn = new LinkButton();

hbtn.CommandName = "Sort";

hbtn.Command += new CommandEventHandler(CommandCallback);

hcell.Controls.Add(hbtn);

if (useDataSource)

{

 hbtn.CommandArgument =

  currentDataItemFields[currentFieldIndex].Name;

 hbtn.Text =

  currentDataItemFields[currentFieldIndex].Name;

}

 

When the user clicks the header text of a column, the CommandCallback method calls the Sort method (see Figure 3).

 

private void Sort(string sortExpression)

{

 if (SortExpression == sortExpression)

 {

     if (SortDirection == "Asc")

         SortDirection = "Desc";

     else

         SortDirection = "Asc";

 }

 else

 {

     SortExpression = sortExpression;

     SortDirection = "Asc";

 }

 RequiresDataBinding = true;

}

Figure 3: The Sort method handles the Sort event.

 

The Sort method sets the values of the SortExpression and SortDirection properties and sets the RequiresDataBinding property to true.

 

Recall that the GetData method of the DataBoundControl class calls the Select method of the underlying default view object. The first argument of the Select method calls the CreateDataSourceSelectArguments method to access the DataSourceSelectArguments object. The MasterDetailsForm control overrides the CreateDataSourceSelectArguments method:

 

protected override DataSourceSelectArguments

 CreateDataSourceSelectArguments()

{

 DataSourceView dv = GetData();

 DataSourceSelectArguments args =

  new DataSourceSelectArguments();

 if (dv.CanSort)

     args.SortExpression =

      SortExpression + " " + SortDirection;

 return args;

}

 

The method first calls the GetData method of the DataBoundControl class to access the default tabular view object. It then creates an instance of the DataSourceSelectArguments class and checks the value of the CanSort property of the view object to make sure it supports sorting. The method then sets the SortExpression property of the DataSourceSelectArguments object to the combination of the values of the SortExpression and SortDirection properties of the MasterDetailsForm control.

 

Control State

Many ASP.NET 1.x server controls provide features that rely on viewstate to function properly across postbacks. For instance, the pagination feature of a DataGrid relies on viewstate to recover the current page index when the user clicks the Next button and posts the page back to the server. Without recovering the current page index, the control would not know what the next page is. The problem with viewstate is that it is public, which means page developers may decide to turn it off. Obviously, those features of server controls that rely on viewstate will not function properly when the viewstate is turned off.

 

ASP.NET 2.0 provides server controls with their own private storage named control state. Technically, both control and view states are stored in the same hidden field named __VIEWSTATE. However, the control state is private to the control and page developers cannot turn it off. Therefore, custom controls must use their control states as storage for those properties that are essential to their main features.

 

The MasterDetailsForm control uses its control state to store those properties that are essential to its functionality and features. The control overrides the SaveControlState method to save its essential properties. The following shows a portion of the method:

 

protected override object SaveControlState()

 {

     object[] state = new object[20];

     state[0] = base.SaveControlState();

     state[1] = _sortExpression;

     state[2] = _masterFieldIndeces;

     . . .

     return state;

 }

 

The MasterDetailsForm control also overrides the LoadControlState method to recover its essential properties. The following shows a portion of the method:

 

protected override void LoadControlState(object savedState)

 {

   if (savedState != null)

   {

       object[] state = savedState as object[];

       if (state != null && state.Length == 20)

       {

           base.LoadControlState(state[0]);

           if (state[1] != null)

               _sortExpression = (string)state[1];

           if (state[2] != null)

               _masterFieldIndeces = (ArrayList)state[2];

           . . .

       }

   }

   else

       base.LoadControlState(savedState);

 }

 

The MasterDetailsForm control overrides the OnInit method to notify the containing page that it needs to use its control state:

 

protected override void OnInit(EventArgs e)

{

 Page.RegisterRequiresControlState(this);

 base.OnInit(e);

}

 

Appearance Properties

Composite data-bound controls normally expose style properties that apply to their child controls. For instance, the GridView control exposes a property named SelectedItemStyle that applies to the selected row of the control. To get a better understanding of the problems associated with style properties, let s consider the following similar client-side situation.

 

There are two ways to apply appearance parameters to HTML tags. One way is to do it inline, where the appearance attributes are directly applied to each tag. This mixes content; i.e., HTML tags with presentation (appearance) properties. Cascading Style Sheets were introduced to separate content from presentation. Notice Cascading Style Sheets are client-side technology.

 

The style properties of composite data-bound controls also mix content with presentation, but on the server side. ASP.NET 2.0 comes with what is known as themes to separate content from presentation on the server side. Therefore, the ASP.NET 2.0 themes are the preferred way to apply appearance properties to the child controls of composite data-bound controls.

 

The MasterDetailsForm control exposes SkinID and EnableTheming properties that apply to its child controls. For instance, the MasterDetailsForm control sets the SkinID property of the master table to the value of the MasterTableSkinID property.

 

The MasterDetailsForm control exposes seven SkinID properties:

  • MasterTableSkinID: The SkinID property of the master table.
  • MasterHeaderRowSkinID: The SkinID property of the header row of the master table.
  • MasterRowSkinID: The SkinID property of a row in the master table.
  • MasterAlternatingRowSkinID: The SkinID property of an alternating row in the master table.
  • DetailsTableSkinID: The SkinID property of the details table.
  • DetailsRowSkinID: The SkinID property of a row in the details table.
  • DetailsAlternatingRowSkinID: The SkinID property of an alternating row in the details table.

 

Codeless Master/Details Form

Figure 4 shows that page developers can declaratively use the MasterDetailsForm custom data-bound control without writing a single line of code!

 

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

<%@ Register TagPrefix="custom" Namespace=

 "CustomControls" %>

<!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">

   <custom:MasterDetailsForm DataKeyField=

    "BookID" AllowSorting="true"

     ID="MyControl" Runat="Server" DataSourceID="MySource"

     MasterAlternatingRowSkinID="AlternatingRow"

     MasterTableSkinID="Table"

     DetailsAlternatingRowSkinID="AlternatingRow"

     DetailsTableSkinID="Table"

     MasterHeaderRowSkinID="HeaderRow"

     MasterFields="BookID,Title" />

   <asp:ObjectDataSource ID="MySource" Runat=

    "Server" SortParameterName="sortParameterName"

    TypeName="Books" SelectMethod="SelectBooks"

    DeleteMethod="DeleteBook"

    UpdateMethod="UpdateBook" InsertMethod="InsertBook">

       <UpdateParameters>

           <asp:Parameter Name="BookID" Type="Int32" />

           <asp:Parameter Name="Price" Type="Decimal" />

       </UpdateParameters>

       <DeleteParameters>

           <asp:Parameter Name="BookID" Type="Int32" />

       </DeleteParameters>

       <InsertParameters>

           <asp:Parameter Name="Price" Type="Decimal" />

       </InsertParameters>

    </asp:ObjectDataSource>

 </form>

</body>

</html>

Figure 4: Page developers can declaratively use the MasterDetailsForm control without writing a single line of code.

 

The example page in Figure 4 uses the ObjectDataSource control to access the underlying data source. The Books class is a custom class that exposes the SelectBooks, DeleteBook, UpdateBook, and InsertBook methods. Page developers can use any type of data source control that implements IDataSource, such as ObjectDataSource, SqlDataSource, and AccessDataSource.

 

Conclusion

This two-part article demonstrates how control developers can use or extend the methods and properties of the ASP.NET 2.0 data-bound control base classes to write custom controls that automatically handle the data operations, such as Delete, Insert, Update, and Sort. This allows page developers to use the custom controls without writing a single line of code. The article also delved into the ASP.NET 2.0 implementation of the base classes to provide control developers with an inside view of the ASP.NET 2.0 data-bound control model.

 

The sample code accompanying this article is available for download.

 

Dr. Shahram Khosravi is a Senior Software Engineer with Schlumberger Information Solutions (SIS). Shahram specializes in ASP.NET 1.x/2.0, XML Web services, ADO.NET 1.x/2.0, .NET 1.x/2.0 technologies, XML technologies, 3D Computer Graphics, HI/Usability, and Design Patterns. Shahram has extensive expertise in developing ASP.NET 1.x/2.0 custom server controls and components. He has more than 10 years of experience in object-oriented programming. He uses a variety of Microsoft tools and technologies such as SQL Server 2000 and 2005. Shahram writes articles on developing ASP.NET 2.0 custom server controls and components, ADO.NET 2.0, and XML technologies for various industry-leading magazines such as asp.netPRO magazine, Microsoft MSDN Online, and CoDe magazine. 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