Contents tagged with DataBinding

  • More Data Binding

    Last time I tried to solve one of the deficiencies of data binding by taking advantage of expando attributes. Today I want to throw an idea out there that I’ve been playing with since that blog post.I was looking at WPF’s data binding and wondered what it would take to have data binding in ASP.NET be as first class as data binding in WPF.

    Read more...

  • Databinding 3.0

    There was a post on our internal discussion group recently where a customer pointed out one of the weaknesses of 2 way data binding not working within user controls. Consider the following page:

    Read more...

  • A new way to DataBind()

    The Problem 

    I've been thinking alot recently about the problems with data binding (and they're alot). There are some patterns that play well with ASP.NET (and are repeated everywhere) and some that don't quite fit the model. One of those patterns that don't mesh well is, setting the DataSource property of any of the DataControls. Before ASP.NET 2.0 and DataSource controls we'd have to set the DataSource property and manually call DataBind.

    What's wrong with this you may ask? When are you supposed to DataBind? Page_Load ? Page_Init? Page_PreRender? I'm sure anyone that has had to manually data bind has had the problem of figuring out which code goes in and out of the IsPostBack block.

    Whats the solution?

    I'm looking at an alternative approach to this problem. Event handlers are nice because we all feel confident that the author of the event knows when that event should be raised. No need to figure out the postback logic because the control author already thought about that. All you have to worry about is handling the event.

    I propose a new hypothetical event OnDataRetrieveing, that would be on all DataControls in conjunction with a new flag AutoBind to enable it.

    Let's take a look at a sample implementation of a derived GridView control that has this new behavior. First the markup:

    <custom:AutoGridView 
        OnDataRetrieving="OnDataRetrieving"
        OnPageIndexChanging="OnGridViewPageIndexChanging"
        OnRowEditing="OnGridViewRowEditing"
        OnRowCancelingEdit="OnGridViewRowCancelingEdit"
        AllowPaging="true" 
        runat="server" 
        ID="GridView1" 
        AutoGenerateEditButton="true"
        AutoBind="true">
    </custom:AutoGridView>

    Looks pretty similar to the regular GridView besides the AutoBind="true" flag and OnDataRetrieving event.

    And the code behind

    protected void OnGridViewRowCancelingEdit(object sender, GridViewCancelEditEventArgs e) {
        GridView1.EditIndex = -1;
    }
     
    protected void OnGridViewPageIndexChanging(object sender, GridViewPageEventArgs e) {
        GridView1.PageIndex = e.NewPageIndex;
    }
     
    protected void OnGridViewRowEditing(object sender, GridViewEditEventArgs e) {
        GridView1.EditIndex = e.NewEditIndex;
    }
     
    protected void OnDataRetrieving(object sender, DataBindingEventArgs e) {
        NorthwindDataContext db = new NorthwindDataContext();
        e.DataSource = db.Products;
    }

    We write code as we normally would if we'd been binding manually, but instead, we assign our data to the DataSource property in the DataBindingEventArgs (Which gets called at the "right" time).

    So what is the right time to Databind and why does this control do it better than you? I'm not sure :) but let's look at the source of this control.

    public override object DataSource {
        get {
            return base.DataSource;
        }
        set {
            if (!_settingDataSource && AutoBind) {
                throw new InvalidOperationException("When AutoBind is enabled, setting the DataSource property explicitly is not allowed.");
            }
            base.DataSource = value;
        }
    }
     
    protected override void PerformSelect() {
        if (AutoBind) {                
            DataBindingEventArgs args = new DataBindingEventArgs();
            OnDataRetrieving(args); 
            _settingDataSource = true;
            DataSource = args.DataSource;
            _settingDataSource = false;
        }
        base.PerformSelect();
    }
     
    protected override void EnsureDataBound() {
        if (RequiresDataBinding && (AutoBind || IsBoundUsingDataSourceID)) {
            DataBind();
        }
    }

    The most interesting method is EnsureDataBound. This method is called from PreRender and the original condition for making the control DataBind is:

    if (this.RequiresDataBinding && ((this.DataSourceID.Length > 0) || this._requiresBindToNull)) {
        this.DataBind();
        this._requiresBindToNull = false;
    }

    In the case of manually binding this would never be called unless _requiresBindToNull is true. Our AutoGridView control changes this logic by detecting the AutoBind flag and continues to data bind as usual in PreRender.

    If you interested in when the event gets called you can put some break points in the control's code. What do you think about this alternative?

    Here is a link to the source.

    Read more...

  • How <%# Bind %> Works

    In my last post I spoke about 2-way databinding and how it can be used to extract values from control properties. How does this all work? Lets take a look at a page with 2-way databinding:

    <asp:LinqDataSource ID="productsSource"

        runat="server"

        ContextTypeName="FowlerSamples.NorthwindDataContext"

        EnableDelete="True"

        EnableInsert="True"

        EnableUpdate="True" TableName="Products">

    </asp:LinqDataSource>       

    <asp:GridView ID="products"

        DataKeyNames="ProductID,CategoryID"

        AutoGenerateColumns="False"

        runat="server" DataSourceID="productsSource">

        <Columns>

            <asp:CommandField ShowEditButton="True" />               

            <asp:BoundField DataField="ProductName" />

            <asp:TemplateField>

                <EditItemTemplate>

                    <asp:LinqDataSource

                        ID="categoriesSource"

                        runat="server"

                        ContextTypeName="FowlerSamples.NorthwindDataContext"

                        TableName="Categories" AutoGenerateWhereClause="true">

                    </asp:LinqDataSource>

                    <asp:DropDownList

                        runat="server"

                        ID="categories"

                        DataSourceID="categoriesSource"

                        DataTextField="CategoryName"

                        DataValueField="CategoryID"

                        SelectedValue='<%# Bind("CategoryID") %>'>                           

                    </asp:DropDownList>                       

                </EditItemTemplate>                   

            </asp:TemplateField>

        </Columns>

    </asp:GridView>

     In the above example, the GridView has a template field with an EditItemTemplate that has a DropDownList that is 2-way databound. We're going to introduce a small error in the page in order to see what the generated code looks like:

    <EditItemTemplate>

         <asp:LinqDataSource

             ID="categoriesSource"

             runat="server"

             ContextTypeName="FowlerSamples.NorthwindDataContext"

             TableName="Categories" AutoGenerateWhereClause="true">

         </asp:LinqDataSource>

         <asp:DropDownList

            runat="server"

            ID="categories"

            DataSourceID="categoriesSource"

            DataTextField="CategoryName"

            DataValueField="CategoryID"

            SelectedValue='<%# Bind("CategoryID") %>'>                           

        </asp:DropDownList>

        <%# Eval(3) %>

    </EditItemTemplate>


    When we try to run this page we'll get a compile error and the famous ASP.NET YSOD(Yellow Screen of Death):



    Click on Show Complete Compilation Source, if your curious about how ASP.NET converts your the markup to code.

    When examining the source, we see a rather interesting method:

    [System.Diagnostics.DebuggerNonUserCodeAttribute()]

    public System.Collections.Specialized.IOrderedDictionary @__ExtractValues__control8(System.Web.UI.Control @__container) {

        System.Collections.Specialized.OrderedDictionary @__table;

        System.Web.UI.WebControls.DropDownList categories;

     

        categories = ((System.Web.UI.WebControls.DropDownList)(@__container.FindControl("categories")));

     

        @__table = new System.Collections.Specialized.OrderedDictionary();

     

        if ((categories != null)) {

            @__table["CategoryID"] = categories.SelectedValue;

        }

     

        return @__table;

    }


    As you can see in the above method, an OrderedDictionary is created and the SelectedValue property of the DropDownList is pushed into the "CategoryID" field. But how does this get all the way to the data control? Each control has a BuildControl method associated with it, if we examine the BuildControl method for the TemplateField it becomes a bit more clear how things get hooked up.

     

    [System.Diagnostics.DebuggerNonUserCodeAttribute()]

    private global::System.Web.UI.WebControls.TemplateField @__BuildControl__control7() {

        global::System.Web.UI.WebControls.TemplateField @__ctrl;

     

        @__ctrl = new global::System.Web.UI.WebControls.TemplateField();

     

        @__ctrl.EditItemTemplate = new System.Web.UI.CompiledBindableTemplateBuilder(new System.Web.UI.BuildTemplateMethod(this.@__BuildControl__control8), new System.Web.UI.ExtractTemplateValuesMethod(this.@__ExtractValues__control8));

     

        return @__ctrl;

    }

    The EditItemTemplate property is of type ITemplate. CompiledBindableTemplate implements both ITemplate and IBindableTempalte.

    public interface IBindableTemplate : ITemplate {           

        IOrderedDictionary ExtractValues(Control container);

    }

    It's slowly coming together. So lets put together what we're learnt so far:

    1. Code Gen creates ExtractValues method that returns the dictionary of values for each ITemplate that has a Bind expression.
    2. The BuildControl method for the ITemplate's container (TemplateField in this case) assigns a new CompiledBindableTemplate to an ITemplate (EditItemTemplate in this case)
    3. CompiledBindableTemplate implements IBindableTemplate, which has a method, ExtractValues which returns the dictionary given a container.

    It almost all makes sense now. Each data control uses this mechanism to extract values from template fields with 2-way databinding expressions.
    What can you do with your new found knowledge?

    <asp:FormView

        runat="server"

        ID="formView"

        DefaultMode="Edit">

        <EditItemTemplate>

            <asp:TextBox ID="textBox" runat="server" Text='<%# Bind("Name") %>'></asp:TextBox>

            <asp:TextBox ID="textBox1" runat="server" Text='<%# Bind("Age") %>'></asp:TextBox>

            <asp:Button runat="server" ID="updateButton" CommandName="Update" Text="Update" />

        </EditItemTemplate>

    </asp:FormView>


    And the code behind:

     

    class Person {

        public string Name { get; set; }

        public int Age { get; set; }

    }

     

    protected void Page_Load() {

        formView.ItemUpdating += formView_ItemUpdating;

        if (!IsPostBack) {

            formView.DataSource = new Person[] { new Person { Name = "David", Age = 22 } };

            formView.DataBind();

        }

    }

     

    protected void formView_ItemUpdating(object sender, FormViewUpdateEventArgs e) {

        IBindableTemplate template = formView.EditItemTemplate as IBindableTemplate;

        if (template != null) {

            IOrderedDictionary values = template.ExtractValues(formView);

            Response.Write(values["Name"]);

            Response.Write(values["Age"]);

        }

    }


    How cool is that? :)

    Read more...

  • Getting your data out of the data controls

    After you've created a layout with a data control of your choice and eventually bound them to some datasource, you almost always want to get that data back out of them(and why wouldn't you, its your data). One thing you'll learn about developing asp.net webpages is that its like petting a porcupine (yikes). If you go with the flow, you probably won't get stuck, but the moment you try to go against the grain you end up with a hand full of thorns.

    Most of the data controls have events ending in ing and ed e.g. RowUpdating, RowUpdated etc. In most of the event handlers of the ing events it is easy to get the values from the event args.

    private void OnGridViewRowUpdating(object sender, GridViewUpdateEventArgs e) {

        IDictionary keys = e.Keys;

        IDictionary newValues = e.NewValues;

        IDictionary oldValues = e.OldValues;

    }

    Forgive me for the variation in my code formatting I'm trying to find the right one.

    If we look at this event we can see the GridView nicely packages for us the new values, old values and the keys for the updating row. Unfortunately these dictionaries are only filled out if you are bound to a datasource control :(. So that means when you bind to some raw collection and hook up to the DataSource property then call databind, if you try to handle the delete or update events these dictionaries are going to be empty. Right now you must be asking yourself how can you get those dictionaries filled out just as if you were bound to a Datasource control? The good news is you CAN do it(this is what this blog post is all about right?).

    Going Hunting in the Control Tree



    This is probably one of the worst things you can do. From the time you write code that depends on the immediate layout of your page then your asking for trouble. I often see people on the forums writing code like this:

    ((TextBox)GridView1.Rows[e.RowIndex].Cells[2].Controls[1]).Text

    Seeing stuff like that makes my spine tingle (and not in a good way). DO NOT write code like this!

    FindControl
    FindControl is a very powerful method on Control that allows you to search for a nested control, NOT synonymous to DOM function getElementById. I Often see people abusing find control and not understand that it is NOT recursive by default and complain when code like this

    ((TextBox)GridView1.FindControl("TextBox1")).Text

    Throws a null reference exception. My advice is use Use FindControl as a last resort.

    2-way DataBinding
    2-way Databinding is a cool feature in ASP.NET 2.0 which allows the user to write some special syntax to bind against property values they would like to extract from a control.
    There is alot of magic going on behind the scenes (which I will blog about in a separate post), but you do not need to know how it works to use it, e.g

    <asp:GridView runat="server" ID="GridView1">

        <Columns>

            <asp:TemplateField>

                <EditItemTemplate>

                    <asp:TextBox ID="TextBox1" runat="server" Text='<%# Bind("ProductName") %>'></asp:TextBox>

                </EditItemTemplate>

            </asp:TemplateField>

        </Columns>

    </asp:GridView>

    When you write the <%# Bind("ProductName") #> expression then the value of the Text property is pushed into a dictionary which the data control can retrieve later. So how does GridView/FormView/DetailsView/ListView get these values? Each data control has a method which is responsible for populating a dictionary of name value pairs from field name to value. Here is a mapping of data control to method used to extract values:

    GridView => protected ExtractRowValues
    FormView => protected ExtractRowValues
    DetailsView = > protected ExtractRowValues
    ListView => public ExtractItemValues

    As we can see with the exception of ListView these oh so useful methods are protected, that means we can't call them from our code if we are using these built in controls. What can we do to surface these methods:

    • Derive new controls that expose ExtractRowValues through a public method
    • Use private reflection to call the protected method (yikes)
    • Stick all of your interesting fields in DataKeyNames, then use the DataKeys[rowIndex].Values[fieldName] (and watch your ViewState grow :( )
    • Do Nothing :)

    To rectify some of this I've written a little helper for the GridView (and the other controls if you demand) that basically duplicates the functionality of ExtractRowValues method.

    public static IDictionary GetValues(GridViewRow row) {           

        IOrderedDictionary values = new OrderedDictionary();

        foreach (DataControlFieldCell cell in row.Cells) {

            if (cell.Visible) {

                // Extract values from the cell

                cell.ContainingField.ExtractValuesFromCell(values, cell, row.RowState, true);

            }

        }

     

        return values;

    }

    The method itself is pretty simple. It iterates over the control collection of the GridViewRow and calls ExtractValuesFromCell on each cell which puts values into the dictionary.

    Uses
    Most data controls support writing custom commands and handling some Command event(RowCommand for GridView). This method would come in handy if you needed to get the values out of the GridView for some custom command you wanted to execute.

    If you can, use 2 way databinding, if your control doesn't have a useful property to bind against and you need to do some more logic to get the right value then use FindControl.

    Edit:
    I've updated the code to loop over the Cells collection instead of the Controls collection (which is alot cleaner). Also there is no real compelling reason to return a strongly typed IDictionary<string, object>, so I just return the IDictionary instead.

    DetailsView:

    Reader daveh551 needed some help adapting the code to DetailsView so here it is:

    public static IDictionary GetValues(DetailsView detailsView) {
        IOrderedDictionary values = new OrderedDictionary();
        foreach (DetailsViewRow row in detailsView.Rows) {
            // Only look at Data Rows
            if (row.RowType != DataControlRowType.DataRow) {
                continue;
            }
            // Assume the first cell is a header cell
            DataControlFieldCell dataCell = (DataControlFieldCell)row.Cells[0];                
            // If we are showing the header for this row then the data is in the adjacent cell
            if (dataCell.ContainingField.ShowHeader) {
                dataCell = (DataControlFieldCell)row.Cells[1];
            }
     
            dataCell.ContainingField.ExtractValuesFromCell(values, dataCell, row.RowState, true);
        }
        return values;
    }

    FormView:

    public static IDictionary GetValues(FormView formView) {
        IOrderedDictionary fieldValues = new OrderedDictionary();
        ExtractValuesFromBindableControls(fieldValues, formView);
        IBindableTemplate itemTemplate = null;
     
        if (formView.CurrentMode == FormViewMode.ReadOnly && formView.ItemTemplate != null) {
            itemTemplate = formView.ItemTemplate as IBindableTemplate;
        }
        else if (formView.CurrentMode == FormViewMode.Edit && formView.EditItemTemplate != null) {
            itemTemplate = formView.EditItemTemplate as IBindableTemplate;
        }
        else if (formView.CurrentMode == FormViewMode.Insert && formView.InsertItemTemplate != null) {
            itemTemplate = formView.InsertItemTemplate as IBindableTemplate;
        }
        if (itemTemplate != null) {
            foreach (DictionaryEntry entry in itemTemplate.ExtractValues(formView)) {
                fieldValues[entry.Key] = entry.Value;
            }
        }
        return fieldValues;
    }
     
    private static void ExtractValuesFromBindableControls(IOrderedDictionary values, Control container) {
        IBindableControl control = container as IBindableControl;
        if (control != null) {
            control.ExtractValues(values);
        }
        foreach (Control childControl in container.Controls) {
            ExtractValuesFromBindableControls(values, childControl);
        }
    } 

    Let me know if you find any bugs.
    Hope this helps

    UPDATE: I've had some requests to add more data controls and to convert the code to VB etc. So I decided to instead put the code in an assembly and make available for download here.

    Read more...