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

Automate Data-binding Processes and Enable Your Custom Controls to Access any Type of Data Store

asp:Feature

LANGUAGES: C# | VB.NET

ASP.NET VERSIONS: 2.0 (Beta 2)

 

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

Automate Data-binding Processes and Enable Your Custom Controls to Access any Type of Data Store

 

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 without writing a single line of code!

 

The ASP.NET 2.0 data-bound control model consists of five base classes: BaseDataBoundControl, DataBoundControl, HierarchicalDataBoundControl, ListControl, and CompositeDataBoundControl. In Part I we ll implement the important methods and properties of the BaseDataBoundControl, DataBoundControl, and CompositeDataBoundControl base classes to help developers delve into the ASP.NET 2.0 implementation of these methods and properties. Because this article s implementation of these methods and properties is fully functional, we will have the option of using the ASP.NET 2.0 implementation or this article s implementation discussed in Part II.

 

The data-binding process involves three tasks:

1)     Extract the data from the underlying data source.

2)     Enumerate the data and generate the appropriate HTML markup text.

3)     Take the above two actions in response to the events that require fresh data from the data store, such as Load, Delete, Update, and Insert, just to name a few.

 

The main goal of the ASP.NET 2.0 data-bound control model is to automate all three data-binding tasks without a single line of code from page developers. As mentioned, the model consists of five base classes. This article will only discuss the BaseDataBoundControl, DataBoundControl, and CompositeDataBoundControl classes. Each base class does its share to help automate the three data-binding tasks. The following sections will discuss the contributions of each base class.

 

Before we proceed with the discussions, let s examine the following common theme in the ASP.NET 2.0 data-bound control model. When it comes to a given method of a base class, we must separate the following two responsibilities:

1)     Implementing the method.

2)     Automating its invocation.

 

These are two different responsibilities that should be assigned to two different classes.

 

BaseDataBoundControl

This section focuses on the contributions that the BaseDataBoundControl class makes to automate the three data-binding tasks. The Control class exposes a method named DataBind that handles the data-binding process. One of the main goals of the BaseDataBoundControl class is to automate the invocation of this method. The class uses two different mechanisms to achieve its goal. The first mechanism is very similar to the mechanism that the Control class uses to automate the invocation of the CreateChildControls method.

 

The Control class exposes a method named EnsureChildControls. The method first checks the value of the ChildControlsCreated property. If it is false, it first calls the CreateChildControls method and then sets the ChildControlsCreated property to true. The ChildControlsCreated property is used to make sure that the CreateChildControls method is not called more than once for each request.

 

The Control class implementation of the OnPreRender method calls the EnsureChildControls method to make sure that the CreateChildControls method is called (if ChildControlsCreated is false) before the control enters its rendering phase. Because the OnPreRender method is automatically called for every single request, the EnsureChildControls method and, consequently, the CreateChildControls method (if the ChildControlsCreated property is false) are guaranteed to be called automatically.

 

The BaseDataBoundControl class uses a similar approach to automate the invocation of the DataBind method. The class introduces a new Boolean field named _requiresDataBinding, which operates similarly to the ChildControlsCreated property. The field is used to make sure that the DataBind method is not called more than once for each request. The class also introduces a new method named EnsureDataBound, which operates similarly to the EnsureChildControls method. The method first checks the value of the _requiresDataBinding field. If it is true, it first calls the DataBind method and then sets the _requiresDataBinding field to true:

 

protected void EnsureDataBound()

 {

     if (_requiresDataBinding)

         DataBind();

     _requiresDataBinding = false;

 }

 

The BaseDataBoundControl class then overrides the OnPreRender method to do exactly what the Control class did to automate the invocation of the EnsureChildControls method (it calls the EnsureDataBound method):

 

protected override void OnPreRender(EventArgs e)

{

   EnsureDataBound();

   base.OnPreRender(e);

}

 

Because the OnPreRender method is automatically called for every single request, the EnsureDataBound method and, consequently, the DataBind method (if the _requiresDataBinding field is true) are guaranteed to be called automatically. The BaseDataBoundControl class also overrides the OnInit method where it initializes the _requiresDataBinding field to false:

 

protected override void OnInit(EventArgs e)

{

   _requiresDataBinding = false;

   base.OnInit(e);

}

 

As mentioned, the BaseDataBoundControl class uses two mechanisms to automate the invocation of the DataBind method. We have so far discussed the first mechanism. The second mechanism involves the following new property of the BaseDataBoundControl class:

 

private bool _requiresDataBinding;

protected bool RequiresDataBinding

{

   get

   {

       return _requiresDataBinding;

   }

   set

   {

       _requiresDataBinding = value;

       EnsureDataBound();

   }

}

 

The setter of the RequiresDataBinding property calls the EnsureDataBound method. Therefore, the BaseDataBoundControl class guarantees to automatically call the EnsureDataBound method and, consequently, the DataBind method every time the RequiresDataBinding property is set to true. A single line of code, such as setting the RequiresDataBinding property to true, is all it takes to trigger the data-binding process! We ll discuss the significance of this mechanism in the next sections.

 

The BaseDataBoundControl class inherits the DataBind method from the WebControl class, which in turn inherits the method from the Control class. The Control class implementation of the DataBind method does not do much; it simply raises the OnDataBinding event. Therefore, the automation of the invocation of the DataBind method would do nothing to automate the previously-mentioned three data-binding tasks unless the subclasses of the BaseDataBoundControl class override its implementation where they add code to automate the three data-binding tasks. Because the DataBind method is not an abstract class, the BaseDataBoundControl class cannot enforce the requirement that its base classes must implement the method. This is why the BaseDataBoundControl class exposes a new abstract method, as follows:

 

protected abstract void PerformSelect();   

 

The class overrides the DataBind method where it calls the PerformSelect method:

 

public override void DataBind()

{

   base.DataBind();

   PerformSelect();

}

 

This will force the subclasses of the BaseDataBoundControl class to implement the PerformSelect method where they add code to automate the three data-binding tasks.

 

The BaseDataBoundControl also makes contributions to the first task of the data-binding process as follows. In general, there are two ways to extract the data from the underlying data store. One way is the traditional ASP.NET 1.x approach where page developers do all the work. The BaseDataBoundControl exposes a property named DataSource to support this approach:

 

private object myDataSource;

public virtual object DataSource {

   get

    {

       return myDataSource;

   }

   set

   {

       ValidateDataSource(value);

       myDataSource = value;

   }

}

 

The BaseDataBoundControl exposes a method named ValidateDataSource that is used to validate a data source before it is assigned to the DataSource property:

 

protected abstract void ValidateDataSource(object dataSource);

 

The BaseDataBoundControl class automatically calls the ValidateDataSource method every time the DataSource property is set. However, it is not its responsibility to implement the method. The descendants of the BaseDataBoundControl class are responsible for implementing the method where they decide what types of data sources they support. The second way to extract the data from the data store is the new ASP.NET 2.0 approach. The BaseDataBoundControl exposes a new property named DataSourceID to support this approach, as well. Page developers set the value of the DataSourceID to the value of the ID property of the desired data source control.

 

DataBoundControl

The DataBoundControl class derives from the BaseDataBoundControl class. All the other methods and properties of the DataBoundControl class are there to help the class implement the PerformSelect and ValidateDataSource methods. The DataBoundControl class implementation of the ValidateDataSource method is kept very simple to keep the focus on the main theme of this article. However, it can easily be extended to support any data source of type IListSource.

 

The DataBoundControl class exposes a new method named PerformDataBinding:

 

protected abstract void PerformDataBinding(IEnumerable data);

 

The method automates the second data-binding task (it enumerates the data passed in as its argument and generates the appropriate HTML markup text). Notice the DataBoundControl class does not implement the method. However, because the DataBoundControl class calls the method in its implementation of the PerformSelect method (as discussed in an upcoming section), the class guarantees to automatically call the method when the RequiresDataBinding property is set to true.

 

The PerformSelect method has two main responsibilities:

1)     Automatically extract the data from the underlying data store, which automates the first data-binding task.

2)     Automatically call the PerformDataBinding method and pass the data as its argument, which automates the second data-binding task.

 

As mentioned, there are two ways to extract the data from the underlying data store. One way is the traditional ASP.NET 1.x approach where page developers take complete control of the data-binding process. Let s list the steps involved:

1)     Page developers query the data from the underlying data store.

2)     Page developers assign the data to the DataSource property.

3)     The DataSource property automatically calls the ValidateDataSource method of the DataBoundControl.

4)     The ValidateDataSource method automatically validates the data.

5)     Page developers call the DataBind method.

6)     The DataBind method automatically calls the PerformSelect method of the BaseDataBoundControl.

7)     The PerformSelect method automatically calls the PerformDataBinding method and passes the IEnumerable part of the DataSource value as its argument.

8)     The PerformDataBinding method enumerates the data passed in as its argument and generates the required HTML markup text.

 

Therefore, the PerformSelect method does not play a significant role in the traditional ASP.NET 1.x data-binding scenario, other than calling PerformDataBinding. Another way to extract the data from the underlying data store is the new ASP.NET 2.0 data source control model. This model allows the PerformSelect method to automate the first data-binding task without a single line of code from page developers!

 

ASP.NET 2.0 Data Source Control Model

The DataBoundControl class expects tabular data from the underlying data store. This causes a major problem in data-driven Web applications because data comes from different sources, such as Microsoft SQL Server, Oracle, XML documents, flat files, XML Web services, etc., and there is no guarantee that the DataBoundControl class will always receive tabular data.

 

This is where the ASP.NET 2.0 tabular data source controls come into play. Tabular data source controls isolate the DataBoundControl class from the underlying data store and provide it with tabular views of the data store, whether or not the data store itself is tabular. This allows the DataBoundControl class to deal with the views instead of the data store itself.

 

The data source controls that provide tabular views of their underlying data stores are referred to in this series as tabular data source controls. Each type of tabular data source control is specifically designed to work with a specific type of data store. For instance, the SqlDataSource control knows how to query and update data from relational data stores such as Microsoft SQL Server and Oracle databases; the XmlDataSource control knows how to query data from an XML document.

 

However, because all tabular data source controls implement IDataSource, the DataBoundControl class does not have to deal with the particular features of each data source control and can treat all of them as objects of type IDataSource. Therefore, the DataBoundControl class must use only the methods of the IDataSource interface to deal with the associated data source control. It must not use any method or property that is specific to a particular type of data source control, such as SqlDataSource or XmlDataSource. This will allow page developers to bind the descendants of the DataBoundControl control to any type of data source control that implements IDataSource.

 

The DataBoundControl class exposes a method named GetDataSource that returns a reference to the underlying IDataSource object:

 

protected IDataSource GetDataSource()

{

   return (IDataSource)Page.FindControl(DataSourceID);

}

 

Because all data source controls derive from the Control class, the GetDataSource method calls the FindControl method of the containing page to access the data source control.

 

IDataSource exposes a method named GetView that takes the view name as its only argument and returns an instance of the DataSourceView class that represents the view. It is the view object that takes care of all data operations, such as Select, Delete, Update, Insert, Sort, and paging.

 

The DataBoundControl class exposes a method named GetData that returns the default tabular DataSourceView object:

 

protected DataSourceView GetData()

{

 IDataSource ds = GetDataSource();

 DataSourceView dv = ds.GetView(String.Empty);

 dv.DataSourceViewChanged += new EventHandler(

  OnDataSourceViewChanged);

 return dv;

}

 

The GetData method first calls the GetDataSource method to access the data source control, then calls the GetView method of the data source control with an empty string as its argument to access the default tabular DataSourceView object.

 

Every DataSourceView object raises an event named DataSourceViewChanged every time:

1)     one or more of its properties change value;

2)     the underlying data store changes because of the data operations, such as Delete, Update, and Insert.

 

The DataBoundControl class exposes a method named OnDataSourceViewChanged that the GetData method registers as the callback for the DataSourceViewChanged event:

 

protected virtual void OnDataSourceViewChanged(

 Object sender, EventArgs e)

 {

         RequiresDataBinding = true;

 }

 

The DataBoundControl class implementation of the OnDataSourceViewChanged simply sets the RequiresDataBinding property to true. Recall the BaseDataBoundControl class guarantees to call the PerformSelect method every time the property is set to true. Part II will show how the OnDataSourceViewChanged method will help control developers automate the data operations, such as Delete, Update, and Insert.

 

To keep things simple, this article provides a straightforward implementation for the GetData method. Another way is to encapsulate the DataSource value in a DataSourceView object. This article s implementation can easily be extended to use a DataSourceView object instead.

 

Now let s see how the PerformSelect method can use the ASP.NET 2.0 data source model to automate the first data-binding task. Figure 1 shows the implementation of the PerformSelect method.

 

protected override void PerformSelect()

{

 if (IsBoundUsingDataSourceID && DataSource != null)

     throw new Exception("Both DataSourceID and DataSource

                          properties cannot be set at the

                         same time!");

 OnDataBinding(EventArgs.Empty);

 if (IsBoundUsingDataSourceID)

 {

     DataSourceView dv = GetData();

     dv.Select(CreateDataSourceSelectArguments(),

               new DataSourceViewSelectCallback(

               PerformDataBinding));

 }

 else

     PerformDataBinding(GetEnumerableDataSource());

}

Figure 1: The PerformSelect method extracts the data and calls the PerformDataBinding method.

 

The PerformSelect method first checks the value of the IsBoundUsingDataSourceID property. If it s true, the PerformSelect method takes control of data query. The method first calls the GetData method to access the DataSourceView object that represents the default tabular view. It then calls the Select method of the view object to extract the data from the underlying data store. The Select method takes two arguments. The first argument takes an object of type DataSourceSelectArguments. This object is used to request extra operations on the extracted data. These operations include sorting, paging, and retrieving the total number of rows for paging purposes.

 

The DataBoundControl class exposes a method named CreateDataSourceSelectArguments that creates an instance of the DataSourceSelectArguments class:

 

protected virtual DataSourceSelectArguments

 CreateDataSourceSelectArguments()

   {

           return new DataSourceSelectArguments();

    }

 

Part II will show how control developers can override the CreateDataSourceSelectArguments method to request extra operations on the data.

 

The first argument of the Select method calls the CreateDataSourceSelectArguments method to access the DataSourceSelectArguments object.

 

Because the Select method is asynchronous, the PerformSelect method registers the PerformDataBinding abstract method as the callback. The Select method will automatically call the PerformDataBinding method after it extracts the data, then pass the data as its argument. This helps the subclasses of the DataBoundControl automate the second data-binding task.

 

The DataBoundControl class also overrides the OnLoad method to automate the third data-binding task in response to the Page Load event when the page is accessed the first time (see Figure 2).

 

protected override void OnLoad(EventArgs e)

{

  if (!Page.IsPostBack)

   {

       if (IsBoundUsingDataSourceID)

        {

           Control c = Page.FindControl(DataSourceID);

           if (c == null)

               throw new Exception("The data source " +

                                   DataSourceID + "

                                   does not exist!");

           IDataSource ds = c as IDataSource;

           if (ds == null)

               throw new Exception("The data source " +

                                   DataSourceID + " is not

                                   of type IDataSource!");

       }

       RequiresDataBinding = true;

   }

   base.OnLoad(e);

}

Figure 2: The OnLoad method.

 

As the code in Figure 2 shows, setting the RequiresDataBinding property to true is all it takes to automate the third data-binding task in response to the Load event. This is because the BaseDataBoundControl guarantees to automatically call the PerformSelect method every time the RequiresDataBinding property is set to true.

 

In summary, the DataBoundControl class makes the following contributions to help automate the three data-binding tasks:

1)     Automates the first data-binding task; i.e., it automatically extracts the data from the underlying data store.

2)     Automatically calls the PerformDataBinding method every time the RequiresDataBinding property is set to true.

3)     Passes the data as an argument to the PerformDataBinding method. It is the responsibility of the subclasses of the DataBoundControl class to implement the PerformDataBinding method where they enumerate the data and generate the required HTML markup text, which means they automate the second data-binding task.

4)     Automates the third data-binding task in response to the Load event when the page is accessed the first time. However, it does not automate the third data-binding task in response to other events, such as Delete, Insert, Update, Sort, or any other event that requires fresh data from the data store.

 

CompositeDataBound Control

Control developers can write custom controls that directly derive from the DataBoundControl to avoid writing their own data access code to extract the data from the underlying data store. This allows them to simply implement the PerformDataBinding method to enumerate the data and generate the required HTML markup text.

 

However, most custom controls delegate the responsibility of generating the required HTML markup text to other standard or custom controls. These custom controls are known as composite data-bound controls. All composite data-bound controls follow the same implementation pattern. For instance, they all must use a single method to create the control hierarchy from both the data source and saved viewstate. The CompositeDataBoundControl class contains the code that all composite data-bound controls need. Therefore, control developers can create high-quality composite data-bound controls with minimal effort.

 

The CompositeDataBoundControl derives from the DataBoundControl class, where it provides the following implementation for the PerformDataBinding method:

 

private IEnumerable _dataSource;

protected override void PerformDataBinding(IEnumerable data)

{

   _dataSource = data;

   DataBind(false);

}

 

The method sets the local variable _dataSource to the data and calls the DataBind method with the false argument. The CompositeDataBoundControl class also implements the DataBind method:

 

protected override void DataBind(bool raiseOnDataBinding)

{

 if (raiseOnDataBinding)

     base.OnDataBinding(System.EventArgs.Empty);

 Controls.Clear();

 ClearChildViewState();

 TrackViewState();

 ViewState["RowCount"] =

 CreateChildControls(_dataSource, true);

 ChildControlsCreated = true;

}

 

The method first clears the Controls collection and the saved viewstate and calls the TrackViewState method to start the viewstate tracking. It then calls the following overload of the CreateChildControls method:

 

protected abstract int CreateChildControls(

 IEnumerable dataSource, bool useDataSource);

 

Because the DataBoundControl class guarantees to automatically call the PerformDataBinding method every time the RequiresDataBinding property is set to true, the CompositeDataBoundControl guarantees to automatically call the CreateChildControls method every time the RequiresDataBinding property is set to true. However, it does not implement the CreateChildControls method. Its subclasses are responsible for implementing the method.

 

The method takes two arguments. The second argument specifies whether the first argument contains real data or dummy data for enumeration purposes. The subclasses must enumerate the data and create the control hierarchy from the saved viewstate if useDataSource is false and from the data source otherwise.

 

The following overload of the CreateChildControls method creates the control hierarchy from the saved viewstate:

 

protected override void CreateChildControls();

 

The CompositeDataBoundControl overrides the above overload of the CreateChildControls to delegate the responsibility of creating the control hierarchy to the previous overload of the CreateChildControls method:

 

protected override void CreateChildControls()

{

 Controls.Clear();

 if (ViewState["RowCount"] != null)

     ViewState["RowCount"] = CreateChildControls(

      new object[(int)ViewState["RowCount"]], false);

}

 

Notice that the method creates an array of objects and passes it as the first argument of the CreateChildControls method. The subclasses of the CompositeDataBoundControl will enumerate this dummy data source to create the control hierarchy from the saved viewstate.

 

The CreateChildControls method returns the number of enumerated data rows. The DataBind method stores the number in the viewstate. When the containing page is posted back to the server, the CreateChildControls method is called. The method retrieves the number of enumerated records from the saved viewstate and creates an array of dummy records for enumeration purposes. This guarantees that the CreateChildControls method will create the same control hierarchy from both data source and saved viewstate.

 

The CompositeDataBoundControl class also overrides the Controls collection to add a call to the EnsureChildControls method:

 

public override ControlCollection Controls

{

 get

 {

     EnsureChildControls();

     return base.Controls;

 }

}

 

This makes sure that the Controls collection is populated before it is accessed.

 

Control developers have only one responsibility: to implement the CreateChildControls method. The ASP.NET 2.0 data-bound control model takes care of everything else.

 

Part II will demonstrate how to 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.

 

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