April 2007 - Posts

Using ObjectDataSource to do the dirty work for your custom data source

Since ASP.NET 2.0 was released in 2005 many of you have taken advantage of the new data source control controls. Drop a GridView on the page. Bind it to a data source. Check a few checkboxes. Run the page. Go home early!

However, some of you want to take data sources a step further. I've recently been chatting with Tobias Hertkorn on his blog about Trying to enhance ObjectDataSource. I figured I'd take advantage of Windows Live Writer to make a nice looking blog post about some neat techniques you can use to enhance the ObjectDataSource control. Tobi is trying to derive from ObjectDataSource to add some new and much simplified functionality for his users.

Sure enough, Tobi encountered problems taking advantage of ObjectDataSource to do the dirty work of data source architecture. My response mostly boiled down to "this type of extensibility was not what we had in mind for ObjectDataSource" and "We felt that if you need this degree of customization that writing your own data source is not that much of a stretch."

While I'm not taking back those statements, there are still a lot of neat things you can do with ObjectDataSource. For example, let's say you want to have a friendly data source that lets you choose from some simple sources of data instead of requiring users to select a type name and a select method. Following is the all new eStuff Sample FoodDataSource. The FoodDataSource has just one required property: FoodType. You pick the FoodType, and the FoodDataSource figures out what data to return. Pretty simple, isn't it?

namespace eStuff.Samples {
    using System.Collections;
    using System.Web.UI;
    using System.Web.UI.WebControls;

    public class FoodDataSource : ObjectDataSource {

        private string _foodType;

        public FoodDataSource()
            : base(typeof(FoodData).FullName, "GetFood") {
            // Constructor parameters:
            // - ObjectDataSource complains if we don't have a TypeName set.
            // - Our custom object's SelectMethod is always called GetFood.

            // Hook up the ObjectCreating event so we can use our custom object
            ObjectCreating += delegate(object sender, ObjectDataSourceEventArgs e) {
                // Here we create our custom object that the ObjectDataSource will use
                e.ObjectInstance = new FoodData(this);
            };
        }

        // Public property for the user to choose their food type
        public string FoodType {
            get {
                return _foodType;
            }
            set {
                _foodType = value;
            }
        }

        // Custom data object that returns yummy foods
        private sealed class FoodData {
            private FoodDataSource _dataSource;

            public FoodData(FoodDataSource dataSource) {
                _dataSource = dataSource;
            }

            public IEnumerable GetFood() {
                string foodType = _dataSource.FoodType;
                yield return foodType + " yummy 1";
                yield return foodType + " yummy 2";
                yield return foodType + " yummy 3";
                yield return foodType + " yummy 4";
            }
        }
    }
}

And now a sample ASPX page that uses the data source:

<%@ Page Language="C#" %>

<%@ Register Namespace="eStuff.Samples" TagPrefix="eStuff" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Yummy Foods</title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <eStuff:FoodDataSource runat="server" ID="FoodData" FoodType="ice cream">
            </eStuff:FoodDataSource>
            <asp:GridView ID="FoodGrid" DataSourceID="FoodData" runat="server">
            </asp:GridView>
        </div>
    </form>
</body>
</html>

As you can tell from the page, using the FoodDataSource is indeed very simple. No need to worry about long type names and select methods! All the real work was even done in ObjectDataSource's constructor. By passing a reference to the FoodDataSource into the FoodData object, the FoodData object can lazily decide what data it wants to return from its SelectMethod ("GetFood"). My initial version of this control was quite hacky about how and when it knew what data to return. But I ended up with this rather clean approach that doesn't violate and philosophical or technical rules about writing controls.

Have any of you adapted the ObjectDataSource to do your dirty work? Did you run into any problems?

Posted by Eilon with 12 comment(s)
Filed under: , ,

Registering scripts in ASP.NET controls (the right way)

Let's start out with an innocent control that registers a simple script file include:

    public class BaseControl : Control {
        protected override void OnPreRender(EventArgs e) {
            Page.ClientScript.RegisterClientScriptInclude(
                GetType(),
                "InitScript",
                ResolveClientUrl("~/ScriptLibrary/BaseControlInitScript.js"));
            base.OnPreRender(e);
        }
    }

All the script file does is show a little alert message when it is loaded. Stick as many of these BaseControl controls as you want on a page and the script gets included only once. This happens because ASP.NET uses the first parameter (type) and second parameter (key) in the call to RegisterClientScriptInclude to determine the uniqueness of the script. Now a customer decides to write a custom control that derives from my control and for whatever reason decides to add no new functionality (not that it matters, but this customer is really lazy!):

    public class DerivedControl : BaseControl {
    }

What happens if you add one BaseControl and one DerivedControl to the page? How many scripts get included? Well, let's just say that I wouldn't be writing this blog if the answer was only one.

Since the BaseControl used GetType() as its type parameter for RegisterClientScriptInclude, the return value of GetType() is unique for each type that derives from BaseControl. As we all know, typeof(BaseControl) != typeof(DerivedControl). Since the types are unique (and thus distinct), ASP.NET concludes that these are two distinct requests to register a script and it gets included twice in the rendered page.

The fix, fortunately, is very simple. Just replace GetType() with something constant, such as typeof(BaseControl). This way when the code executes in the context of DerivedControl, the constant Type value remains the same, and ASP.NET removes the duplicate registration request.

At this point you might wonder why ASP.NET detects duplicates based on type and key instead of based on URL. The answer: I have no idea why. A possible reason that it was done is that the same script URL might return different results each time. For example, maybe it's a script for advertisements and the URL is http://www.example.org/ads/AutomaticAd.aspx and it returns different script for each request. If ASP.NET eliminated duplicates it might cause a site to not work properly. Another reason is that different URLs get canonicalized to the same value. Should ASP.NET consider "foo.js" and "FOO.js" the same? On Windows they're the same, but on Unix they are not. Keep it simple.

The reality is that it doesn't matter much anymore why it was done since that's the way it is. The important part is that you have to be careful when you're writing controls that register scripts. Go take a look at all your calls to ASP.NET's Page.ClientScript and ASP.NET AJAX's ScriptManager and see what types you're passing in.

 

Unfunny story: In my numerous years reviewing other people's sample ASP.NET controls you wouldn't believe how many people decided it was a good idea to pass in typeof(int) for the type parameter and the string "key" for the key parameter. Now I wonder how many people used two different sample controls on the same page and realized that they don't work together.

Posted by Eilon with 2 comment(s)
More Posts