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:skhosravi@alltel.net.