Blog Home  Home Feed your aggregator (RSS 2.0)  
Playing with the Data Binding Infrastructure - Manuel Abadia's ASP.NET stuff
 
# Friday, April 21, 2006

ASP.NET 2.0 data binding infrastructure is a lot better than the previous binding infrastructure. However, for complex scenarios is not mature enough. For example, suppose we’re making a web application to sell products. Obviously we need to categorize the products, so we have 2 tables:

Categories
(
idCategory int Identity (1, 1),
name nvarchar(50) NOT NULL,
idParentCategory int NOT NULL
)

Products
(
 idProduct int Identity(1, 1),
 name nvarchar(50) NOT NULL,
 price decimal(10, 2) NOT NULL,
 category int NOT NULL
)

The categories table is designed to hold any number of levels but we’ll be using only two levels to make things easier.

We’ll have a web form to add products using a FormView. We’ll be using TableAdapters for the DAL in order to avoid spending too much time in things that are not of interest right now.

A product is related to a subcategory, and a subcategory is related to a category, so in the FormView we’ll have a textbox for the name and price, and two dropdownlists, one for the category and one for the subcategory. To perform automatic two way data binding the name, price and selected subcategory will be bound using a Bind() expression. To insert the product we’ll use an ObjectDataSource with a ProductsTableAdapter. To get the categories we’ll use another ObjectDataSource with the CategoriesTableAdapter. Finally the subcategories will be loaded using another ObjectDataSource with a CategoriesTableAdapter, but the subcategories will not be loaded until a category has been selected. The ASPX page looks like:

<asp:FormView ID="FormView1" runat="server" DataKeyNames="idProduct" DataSourceID="ObjectDataSource1"

            DefaultMode="Insert">

            <InsertItemTemplate>

                name:

                <asp:TextBox ID="nameLabel" runat="server" Text='<%# Bind("name") %>'></asp:TextBox><br />

                price:

                <asp:TextBox ID="priceLabel" runat="server" Text='<%# Bind("price") %>'></asp:TextBox><br />

                category:

                <asp:DropDownList ID="ddlCategories" runat="server" AppendDataBoundItems="True" AutoPostBack="True"

                    DataSourceID="ObjectDataSource2" DataTextField="name" DataValueField="idCategory"

                    OnSelectedIndexChanged="ddlCategories_SelectedIndexChanged">

                    <asp:ListItem Selected="True" Value="-1">[Select a category]</asp:ListItem>

                </asp:DropDownList>&nbsp;

                <br />

                subcategory:

                <asp:DropDownList ID="ddlSubcategories" runat="server" DataSourceID="ObjectDataSource3"

                    DataTextField="name" DataValueField="idCategory" Enabled="False" SelectedValue='<%# Bind("category") %>'>

                </asp:DropDownList>

                <br />

                <asp:LinkButton ID="InsertButton" runat="server" CausesValidation="True" CommandName="Insert"

                    Text="Insert">

                </asp:LinkButton>

                <asp:LinkButton ID="InsertCancelButton" runat="server" CausesValidation="False" CommandName="Cancel"

                    Text="Cancel">

                </asp:LinkButton>

            </InsertItemTemplate>

        </asp:FormView>

        <asp:ObjectDataSource ID="ObjectDataSource1" runat="server"

            SelectMethod="GetDataById" InsertMethod="Insert" OldValuesParameterFormatString="original_{0}"

            TypeName="ProductsTableAdapters.ProductsTableAdapter" OnInserted="ObjectDataSource1_Inserted">

            <InsertParameters>

                <asp:Parameter Name="name" Type="String" />

                <asp:Parameter Name="price" Type="Int32" />

                <asp:Parameter Name="category" Type="Int32" />

            </InsertParameters>

            <SelectParameters>

                <asp:QueryStringParameter Name="idProduct" QueryStringField="idProduct" Type="Int32" />

            </SelectParameters>

        </asp:ObjectDataSource>

        <asp:ObjectDataSource ID="ObjectDataSource2" runat="server" OldValuesParameterFormatString="original_{0}"

            SelectMethod="GetDataByParent" TypeName="CategoriesTableAdapters.CategoriesTableAdapter">

            <SelectParameters>

                <asp:Parameter DefaultValue="-1" Name="idParentCategory" Type="Int32" />

            </SelectParameters>

        </asp:ObjectDataSource>

        <asp:ObjectDataSource ID="ObjectDataSource3" runat="server" SelectMethod="GetDataByParent"

                    OldValuesParameterFormatString="original_{0}" TypeName="CategoriesTableAdapters.CategoriesTableAdapter"

                    OnSelecting="ObjectDataSource3_Selecting">

                    <SelectParameters>

                        <asp:Parameter DefaultValue="-1" Name="idParentCategory" Type="Int32" />

                    </SelectParameters>

                </asp:ObjectDataSource>

        <br />

        <asp:Label ID="lMessage" runat="server"></asp:Label><br />

 

Note that I have enable AutoPostback in the categories dropdownlist. When we run the page for the first time, the FormView1 gets instanced and because it’s bound using DataSourceID, on the PreRender stage the DataBind method will be called (take a look at my posts of January about Data Source Controls if you don’t know the internal behaviour of a data source control). When the DataBind is called in the FormView, the InsertTemplate is instantiated (because it’s in Insert mode) and the child controls are created. Because the dropdownlists are bound using DataSourceId and we’re on the PreRender stage, the DataBind method will be called for each dropdownlist. As we want to populate the subcategories only if there is a category selected, and initially we have the item: “[Select a category]” selected, we capture the Inserting event of the ObjectDataSource that loads the category and we cancel loading the data if there isn’t any real category selected:

protected void ObjectDataSource3_Selecting(object sender, ObjectDataSourceSelectingEventArgs e)

    {

        if (FormView1.CurrentMode == FormViewMode.Insert) {

            DropDownList ddlCategories = (DropDownList)FormView1.FindControl("ddlCategories");

 

            // if we haven't selected a category avoid getting the subcategories

            if (ddlCategories.SelectedValue == "-1") {

                e.Cancel = true;

            }

        }

    }

 

After the page completes the lifecycle, the user can select a category. Because the associated dropdownlist has the AutoPostBack property, the SelectedItemChanged gets fired and the associated handler gets executed. In the handler, we get the selected category and force a rebind of the subcategories. In case the user selected the “[Select a category]” option, we clear the subcategories. To force a rebind we just change a parameter from the ObjectDataSource (as I explained in my posts about the ObjectDataSource, if any of the SelectParameters or FilterParameters change the DataBind method will be called on the associated control on the PreRender stage):

protected void ddlCategories_SelectedIndexChanged(object sender, EventArgs e)

    {

        // when a category is selected, get the subcategories of the selectedcategory

        if (FormView1.CurrentMode == FormViewMode.Insert) {

            // get the controls

            DropDownList ddlCategories = (DropDownList)FormView1.FindControl("ddlCategories");

            DropDownList ddlSubcategories = (DropDownList)FormView1.FindControl("ddlSubcategories");

 

            // update the select parameters (causes a rebind)

            ObjectDataSource3.SelectParameters["idParentCategory"].DefaultValue = ddlCategories.SelectedValue;

            if (ddlCategories.SelectedValue != "-1") {

                ddlSubcategories.Enabled = true;

            } else {

                ddlSubcategories.Enabled = false;

                ddlSubcategories.Items.Clear();

            }

        }

    }

However, when the subcategories dropdownlist have its DataBind method called the following error appears:

“Databinding methods such as Eval(), XPath(), and Bind() can only be used in the context of a databound control”

Why? Because this time when the binding occurs there is no binding context. The first time the page loads, when the DataBind method gets called for the FormView, as the FormView is a control that supports two way data binding, it has a binding context that is pushed on a stack that the Page has for data binding. Eval() and Bind() expressions internally use the binding context from the Page and fail if it isn’t a binding context.
When we force a rebind for the subcategories dropdownlist, the DataBind is called. Internally the data binding is implemented handling the DataBinding event and calling a method that does the actual binding evaluating the binding expressions (see my february post Data Source Controls - Under the Hood,  Part 4). As the subcategory dropdownlist is like this:

<asp:DropDownList ID="ddlSubcategories" runat="server" DataSourceID="ObjectDataSource3"

                    DataTextField="name" DataValueField="idCategory" Enabled="False" SelectedValue='<%# Bind("category") %>'>

                </asp:DropDownList>

 

The associated method that performs the data binding is:

public void @__DataBinding__control10(object sender, System.EventArgs e) {

        System.Web.UI.WebControls.DropDownList dataBindingExpressionBuilderTarget;

        System.Web.UI.WebControls.FormView Container;

        dataBindingExpressionBuilderTarget = ((System.Web.UI.WebControls.DropDownList)(sender));

        Container = ((System.Web.UI.WebControls.FormView)(dataBindingExpressionBuilderTarget.BindingContainer));

        if ((this.Page.GetDataItem() != null)) {

            dataBindingExpressionBuilderTarget.SelectedValue = System.Convert.ToString(this.Eval("category"), System.Globalization.CultureInfo.CurrentCulture);

        }

    }

 

As the dropdownlist has a Bind expression the above method calls the GetDataItem method from the page that retrieves the current binding context. If there isn’t any binding context the method throws an exception and the above error appears. So we have to manually bind the subcategories dropdownlist in code without using the ObjectDataSource. I think this scenario should be supported by the binding infrastructure somehow (maybe adding more flexibility to the Bind expressions and binding context).

As I didn’t want to write the code I “played” a bit with the data binding infrastructure to simulate a binding context. The following is a BIG INTRUSIVE HACK that works but I don’t recommend to use. Anyway it can help you to understand some of the inner details of the data binding infrastructure.

 The binding context is added to the Page at the beginning of the DataBind method in the Control class and removed at the end. As the methods to push and pop binding contexts are internal the only way to access them is using reflection. When the binding is performed the current context is accessed using the GetDataItem method in the Page class and in the scenario we’re investigating, the binding infrastructure asks to the object returned from GetDataItem the value of the “category” property (as specified in the Bind expression). After the “category” value has been read it is bound to the SelectedValue of the dropdownlist. This is not what we want. We added a Bind expression to have the SelectedValue available in the Insert method for the ObjectDataSource that insert products, but as we’re now making a select operation to get subcategories on the same control, the Bind expression is being evaluated the other way (Eval works one way, Bind works two way and here another binding expression that works one way but the opposite way than Eval would help). However we can avoid evaluating the Bind expression if the current binding context is null (see the generated code). If we could set up a null binding context the error will not appear and the Bind expression will not be evaluated. The problem now is when to push the binding context and when to pop it from the stack. We only need the context when the generated method does the actual binding so if we could push it before calling the method and pop it after we’re done. There’s a way to do that. The method that performs the actual binding handles the DataBinding event, and the Control class has an event list where it stores the handlers for the events. We can get the Delegate that performs the actual binding and replace it with a custom one that first pushes a new null binding context, then calls the method that performs the actual data binding and then pops the binding context. I have encapsulated the code in a class:

using System;

using System.ComponentModel;

using System.Configuration;

using System.Reflection;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

 

/// <summary>Class that helps to simulate a fake binding context</summary>

public class FakeBinder

{

    protected Page _page;

    protected Control _control;

    protected Delegate _originalDataBind;

 

    protected FakeBinder(Page page, Control control)

    {

        _page = page;

        _control = control;

    }

 

    protected void Init()

    {

        Type controlType = typeof(Control);

 

        // get the Events property of the Control class

        PropertyInfo eventsProp = controlType.GetProperty("Events", BindingFlags.NonPublic | BindingFlags.Instance);

        EventHandlerList events = eventsProp.GetValue(_control, null) as EventHandlerList;

 

        // get the static EventDataBinding object of the Control class

        FieldInfo eventDataBindingField = controlType.GetField("EventDataBinding", BindingFlags.NonPublic | BindingFlags.Static);

        object eventDataBinding = eventDataBindingField.GetValue(null);

 

        // get the delegate used for the DataBinding event by the control

        Delegate del = events[eventDataBinding];

 

        // find the generated DataBinding method and remove it

        foreach (Delegate currentDelegate in del.GetInvocationList()) {

            // the generated method has a fixed prefix

            if (currentDelegate.Method.Name.StartsWith("__DataBinding__control")) {

                _originalDataBind = currentDelegate;

                events.RemoveHandler(eventDataBinding, currentDelegate);

            }

        }

 

        // adds another handler to the DataBinding event

        _control.DataBinding += new EventHandler(control_DataBinding);

    }

 

    protected void control_DataBinding(object sender, EventArgs e)

    {

        // get the pushDataBindingContext method

        Type pageType = typeof(Page);

        MethodInfo pushDataBindingContextMethod = pageType.GetMethod("PushDataBindingContext", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

 

        // call the pushDataBindingContextMethod with a null BindingContext

        pushDataBindingContextMethod.Invoke(_page, new object[] { null });

 

        // call the old handler. The handler will not fail because there is a BindingContext but as

        // the GetDataItem method returns null it will not try to bind the values to the control

        _originalDataBind.DynamicInvoke(sender, e);

 

        // remove the BindingContext

        System.Reflection.MethodInfo popDataBindingContextMethod = pageType.GetMethod("PopDataBindingContext", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

        popDataBindingContextMethod.Invoke(_page, null);

    }

 

    public static void SimulateBindingContext(Page page, Control control)

    {

        FakeBinder binder = new FakeBinder(page, control);

        binder.Init();

    }

}

 

The SelectedItemChanged handler modified to use the hack is:

protected void ddlCategories_SelectedIndexChanged(object sender, EventArgs e)

    {

        // when a category is selected, get the subcategories of the selectedcategory

        if (FormView1.CurrentMode == FormViewMode.Insert) {

            // get the controls

            DropDownList ddlCategories = (DropDownList)FormView1.FindControl("ddlCategories");

            DropDownList ddlSubcategories = (DropDownList)FormView1.FindControl("ddlSubcategories");

 

            // update the select parameters (causes a rebind)

            ObjectDataSource3.SelectParameters["idParentCategory"].DefaultValue = ddlCategories.SelectedValue;

            if (ddlCategories.SelectedValue != "-1") {

                ddlSubcategories.Enabled = true;

            } else {

                ddlSubcategories.Enabled = false;

                ddlSubcategories.Items.Clear();

            }

 

            // simulate a binding context so when the dropdownlist is being bound the Bind expression don't generate an exception

            FakeBinder.SimulateBindingContext(this, ddlSubcategories);

        }

    }

 

(The only change is the last line)

Enough hacking for now. Any ideas to avoid coding if the FormView is used to edit an existing product?

SmartHacking.zip (202.3 KB)
Friday, April 21, 2006 2:15:05 AM (Romance Daylight Time, UTC+02:00)  #    Comments [2]   ASP.NET  | 
Friday, November 16, 2007 3:03:15 PM (Romance Standard Time, UTC+01:00)
Thanks, it works for me !
Monday, May 5, 2008 12:56:18 PM (Romance Daylight Time, UTC+02:00)
it is much easier to do this.DataBindChildren();
where this is control
All comments require the approval of the site owner before being displayed.
Name
E-mail
Home page

Comment (Some html is allowed: a@href@title, strike) where the @ means "attribute." For example, you can use <a href="" title=""> or <blockquote cite="Scott">.  

[Captcha]Enter the code shown (prevents robots):

Live Comment Preview
Copyright © 2020 Manuel Abadia. All rights reserved.
DasBlog 'Portal' theme by Johnny Hughes.