ASP.NET Databinding Bind() Method Dissected

Posted by: Aaron Goldenthal 3/15/2009 10:00 PM

I was thinking about a question on the asp.net forums recently and while doing a little research I learned a few things about how the databinding Bind() method really works, and a few of the limitations.

The Bind() methods performs two functions:

  • Extract values during databinding to populate properties of a control (done via the same method used by the Eval() method)
  • Extract values from controls (e.g. to perform insert/update operations)

I’ve set up a quick example to illustrate how each of these functions is performed.  I’m starting with a simple test table ([ID] INT, [Name] VARCHAR, [Value] VARCHAR), and have setup a ListView to display all of the existing data and insert new data.

   1: <asp:ListView ID="ListView1" runat="server" DataSourceID="SqlDataSource1" 
   2:     InsertItemPosition="LastItem">
   3:     <LayoutTemplate>
   4:         <table>
   5:             <thead>
   6:                 <tr>
   7:                     <th>ID</th>
   8:                     <th>Name</th>
   9:                     <th>Value</th>
  10:                 </tr>
  11:             </thead>
  12:             <tbody>
  13:                 <asp:PlaceHolder ID="ItemPlaceholder" runat="server"></asp:PlaceHolder>
  14:             </tbody>
  15:         </table>
  16:     </LayoutTemplate>
  17:     <ItemTemplate>
  18:         <tr>
  19:             <td><%# Eval("ID") %></td>
  20:             <td><%# Eval("Name") %></td>
  21:             <td><%# Eval("Value") %></td>
  22:         </tr>
  23:     </ItemTemplate>
  24:     <InsertItemTemplate>
  25:         <tr>
  26:             <td>
  27:                 <asp:Button ID="InsertButton" runat="server" CommandName="Insert" Text="Add" />
  28:             </td>
  29:             <td>
  30:                 <asp:TextBox ID="NameTB" runat="server" AutoPostBack="true" CausesValidation="true" 
  31:                     Text='<%# Bind("Name") %>'></asp:TextBox>
  32:             </td>
  33:             <td>
  34:                 <asp:TextBox ID="ValueTB" runat="server" Text='<%# Bind("Value") %>'></asp:TextBox>
  35:             </td>
  36:      </tr>
  37:     </InsertItemTemplate>
  38: </asp:ListView>
  39: <asp:SqlDataSource ID="SqlDataSource1" runat="server" 
  40:     ConnectionString="<%$ ConnectionStrings:testConnectionString %>" 
  41:     SelectCommand="SELECT [ID], [Name], [Value] FROM [Table_3]" 
  42:     InsertCommand="INSERT INTO [Table_3] ([Name], [Value]) VALUES (@Name, @Value)">
  43:     <InsertParameters>
  44:         <asp:Parameter Name="Name" Type="String" />
  45:         <asp:Parameter Name="Value" Type="String" />
  46:     </InsertParameters>
  47: </asp:SqlDataSource>

With this example set up, let’s examine how the two Bind() functions are accomplished.

Extracting values during databinding to populate a control

Once the page is compiled you can examine the resulting class (I personally use Reflector).  A method is created to build each of the controls, applying the relevant properties and event handlers from the page markup.  For the first TextBox in the InsertItemTemplate (NameTB), the following method is created.

   1: private TextBox __BuildControl__control10()
   2: {
   3:     TextBox __ctrl = new TextBox { TemplateControl = this };
   4:     __ctrl.ApplyStyleSheetSkin(this);
   5:     __ctrl.ID = "NameTB";
   6:     __ctrl.AutoPostBack = true;
   7:     __ctrl.CausesValidation = true;
   8:     __ctrl.DataBinding += new EventHandler(this.__DataBinding__control10);
   9:     return __ctrl;
  10: }

In addition to all of the declared properties for the TextBox, a method is also created to handle the DataBinding event for the TextBox.

   1: public void __DataBinding__control10(object sender, EventArgs e)
   2: {
   3:     TextBox dataBindingExpressionBuilderTarget = (TextBox) sender;
   4:     ListViewItem Container = 
   5:         (ListViewItem) dataBindingExpressionBuilderTarget.BindingContainer;
   6:     if (this.Page.GetDataItem() != null)
   7:     {
   8:         dataBindingExpressionBuilderTarget.Text = 
   9:             Convert.ToString(base.Eval("Name"), CultureInfo.CurrentCulture);
  10:     }
  11: }

This method gets the current DataItem (through the Page.GetDataItem() method) and set the TextBox’s Text property to the values of the appropriate Field (in this case “Name”).  For completeness, the Eval method called in the last line comes from System.Web.UI.TemplateControl (which the Page class inherits from).

   1: protected internal object Eval(string expression)
   2: {
   3:     this.CheckPageExists();
   4:     return DataBinder.Eval(this.Page.GetDataItem(), expression);
   5: }

Although I’ve only examined one TextBox here, the same technique is used for each case of the Bind() method (as well as the Eval() method).

Extracting values from controls (to perform insert/update)

Extracting values from controls starts with the creation of the ListView.  Like we saw in the previous examples, a method is generated to build the ListView.

   1: private ListView __BuildControlListView1()
   2: {
   3:     ListView __ctrl = new ListView();
   4:     base.ListView1 = __ctrl;
   5:     __ctrl.ApplyStyleSheetSkin(this);
   6:     __ctrl.LayoutTemplate = 
   7:         new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control4));
   8:     __ctrl.ItemTemplate = 
   9:         new CompiledBindableTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control6), 
  10:             null);
  11:     __ctrl.InsertItemTemplate = 
  12:         new CompiledBindableTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control8), 
  13:             new ExtractTemplateValuesMethod(this.__ExtractValues__control8));
  14:     __ctrl.ID = "ListView1";
  15:     __ctrl.DataSourceID = "SqlDataSource1";
  16:     __ctrl.InsertItemPosition = InsertItemPosition.LastItem;
  17:     return __ctrl;
  18: }

I’ll skip over some of the details, but the important part to notice is the InsertItemTemplate.  When the InsertItemTemplate is created, it is built from the declared template.  In this example, that’s via the __BuildControl__control8 method, which uses a similar process to the method shown previously to build the TextBox.  In addition, the method to be used to extract values from the template is also assigned.

   1: public IOrderedDictionary __ExtractValues__control8(Control __container)
   2: {
   3:     TextBox NameTB = (TextBox) __container.FindControl("NameTB");
   4:     TextBox ValueTB = (TextBox) __container.FindControl("ValueTB");
   5:     OrderedDictionary __table = new OrderedDictionary();
   6:     if (NameTB != null)
   7:     {
   8:         __table["Name"] = NameTB.Text;
   9:     }
  10:     if (ValueTB != null)
  11:     {
  12:         __table["Value"] = ValueTB.Text;
  13:     }
  14:     return __table;
  15: }

You can see that the process that is used to extract values is essentially the same as what you would write to pull the values manually:

  • Use FindControl to get a reference to the control
  • If the control is found, pull data from the appropriate property

The data returned is simply a collection of key/value pairs in an OrderedDictionary.  In the case of our ListView Insert, this data would then be used to populate the InsertParameters of the SqlDataSource that’s tied to the ListView.

Bind() Limitations

To illustrate some of the limitations with Bind(), let’s make a small change to the InsertItemTemplate.  I’ve added a CustomValidator for NameTB, and wrapped the TextBox and Validator in an UpdatePanel.  The details of the CustomValidator aren’t really significant, but it could be used, for example, to verify uniqueness.

   1: <InsertItemTemplate>
   2:     <tr>
   3:         <td>
   4:             <asp:Button ID="InsertButton" runat="server" CommandName="Insert" 
   5:                 Text="Add" />
   6:         </td>
   7:         <td>
   8:             <asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
   9:                 <ContentTemplate>
  10:                     <asp:TextBox ID="NameTB" runat="server" AutoPostBack="true" 
  11:                         CausesValidation="true" Text='<%# Bind("Name") %>'></asp:TextBox>
  12:                     <asp:CustomValidator ID="CheckNameUniqueVal" runat="server" 
  13:                         ControlToValidate="NameTB" ErrorMessage="Name already exists" 
  14:                         OnServerValidate="CheckNameUniqueVal_ServerValidate"></asp:CustomValidator>
  15:                 </ContentTemplate>
  16:             </asp:UpdatePanel>
  17:         </td>
  18:         <td>
  19:             <asp:TextBox ID="ValueTB" runat="server" Text='<%# Bind("Value") %>'></asp:TextBox>
  20:         </td>
  21:     </tr>
  22: </InsertItemTemplate>

In the resulting class, the methods to build and data bind the TextBox are the same so I' haven’t repeated them.  What’s interesting is the change to the method to extract values for insert.

   1: public IOrderedDictionary __ExtractValues__control8(Control __container)
   2: {
   3:     TextBox ValueTB = (TextBox) __container.FindControl("ValueTB");
   4:     OrderedDictionary __table = new OrderedDictionary();
   5:     if (ValueTB != null)
   6:     {
   7:         __table["Value"] = ValueTB.Text;
   8:     }
   9:     return __table;
  10: }

As you can see, the code to extract the data from ValueTB hasn’t changed, but the compiler does not even generate the code to extract values from NameTB, which is now within an UpdatePanel.  Attemping to perform an insert results in the dreaded:

Cannot insert the value NULL into column 'Name', table 'test.dbo.Table_3'; column does not allow nulls. INSERT fails. The statement has been terminated.

I haven’t exhaustively researched the cases where this occurs, but the ones I know of are when Bind() is used with:

  • Controls within any control than implements INamingContainer (in this case, FindControl would fail to return a reference to the control).  There are many possibilities here, but one I see people have issues with a lot is a TabPanel.
  • Controls within other server controls, e.g. using Bind() with a TextBox that is within an UpdatePanel or Table webcontrol.

The workaround for this is straightfoward, you just need to extract the values manually.  For this example, I’ve used the ListView’s ItemInserting event.

   1: protected void ListView1_ItemInserting(object sender, ListViewInsertEventArgs e)
   2: {
   3:     TextBox NameTB = (TextBox)ListView1.InsertItem.FindControl("NameTB");
   4:     e.Values["Name"] = NameTB.Text;
   5: }

Hopefully all of this helps clarify what’s really occurring when you use Bind().

Tags:
Categories: Data Controls
E-mail | Kick it! | DZone it! | del.icio.us Permalink | Comments (10) | Post RSSRSS comment feed

Comments (10) -


United States Bob A 
4/19/2009 8:54 AM
Bob A
I've run across this issue when placing bound controls in a server side HTML Table, i.e.: <table runat="server"...>

It completely ignores the controls inside this.  I can't believe this is by design.  My reason for doing this was to dynamically add rows to the table based on a drop down selection in the form view (for an insert).  Seems like a pretty common occurance.


4/19/2009 4:18 PM
Aaron
Bob - I spent a little time today trying to narrow down what the commonality was for the controls that have this problem, which so far seems to be UpdatePanels, Tables, and as you mentioned, HtmlTables.  So far it's still eluding me, but in all of these cases code is not generated to extract the values, so there's obviously something that's triggering the compiler to treat them differently.  On a slightly entertaining note, I found some cases where the extraction code is generated, but the controls are with a NamingContainer and the extraction always fails.


United States Maher 
6/14/2009 1:23 AM
Maher
Aaron, how do you bind to a referenced field.  For example, I have two tables in a relation where table 1 has a column which holds an Id pointing to another table.   I want my GridView to display the content of table 1, but when it comes to the referenced Id, I want it to display the name of the field off table 2 (not the id).


6/19/2009 1:29 PM
Aaron
Maher - The details can vary a little depending on how you're retrieving the data, but in general you can pull the data from the database that you want to display in addition to the other data.  There's an example with the associated SQL at www.asp.net/learn/data-access/tutorial-01-vb.aspx (see step 5).  The article is about typed datasets, so it would apply there, but that SQL could also be used explicitly.


6/27/2009 4:23 AM
Victor
Hi Aaron, I suffered this issue and I think that it is because findControl() can not find a control inserted in a FormView directy if it is into an asp:table.

If you want to search some control inside of asp:table, you should do something like this:

protected void aDataSource_Inserting(object sender, ObjectDataSourceMethodEventArgs e)
    {
        Table aspTable = aFormView.FindControl("anAspTable") as Table;
        TextBox aTextBox = aspTable .Rows[1].Cells[1].FindControl("aTextBox ") as TextBox;
        string value = aTextBox .Text;
    }

Best Regards.

Victor


7/1/2009 4:00 AM
L&#229;n Penge
Thanks a lot for sharing this. Definately bookmarked Smile


7/30/2009 9:49 PM
запознанства
Thank you, for this code Smile Cheers!


7/30/2009 9:52 PM
Hoodia
Good post, I will mention it on my blog.. Cheers


United States quidproms 
1/28/2011 10:58 PM
quidproms
I've got a ListView InsertItem Template that contains a Panel that wraps a Wizard. The Panel is the popup for a ModalPopupExtender. One of the TemplatedWizardSteps contains a table that has the Bind expressions. Indeed the values are not extracted. However all the other magic works great(after some effort)--my employer loves the AJAX wizard approach to filling out the data record. I have not built the EditItem Template yet; but I bet the Eval expressions will fail there too. Each of these Panel, TemplatedWizardStep implements NamingContainer property.  


1/2/2012 7:39 AM
Bj&#246;rne
I've researched this issue extensively and it appears to be caused by components implementing the INamingContainer marker interface getting their own namespace. For example, in a FormViews InsertItemTemplate there way be multiple UpdatePanels and in each UpdatePanel there may be components with id's that are also present in the other UpdatePanels. So reading the fields will work (both fields will be populated), but postbacking wont because asp.net cannot determine which of the fields to save the value from.