posts - 76, comments - 26, trackbacks - 0

Databinding Friendly Encapsulating Web Controls

Custom web controls (both server controls and user controls) in ASP.NET are powerful building blocks.  The idea is obvious - reusable chunks of presentation-oriented software that can be used to assemble a user interface for a web app.  I've seen fair amount of web application architectures and a variety of approaches to custom web controls. 

This might be a yawner for some, but I've seen this done poorly so many times... I'm working with an application right now that's jacked in this area.  This is a subtle, yet very significant aspect of a web application's architecture.  There are several ways to do this, but very few that promote encapsulation of the control, while letting it earn it's paycheck in the architecture.

Lets start with a simple example of wanting to display a list of guitars on a page.

In this case, <kb:Guitar ... /> doesn't do anything other than display the guitar instance in a table.

Markup
Here's the beginning of something nice.  We have a stock repeater housing a custom web control (Guitar.ascx).  Obviously, I'm going to want to bind a list of Guitar entity instances to this repeater and have Guitar.ascx handle the work of presenting the Guitar instance data to the user.

<body>
    <form id="form1" runat="server">
    <div>
        <p>Guitars: </p>
        <asp:Repeater ID="GuitarRepeater" runat="server">
            <ItemTemplate>
                <kb:Guitar ID="Guitar" runat="server"></kb:Guitar>
            </ItemTemplate>
        </asp:Repeater>
    </div>
    </form>
</body>

Code-behind
Pretty standard...

    protected void Page_Load(object sender, EventArgs e)
    {
        List<Guitar> guitars = new List<Guitar>();
        guitars.Add(new Guitar(1, "HD28"));
        guitars.Add(new Guitar(2, "HD41"));
        guitars.Add(new Guitar(3, "HD20"));

        GuitarRepeater.DataSource = guitars;
        GuitarRepeater.DataBind();
    }

What's Missing?
When the page runs, we all know that an instance of <kb:Guitar ... /> is rendered per guitar in the list.  So, at runtime, a guitar instance is handed out to each <kb:Guitar ... /> instance the repeater renders.  How does <kb:Guitar ... /> wire itself up to the databinding process?  How did it get a handle to the Guitar instance it was bound to?  How do you lay out your markup and control structure in a way that enlists the control in the databinding process and keep things nice and encapsulated? 

Anemic, Non-Encapsulating Approach
I usually see it done (and have done myself) by exposing a property on the user control to accept the entity instance, the page code-behind reacting to the repeater's OnItemDataBound event, finding the control inside of the repeater, and handing it the instance.  Ah la...

<asp:Repeater ID="GuitarRepeater" runat="server" 
                    OnItemDataBound="Guitar_DataBound">
    protected void Guitar_DataBound( object sender, RepeaterItemEventArgs e )
    {
        GuitarControl guitar = (GuitarControl) GuitarRepeater.FindControl("Guitar");
        guitar.GuitarData = (Guitar) e.Item.DataItem;
    }

This works just fine, but the control is overly dependent on it's host.  It's host has to do a lot of work to get the control in the game.  Your control becomes very anemic at this point.

Oblivious, Zombie-Control Approach
Another approach is to make the the control smart enough to go fetch it's own data.  You can drop it on a page and not only does it know how to render a guitar, but it also knows how to go get that guitar.  This approach usually manifests itself as very course-grain controls.  The example above would end up being a <kb:GuitarList ... /> which housed it's own repeater and spits out the raw markup for the guitars inside of the repeater's <ItemTemplate>.  Ah la...

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="GuitarControl.ascx.cs" Inherits="WebApplication1.GuitarControl" %>

<asp:Repeater ID="GuitarRepeater" runat="server">
    <ItemTemplate>
        <table border="1">
            <tr>
                <td>
                    <asp:Label ID="lblID" runat="server" ></asp:Label>&nbsp;
                </td>
                <td>
                    <asp:Label ID="lblName" runat="server"></asp:Label>
                </td>
            </tr>
        </table>
    </ItemTemplate>
</asp:Repeater>
<body>
    <form id="form1" runat="server">
    <div>
       <kb:GuitarList ID="Guitars" runat="server"></kb:GuitarList>
    </div>
    </form>
</body>

Somewhere in there the page code-behind would just hand the list of guitars to <kb:GuitarList ... /> and it would turn right around and use it to databind the repeater. This definitely solves the problem, but with this.... <kb:GuitarList ... /> is a zombie.  It's on an island all by itself.  Who knows how chatty it is with the database?  Is it okay to have multiple instances of this thing on one page?  I don't know.  In my opinion, this approach defeats the purpose of having this content in a control.  You'd be better off with it all in the page.

The real question is - how do I layout my page and my controls in a way so that the controls are:

  • Fine-grained as I want them to be
  • Aware of the data that will be handed to them
  • Easy to drop on a page and enlist them in the databinding process
  • Self encapsulating

A Better Approach
ASP.NET is a beautiful platform.  They really thought this stuff through.  A user control does have a OnDataBinding method that can be overridden.

protected override void OnDataBinding(EventArgs e)

Cool - that smells right, but now what?  EventArgs ain't doing anything useful for me.  NamingContainer... oh yes, NamingContainer.  All this stuff is already wired up for you.  Adding a little bit of code in the code-behind of the control itself does the trick.

protected override void OnDataBinding(EventArgs e)
{
    IDataItemContainer container = this.NamingContainer as IDataItemContainer;

    if (container != null)
    {
        Guitar guitar = (Guitar) container.DataItem;

        this.GuitarID = guitar.GuitarID;
        this.Name = guitar.Name;
    }

    base.OnDataBinding(e);
} 

Use the NamingContainer to grab the guitar entity and act on it.  If it's of type IDataItemContainer, then you're in business.  That's the control's cue that it's being databound as part of a list control.

This approach has a lot of advantages.  The control now has the right amount of separation from the page.  Neither your page, nor your control is overly anemic - it's a harmonious distribution of work. 

Another benefit of this is you can now hand off controls to different developers on the team for concurrent development.  The entity (in this case Guitar) becomes the "contract" between the control and the page.  The developer of this control really doesn't have to worry too much about the page that will be hosting it.  All he/she needs to know is that they will be getting a Guitar handed to them.  Similarly, the page developer doesn't have to do any upfront work to glue all these controls together - just databind as usual.  If the signature of the Guitar changes - no problem.  The control developer can new-up instances of Guitar to test with and act on it as he/she sees fit (encapsulation).

Thoughts?  Comments?

Happy Thanksgiving....

Print | posted on Tuesday, November 21, 2006 12:00 AM | Filed Under [ microsoft .net development ]

Feedback

No comments posted yet.

Post Comment

Title  
Name  
Email
Url
Comment   
Please add 1 and 4 and type the answer here:

Powered by: