ASP.NET: Binding the ObjectDataSource to the hosting page or user control

In my recent RDN presentation on CSS Layout with ASP.NET & Visual Studio 2008 I mentioned a technique I use to get some extra value from the ObjectDataSource control. From speaking with other developers I’ve found that many people try to use the control only to be dissatisfied with the way it works, and in particular the need to provide it with a separate business logic or gateway class to look for its CRUD methods on. This is most apparent when you are trying to separate the data-access, business and presentation logic through way of a MVC or MVP pattern like “Passive View“. In this scenario most people go back to the method of setting the DataSource property of their data bound control directly and manually calling the DataBind() method. This is a shame as it prevents you from realising the productivity gains and ease of maintenance of the no-code support for paging and sorting when using a data source control (plus the ability to fully style the GridView as described in my previous article).

Rather than abandoning the ObjectDataSource altogether, we can make it work for us, by sub-classing it and changing it’s behaviour slightly so that it looks for its methods on the hosting page or user control instead, i.e. its parent in the control hierarchy.

(If you have never looked at the data-source controls and two-way data-binding support in ASP.NET 2.0 before, it might be worth first checking out Scott Mitchell’s excellent tutorials on the subject.)

Start out by adding a new class to your project called ParentDataSource.cs. I like to put all custom and user controls in a Controls sub-folder and thus namespace.

add a new class to your project

Make this new class inherit from the ObjectDataSource class and a ToolboxData attribute to define its tag name (you’ll need to add some using statements to import the appropriate namespaces too):

namespace RDN.CSSReadiDepth.Controls
{
    [ToolboxData("<{0}:ParentDataSource runat=server></{0}:ParentDataSource>")]
    public class ParentDataSource : ObjectDataSource
    {

Now add a couple of constructors. These do a few things. First they enable paging and sorting by default (you don’t need to do this but I use paging and sorting more often than not). Secondly they wire-up some events of the control to local handler methods:

public ParentDataSource()
    : this(String.Empty, String.Empty)
{ }

public ParentDataSource(string typeName, string selectMethod)
    : base(typeName, selectMethod)
{
    this.EnablePaging = true;
    this.SortParameterName = "sortExpression";

    this.Init += new EventHandler(ParentDataSource_Init);
    this.ObjectCreating += new ObjectDataSourceObjectEventHandler(ParentDataSource_ObjectCreating);
    this.ObjectDisposing += new ObjectDataSourceDisposingEventHandler(ParentDataSource_ObjectDisposing);
}

Now we need to add our event handler methods. They’re going to do the following:

  • Init handler: walk the control tree to find the page or user control the data source is being hosted on and then set the TypeName property to the type of the hosting page or user control;
  • ObjectCreating handler: set the instance of the object we want our data source control to bind to, which will be the page or user control it is hosted on; and
  • ObjectDisposing handler: cancel the default behaviour of the ObjectDataSource which is to dispose of the object it is bound to.
void ParentDataSource_Init(object sender, EventArgs e)
{
#if DEBUG
    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
    sw.Start();
#endif
    FindParentHost(this);
#if DEBUG
    sw.Stop();
    System.Diagnostics.Debug.WriteLine(String.Format("Finding parent host for data source took {0}", sw.Elapsed));
#endif
}

void ParentDataSource_ObjectCreating(object sender, ObjectDataSourceEventArgs e)
{
    e.ObjectInstance = _parentHost;
}

void ParentDataSource_ObjectDisposing(object sender, ObjectDataSourceDisposingEventArgs e)
{
    e.Cancel = true;
}

Note I’ve also included some timing code in the Init handler so you can measure the impact of this method (it’s negligible).

Finally we need to add some private members to support the handler methods:

Object _parentHost;

/// <summary>
/// Walks the control tree to find the hosting parent page or user control
/// </summary>
/// <param name="ctl">The control to start the tree walk at</param>
private void FindParentHost(Control ctl)
{
    if (ctl.Parent == null)
    {
        // User control was not found, use page base type instead
        this.TypeName = this.Page.GetType().BaseType.FullName;
        _parentHost = this.Page;
        return;
    }

    // Find the user control base type
    UserControl parentUC = ctl.Parent as UserControl;
    MasterPage parentMP = ctl.Parent as MasterPage;
    if (parentUC != null && parentMP == null)
    {
        Type parentBaseType = ctl.Parent.GetType().BaseType;
        this.TypeName = parentBaseType.FullName;
        _parentHost = ctl.Parent;
        return;
    }
    else
    {
        FindParentHost(ctl.Parent);
    }
}

The method FindParentHost, walks the control tree from the data source control until it finds the user control it is hosted on. If it can’t find a hosting user control it assumes the control is hosted on a page. Once it finds the host it sets the TypeName property accordingly (note that the MasterPage class actually inherits from UserControl so we explicitly exclude that case).

Now that we have our customised data source control it’s time to use it in anger! Add a declaration to the web.config to import the controls from the DLL containing the ParentDataSource control:

<pages styleSheetTheme="Theme1">
  <controls>
    <add tagPrefix="cc" namespace="RDN.CSSReadiDepth.Controls" assembly="RDN.CSSReadiDepth" />
  </controls>
</pages>

Now you can place the control in your page or user control mark-up file (.aspx or .ascx) and set the DataSourceID of your data-binding target control:

<asp:GridView ID="gvExample" runat="server" DataSourceID="pdsExample" DataKeyNames="Id"
    AllowPaging="true" AllowSorting="true" PageSize="10" AutoGenerateColumns="false"
    CssClass="customers-grid">
    <Columns>
        <asp:BoundField HeaderText="First Name" DataField="FirstName" SortExpression="FirstName"
            HeaderStyle-CssClass="first-name" ItemStyle-CssClass="first-name"
            AccessibleHeaderText="The customer's first name" />
        <asp:BoundField HeaderText="Last Name" DataField="LastName" SortExpression="LastName"
            HeaderStyle-CssClass="last-name" ItemStyle-CssClass="last-name"
            AccessibleHeaderText="The customer's last name" />
        <asp:BoundField HeaderText="Age" DataField="Age" SortExpression="Age"
            HeaderStyle-CssClass="age" ItemStyle-CssClass="age"
            AccessibleHeaderText="The age of the customer" />
        <asp:BoundField HeaderText="Member for" DataField="YearsAsMember" SortExpression="YearsAsMember"
            HeaderStyle-CssClass="years-as-member" ItemStyle-CssClass="years-as-member"
            AccessibleHeaderText="How many years the customer has been a member for" />
    </Columns>
</asp:GridView><cc:ParentDataSource ID="pdsExample" runat="server"
    SelectMethod="GetData" SelectCountMethod="GetDataRowCount" />

In this example I need to implement two methods on my page’s code-behind file: GetData and GetDataRowCount. The first gets the current page of data to display, the second gets the total number of records (this is needed so the data source controls can calculate paging information):

#region << Data Binding Methods >>
public IEnumerable<Customer> GetData(string sortExpression, int maximumRows, int startRowIndex)
{
    if (this.Customers == null || this.Customers.Count == 0)
        return this.Customers;

    var sortFunc = this.Customers[0].GetPropertySelector<Customer, object>(sortExpression.Replace(" DESC", ""));

    if (sortExpression.ToUpper().IndexOf(" DESC") >= 0)
    {
        return this.Customers
                // Sort descending
                .OrderByDescending(sortFunc)
                // Grab a single page of data
                .Skip((startRowIndex / maximumRows) * maximumRows)
                .Take(maximumRows);
    }
    else
    {
        return this.Customers
                // Sort ascending
                .OrderBy(sortFunc)
                // Grab a single page of data
                .Skip((startRowIndex / maximumRows) * maximumRows)
                .Take(maximumRows);
    }
}

public int GetDataRowCount()
{
    return (this.Customers != null ? this.Customers.Count : 0);
}

#endregion

In this example the data is stored in a property on the page class itself and I’m using LINQ to simulate sorting and paging. The property is just wrapping calls into the application’s session state. The data itself was loaded into session state when the application started up. In a real world example these calls might look up the data from a web-service or database, return data from cache, or in the MVP scenario first raise an event that the presenter class will handle and then return the value of a property on the page (who’s contents was set by the event handler in the presenter class). Also, because these methods are instance methods on the actual page object you are free to manipulate other controls or properties of the page class.

You can use this method equally well with any of the ASP.NET controls that support the data source controls, such as GridView, DetailsView, FormView and Repeater. It also works with two-way data-binding.

Download the sample solution used in this article here


One Comment on “ASP.NET: Binding the ObjectDataSource to the hosting page or user control”

  1. […] with ASP.NET Web Site projects I got a request today from somebody who was trying to use my ParentDataSource (now called PageDataSource) control in an ASP.NET Web Site project and was running into problems. […]


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s