January 2007 - Posts

From the Suggestion Box: Reusing object instances with ObjectDataSource

From the suggestion box Marc Brooks asks:

"More about the needed fixes for ObjectDataSource (e.g. allowing it to acquire the object instead of creating exnilo)."

In fact, the ObjectDataSource already supports this functionality through its ObjectCreating and ObjectDisposing events. In the attached ZIP file (download it from the bottom of this post) I have a sample web site that demonstrates a factory for business objects. Some details of the factory:

  • The factory maintains a fixed-sized pool of business objects (the size is configurable).
  • Initially no objects are created.
  • Objects are created on demand, if they are needed.
  • Objects are reused when available.
  • Objects are returned to the pool by the consumer (the page developer does this).

Business object factory API:

namespace Eilon.Samples {
    public static class BusinessObjectFactory {
        public static BusinessObject GetBusinessObject();
        public static void ReturnBusinessObject(object o);
    }
}

The sample object in the demo has some calls to Thread.Sleep in it to help demonstrate expensive operations. Each business object instance takes two seconds to initialize, and each call to its Select method takes another two seconds. By making several rapid requests the demo shows that the object pool's maximum size of three objects is quickly exhausted. As mentioned earlier, we need to wire up the business object factory to the ObjectDataSource using some events:

    protected void ProductsDataSource_ObjectCreating(object sender, ObjectDataSourceEventArgs e) {
        e.ObjectInstance = BusinessObjectFactory.GetBusinessObject();
    }

    protected void ProductsDataSource_ObjectDisposing(object sender, ObjectDataSourceDisposingEventArgs e) {
        BusinessObjectFactory.ReturnBusinessObject(e.ObjectInstance);
    }
...
<asp:ObjectDataSource
    ID="ProductsDataSource"
    runat="server"
    TypeName="Eilon.Samples.BusinessObject"
    SelectMethod="GetProducts"
    OnObjectCreating="ProductsDataSource_ObjectCreating"
    OnObjectDisposing="ProductsDataSource_ObjectDisposing">
</asp:ObjectDataSource>

Running the demo:

  1. Unzip the attached application and either run the file-system Web site from Visual Studio or use IIS to create a virtual directory for it.
  2. Request the page from within the browser. You'll note at the top of the page the status reports something along the lines of:
        Getting object from pool. @633053217271
        Creating new object #0. @633053217271
        Returning #0 to pool. @633053217341
    In this case there are no objects in the pool, so a new one is created and then returned to the pool.
  3. Hit refresh in your browser and you'll see this status:
        Getting object from pool. @633053218295
        Reusing object #0. @633053218295
        Returning #0 to pool. @633053218345
    This is almost identical to the previous status except that it is reusing object #0 instead of creating a new one.
  4. Now for the fun stuff, pay attention closely: Open four instances of your browser in different processes. By this I mean that you should not open one instance of IE or Firefox and then just open new tabs or new windows from the same process. You need to actually have four separate iexplore.exe or firefox.exe processes running. This is critical because by default at least some browsers only allow two simultaneous connections to the same domain in a given process. By having separate processes you'll be able to make four requests to the same page in the same domain at the same time.
    In each of the four browser windows hit the demo page - you have only about two seconds to do this! The status will be something like this:
    Browser 1: Reusing the object from step #3.
        Getting object from pool. @633053220563
        Reusing object #0. @633053220563
        Returning #0 to pool. @633053220613
    Browser 2: Since object #0 is still in use by Browser 1, a new object needs to be created.
        Getting object from pool. @633053220567
        Creating new object #1. @633053220567
        Returning #1 to pool. @633053220637
    Browser 3: Since object #0 and #1 are both in use, another object needs to be created.
        Getting object from pool. @633053220575
        Creating new object #2. @633053220575
        Returning #2 to pool. @633053220645
    Browser 4: The entire object pool is in use since the max is 3 objects. We do a busy-wait loop until an object is available.
        Getting object from pool. @633053220580
        Pool is entirely in use, waiting... @633053220580
        ...
        Pool is entirely in use, waiting... @633053220613
        Reusing object #0. @633053220614
        Returning #0 to pool. @633053220664

Feel free to reuse this code for the business object factory, but be aware that it suffers from serious starvation issues due to its busy-wait loop. Using a more dynamic system for maintaining the pool such as a dynamic size might be more appropriate for some systems. This code is just meant to demonstrate a lesser known feature of the ObjectDataSource.

- Eilon

Posted by Eilon with no comments
Filed under: , ,

How to instantiate templates (properly)

As part of my work on the ASP.NET team I've worked directly with several 3rd party control vendors and have spoken to hundreds of customers at conferences such as PDC and TechEd as well as presented on topics related to building controls and using ASP.NET in general. I've looked at the source code for literally hundreds of controls and although the controls are usually pretty darn cool, sometimes I spot some snippets code that just don't look right. That is, they don't look right to me. I'm sure they look right to whoever wrote them.

I can't tell you how many times I've seen code for a templated control such as this:

public ITemplate MyTemplate { ... }

public override void OnPreRender(EventArgs e) {
    if (MyTemplate != null) {
        MyTemplate.InstantiateIn(this);
    }

I mean, it looks pretty good, doesn't it? It checks if the template is set and if so it instantiates it within the container control. This code is wrong. Very wrong!

Here's the correct way to instantiate a template:

public ITemplate MyTemplate { ... }

public override void OnPreRender(EventArgs e) {
    if (MyTemplate != null) {
        Control templateContainer = new Control();
        MyTemplate.InstantiateIn(templateContainer);
        Controls.Add(templateContainer);
    }
}

What's the point of this templateContainer thing? The answer: control lifecycle catch-up, and it's one of the most important concepts you need to understand in ASP.NET, especially if you're a control developer.

Every time a control is added to a parent control the child control will immediately "catch up" to the lifecycle point of the parent control. The control lifecycle includes the familiar Init, Load, PreRender, and a number of other lifecycle stages related to state management. When a template is instantiated through a call to InstantiateIn, all that happens (typically) is that the controls in the template are instantiated one by one, and added to the container that you passed in to InstantiateIn, again one by one. Imagine the template had two controls, call them GridView1 and SqlDataSource1. When InstantiateIn is called, this is the lifecycle of the child controls:

Instantiate GridView1 and add to live control tree at the PreRender point in the lifecycle
GridView1.Init
GridView1.Load
GridView1.PreRender
Instantiate SqlDataSource1 and add to live control tree at the PreRender point in the lifecycle
SqlDataSource1.Init
SqlDataSource1.Load
SqlDataSource1.PreRender

This scenario is now broken since in GridView1's PreRender it will look for SqlDataSource1, which doesn't exist yet, so it will throw an exception. Each control is doing a full lifecycle catch-up on its own.

How do we fix it? Easy: Add the controls to an unparented template container, and then add the entire container to the live control tree. Using the corrected code, this is the lifecycle of the child controls:

Instantiate GridView1 and add to unparented template container control tree before any lifecycle happens
Instantiate SqlDataSource1 and add to unparented template container control tree before any lifecycle happens
Add template container to live control tree at the PreRender point in the lifecycle
GridView1.Init
SqlDataSource1.Init
GridView1.Load
SqlDataSource1.Load
GridView1.PreRender
SqlDataSource1.PreRender

This time around the child controls do their lifecycle catch-up at the same time so in GridView1's PreRender it can do a FindControl to locate SqlDataSource1 and start grabbing its data. This is much closer to what happens to controls that are directly on the page and not inside templates, which is why it works so well.

This very subtle bug is very hard to notice and even harder to find. I once spent about three days debugging such a bug in a control until I discovered that it simply wasn't using a template container.

Bonus: If you want the controls in the template to be in their own naming container, instead of instantiating a regular System.Web.UI.Control, write a derived control that also implements the INamingContainer interface and use that instead.

- Eilon

Posted by Eilon with 6 comment(s)

From the Suggestion Box: Why can't you use code expressions for properties?

From the suggestion box InfinitiesLoop asks:

"How about, why can't you use code expressions inline with server controls? You can obviously with the DataBinding expression <%# code %>, but you can't simply say <%= code %>. Using a custom expression builder it's only a few lines of code to enable this:

http://weblogs.asp.net/infinitiesloop/archive/2006/08/09/The-CodeExpressionBuilder.aspx

So I'm curious whether something like this was considered for the framework at one point, and if so, why it wasn't included. One disadvantage that expression builder has is that it doesn't work with NoCompile pages, but its so extremely useful for compiled pages."

Well, believe it or not, the spec for the Expressions feature for ASP.NET 2.0 gave CodeExpressionBuilder as an example of an expression builder we could implement. This would have been especially nice since as you discovered it takes exactly one line of code to implement it!

So why didn't we do it? I honestly don't know since I didn't own the feature until much later, but here are some reasons to consider:

  • Very poor designer support for it. For most features in ASP.NET 2.0 we made sure they worked really well in the designer. It's awkward to type code in a teeny little textbox with no Intellisense, syntax highlighting, or those cool squigglies that tell you where you messed up.
  • Doesn't work in non-compiled pages. Since it's code inside your expression and non-compiled pages don't go through the compiler, they simply wouldn't work - you'd get a parser error instead. This is similar to the first reason in that we also made sure that most features in ASP.NET 2.0 worked really well in non-compiled pages.
  • Hard for most users to understand page life cycle. What if I wanted to put some code in the expression that used a "calculated" value from another control? Well, it would surely compile, but the value would be wrong 99% - 100% of the time. Since the expressions execute really really really early in the page lifecycle they are mostly only useful for static values, such as connection strings, app settings, and localized resources. Sounds familiar? :)
  • And finally, my philosophical reason: I believe that code shouldn't be scattered in a page. If you want the value of DateTime.Now, I believe you should have a "TimeExpression" that offers several values, similar to my post on custom data source parameters. If you want one for user names, you should have a "UserNameExpression." It might sound like a lot of work, but I think your pages end up being super clean and easy to read. If it really is too much work, then using the custom CodeExpression is fine. Just be aware of when it won't work.

- Eilon

Posted by Eilon with 1 comment(s)

Suggestion Box Now Open

I opened up a suggestion box so you can tell me what you want to hear about.

Posted by Eilon with no comments
More Posts