Creating Custom Data Source Parameters

Posted by: Aaron Goldenthal 8/9/2009 10:58 PM

I’m a fan of using the data source controls to simplify data access.  While there’s a reasonable set of Parameters that come with the framework, it’s easy to quickly get to a point where you want functionality that doesn’t automatically come from one of these parameters.  You can always use the Parameter and update the value programmatically, but you can end up with a lot of logic repeated in various Inserting/Updating/Deleting events.  Luckily, creating a custom data source Parameter to encapsulate this logic is very easy. There’s a good article on 4Guys on Creating Custom Parameter Controls, which is where I learned this technique.  I’m going to go through a couple of examples quickly, feel free to refer to the 4Guys article for a more thorough discussion. To implement a custom Parameter control you essentially need to do three things: Provide an override for the Evaluate method.  This method returns the value of the Parameter. Provide an override for the Clone method.  This method is used by the Visual Studio designer to make a deep copy of the Parameter. Add any properties required to evaluate your Parameter. To illustrate this, I’m going to walk through two custom Parameters that I use frequently: IdentityParameter and AppSettingParameter. IdentityParameter The IdentityParameter returns the name of the logged in user, specifically from HttpContext.Current.User.Identity.Name.  We’ll start with a basic skeleton for the IdentityParameter, which derives from Parameter. 1: public class IdentityParameter : Parameter 2: { 3: public IdentityParameter() 4: { 5: } 6: } For the IdentityParameter, I wanted the option to either include or exclude the domain name of the user since I have different applications that use both options.  So, we’ll add one property to the Parameter. 1: [DefaultValue(true)] 2: public bool IncludeDomain 3: { 4: get 5: { 6: object o = ViewState["IncludeDomain"]; 7: if (o == null) 8: return true; 9: else 10: return Convert.ToBoolean(o); 11: } 12: set 13: { 14: ViewState["IncludeDomain"] = value; 15: } 16: } All of this is pretty standard for a control property – the value is stored in ViewState so it persists between posts, and a default of true is specified. Now, on to evaluating the value of the control.  The Evaluate method is passed two parameters – a reference to the current HttpContext, and a reference to the control that the Parameter object belongs to (SqlDataSource, ObjectDataSource, etc). 1: protected override object Evaluate(HttpContext context, Control control) 2: { 3: if ((context == null) || (context.User == null)) 4: return null; 5: else 6: { 7: if (IncludeDomain) 8: return context.User.Identity.Name; 9: else 10: { 11: string name = context.User.Identity.Name; 12: return name.Substring(name.IndexOf("\\") + 1); 13: } 14: } 15: } We start by checking that the HttpContext and User are not null since these are required to obtain the Identity.  Then we simply return the name value as is, or with the domain stripped off, based on the value of the IncludeDomain property. Finally, we need to override the Clone method.  With that, we’ll add a second constructor that is passed the current parameter. 1: protected IdentityParameter(IdentityParameter original) : base(original) 2: { 3: IncludeDomain = original.IncludeDomain; 4: } 5:  6: protected override Parameter Clone() 7: { 8: return new IdentityParameter(this); 9: } 10:  The new constructor simply calls the base constructor and copies any new properties (in this case IncludeDomain).  The Clone method returns a new IdentityParameter copied from the current parameter. That’s all there is to it.  The IdentityParameter can now be added to any Parameter collection (with intellisense). 1: <InsertParameters> 2: <adg:IdentityParameter Name="UserID" Type="String" /> 3: </InsertParameters> AppSettingParameter The AppSettingParameter returns the value of the AppSettings collection with the specified key.  Again we’ll start with a basic skeleton for the AppSettingParameter, which derives from Parameter. 1: public class AppSettingParameter : Parameter 2: { 3: public AppSettingParameter() 4: { 5: } 6: } For the AppSettingParameter, a key is required to access the appropriate value from the AppSettings collection, so we’ll add one property to the Parameter. 1: public string Key 2: { 3: get 4: { 5: return ViewState["Key"] as string; 6: } 7: set 8: { 9: ViewState["Key"] = value; 10: } 11: } Again, all of this is pretty standard for a control property, so we’ll move on the Evaluate method. 1: protected override object Evaluate(HttpContext context, Control control) 2: { 3: if (String.IsNullOrEmpty(this.Key)) 4: { 5: throw new InvalidOperationException("A Key must be specified for an AppSettingParameter."); 6: } 7: else 8: { 9: return WebConfigurationManager.AppSettings[this.Key]; 10: } 11: } Since we’re not using the HttpContext, we don’t need to check that it’s null, we simply check that a Key is specified and return the value from AppSettings. Finally, we need to override the Clone method.  Again, we’ll add a second constructor that is passed the current parameter. 1: protected AppSettingParameter(AppSettingParameter original) : base(original) 2: { 3: Key = original.Key; 4: } 5:  6: protected override Parameter Clone() 7: { 8: return new AppSettingParameter(this); 9: } The AppSettingParameter can now be added to any Parameter collection (with intellisense). 1: <InsertParameters> 2: <adg:AppSettingParameter Name="Color" Type="String" Key="Color" /> 3: </InsertParameters> The download below includes the complete source code as well as an example using both of these parameters. Download sample website: CustomParameters.zip (171 kb)
Tags: ,
Categories: Data Controls
E-mail | Kick it! | DZone it! | del.icio.us Permalink | Comments (1) | Post RSSRSS comment feed

Creating a Data Control Field – The CounterField

Posted by: Aaron Goldenthal 5/25/2009 9:45 PM

I’ve created a lot of GridViews to display a lot of data.  In many cases they’re simply displaying tabular data and they include one column with a counter cell, typically created with markup like: 1: <asp:TemplateField HeaderText="#"> 2: <ItemTemplate><%# Container.DataItemIndex + 1 %></ItemTemplate> 3: </asp:TemplateField> While this is not overly complicated, rather than create a field like this yet another time, I created a custom data control field to provide that functionality – the CounterField.  Data control fields can be used by both the GridView and the DetailsView (probably why they’re called fields, not columns).  While the CounterField is a fairly simple field, it’s a good introduction to how to create a custom field. The .Net Framework comes with a handful of data control fields, including the BoundField, ButtonField, CheckBoxField, CommandField, HyperlinkField, ImageField, and TemplateField.  All of these are ultimately derived from one base class – DataControlField (ButtonField and CommandField actually derive from ButtonBaseField, which itself derives from DataControlField and exposes several properties used by fields with buttons).  DataControlField includes many of the properties you’re familiar with that are available in all of the various fields – Header/FooterText, HeaderImageUrl, Control/Footer/Header/ItemStyle, and InsertVisible.  As you’ll soon see, this will make developing the CounterField much easier. To start with, we’ll declare the CounterField class, which derives from DataControlField. 1: public class CounterField : DataControlField 2: { 3: } There’s one additional property to add to CounterField on top of what’s already inherited from DataControlField, which we’ll call CountPerPage. 1: public bool CountPerPage 2: { 3: get 4: { 5: object o = ViewState["CountPerPage"]; 6: if (o != null) 7: return (bool)o; 8: else 9: return false; 10: } 11: set 12: { 13: ViewState["CountPerPage"] = value; 14: } 15: } This property will determine whether the count resets with each page, and since we typically won’t want that, the default is false.  As with most control properties, we’ll persist the value to the ViewState. Technically there’s only one method that must be overridden to create a control that derives from DataControlField, and that’s CreateField.  I say technically because DataControlField is an abstract class, and its only abstract method is CreateField.  Doing that alone, however, will create a very boring field (with a header and footer, but no content). 1: protected override DataControlField CreateField() 2: { 3: return new CounterField(); 4: } DataControlField has a method, CloneField, used to clone an existing Field.  When it’s called, it calls CreateField to create a new instance of the appropriate field type, and then calls CopyProperties to copy all of the field’s properties.  Since CounterField has an additional property, we’ll override CopyProperties as well to retain it’s value if the field is cloned. 1: protected override void CopyProperties(DataControlField newField) 2: { 3: base.CopyProperties(newField); 4: ((CounterField)newField).CountPerPage = this.CountPerPage; 5: } Now, to the real meat of the CounterField.  In either a GridView or a DetailsView, a DataControlField ultimately get converted to a TableCell, and the InitializeCell method is called to add content to the cell. 1: public override void InitializeCell(DataControlFieldCell cell, 2: DataControlCellType cellType, DataControlRowState rowState, int rowIndex) 3: { 4: base.InitializeCell(cell, cellType, rowState, rowIndex); 5:  6: if (cellType == DataControlCellType.DataCell) 7: { 8: cell.DataBinding += new EventHandler(OnDataBindField); 9: } 10: } The DataControlField’s InitializeCell method already has the logic we need to populate the header and footer cells, so we’ll call it first to address those.  If the cell is a DataCell, we’ll attach an event handler to the DataBinding event to populate the row index. 1: protected virtual void OnDataBindField(object sender, EventArgs e) 2: { 3: TableCell cell = (TableCell)sender; 4: IDataItemContainer container = (IDataItemContainer)cell.NamingContainer; 5: int rowCount = 6: (CountPerPage ? container.DisplayIndex : container.DataItemIndex) + 1; 7: cell.Text = rowCount.ToString(); 8: } First we need a reference to the cell so we can add the row index.  Since this field can be used in either a GridView or a DetailsView, we’ll utilize the commonality between the two as much as possible.  Both the GridViewRow and the DetailsView implement the IDataItemContainer interface.  You’ve probably made use of this interface in the past and not known it.  This interface is what’s referenced when you use the “Container” keyword within a data control (e.g. Container.DataItem).  The DisplayIndex property is the displayed index on the current page.  The DataItemIndex is the overall index in the data source (including the offset for the page).  So, based on the CountPerPage setting, we pick the appropriate value and set the cell’s Text property. That’s all there is to it.  What’s really nice is that Visual Studio 2008 recognizes this class as a DataControlField, so it appears in Intellisense when adding Columns to a GridView or Fields to a DetailsView. Obviously this is a very simple field, and in most cases the InitializeCell and OnDataBindingField methods would be more complex, but hopefully this was a good introduction. The download includes the complete source code as well as samples of this field used in a GridView and a DetailsView. Download sample website: CounterField.zip (224 kb)
Tags: , , ,
Categories: Data Controls
E-mail | Kick it! | DZone it! | del.icio.us Permalink | Comments (2) | Post RSSRSS comment feed

Manually Databinding a GridView

Posted by: Aaron Goldenthal 4/19/2009 9:36 PM

One of the questions I see frequently on the ASP.NET forums is how to deal with exceptions like The GridView 'GridView1' fired event RowEditing which wasn't handled. The GridView 'GridView1' fired event PageIndexChanging which wasn't handled. The GridView 'GridView1' fired event Sorting which wasn't handled. The GridView 'GridView1' fired event RowDeleting which wasn't handled. when manually databinding a GridView.  When I say manually databinding I mean not using a data source control specified as a DataSourceID, but rather setting the GridView’s DataSource equal to the appropriate data object and calling DataBind.  Developers who were around before ASP.NET 2.0 are familiar with how to deal with this, but since ASP.NET 2.0 most of the examples and tutorials deal with setting the DataSourceID, which buys you a lot of automation that you may not even appreciate unless you’ve done this the old fashioned way.  In this example, we’ll go through a fully featured GridView with editing, deleting, selecting, sorting, and paging functionality that is manually bound and identify the limitations and some of the workarounds. When you use a data source control (e.g. SqlDataSource, ObjectDataSource, LinqDataSource, etc) specified in a DataSourceID, the GridView can automate many functions because, through the data source control, the GridView on its own can perform the following operations: Select data (required any time databinding is required) Update data Delete data Insert data (not necessarily applicable for a GridView, but it is for other data controls) When you manually bind data to a GridView, the GridView itself cannot perform these operations, so they must be implemented in your code.  The GridView does know the events that should be used to implement them, so the exceptions shown above are thrown when you try to perform one of these operations, but you have implemented the appropriate event handler. Working With Other Data Controls The example shown here is for a GridView, but the same types of event handlers must be implemented in roughly the same way for other data controls including the FormView, DetailsView, and ListView.  I’ll try to note any major differences. The general philosophy you’ll see here is: The “-ing” event handlers (e.g. RowEditing, RowUpdating, RowDeleting, Sorting) must be implemented The “-ed” event handlers (e.g. RowUpdated, RowDeleted) do not have to be implemented, and in many cases these events are never raised. To demonstrate the details, I’ve created a very simple table ([UserID] INT, [FirstName] VARCHAR, [LastName] VARCHAR), which we’ll display with the following GridView: 1: <asp:GridView ID="GridView1" runat="server" 2: AllowSorting="true" AllowPaging="true" PageSize="5" 3: AutoGenerateColumns="false" DataKeyNames="UserID" 4: OnPageIndexChanged="GridView1_PageIndexChanged" 5: OnPageIndexChanging="GridView1_PageIndexChanging" 6: OnRowCancelingEdit="GridView1_RowCancelingEdit" 7: OnRowDeleting="GridView1_RowDeleting" 8: OnRowEditing="GridView1_RowEditing" 9: OnRowUpdating="GridView1_RowUpdating" 10: OnSorted="GridView1_Sorted" 11: OnSorting="GridView1_Sorting"> 12: <Columns> 13: <asp:BoundField DataField="UserID" HeaderText="ID" 14: SortExpression="UserID" ReadOnly="true" /> 15: <asp:BoundField DataField="FirstName" HeaderText="First Name" 16: SortExpression="FirstName" /> 17: <asp:TemplateField HeaderText="Last Name" SortExpression="LastName"> 18: <ItemTemplate><%# Eval("LastName")%></ItemTemplate> 19: <EditItemTemplate> 20: <asp:TextBox ID="LastNameTB" runat="server" 21: Text='<%# Bind("LastName") %>'></asp:TextBox> 22: </EditItemTemplate> 23: </asp:TemplateField> 24: <asp:CommandField HeaderText="Actions" ShowDeleteButton="true" 25: ShowEditButton="true" ShowSelectButton="true" /> 26: </Columns> 27: <AlternatingRowStyle CssClass="alt" /> 28: <SelectedRowStyle CssClass="selected" /> 29: <PagerStyle CssClass="pager" /> 30: <PagerSettings Mode="NextPrevious" NextPageText="Next >" 31: PreviousPageText="< Prev" /> 32: </asp:GridView> As you can see this GridView is fairly straightforward: 2 BoundFields 1 TemplateField with a template for display and editing (to show the difference later in retrieving data from a BoundField and a TemplateField) 1 CommandField with the appropriate buttons shown Paging allowed Sorting allowed DataKeyNames set to save the UserID This is the final GridView markup, so you can see the event handlers we’re going to implement.  We’ll start with binding data to the GridView, then build up functionality from there. Databinding the GridView First, we’ll implement a method to databind the GridView, which will be called whenever databinding needs to be performed. 1: protected void BindData() 2: { 3: DataClassesDataContext context = new DataClassesDataContext(); 4: var users = from u in context.Users 5: select u; 6: GridView1.DataSource = users.ToList(); 7: GridView1.DataBind(); 8: } We’re using Linq to SQL to pull data from our database and bind it to the GridView.  We’ll revisit this method when we implement the sorting functionality. We also need force databinding to occur when then page is loaded, which we’ll handle in Page_Load. 1: protected void Page_Load(object sender, EventArgs e) 2: { 3: if (!Page.IsPostBack) 4: { 5: BindData(); 6: } 7: } Paging the GridView Any time the page index is changed, the GridView must be databound, so we need to handle the appropriate event, which in this case is PageIndexChanging. 1: protected void GridView1_PageIndexChanging(object sender, GridViewPageEventArgs e) 2: { 3: GridView1.PageIndex = e.NewPageIndex; 4:  5: GridView1.EditIndex = -1; 6: GridView1.SelectedIndex = -1; 7: } The required actions for paging to work are setting the new page index, setting the DataSource, and calling DataBind (the last two through the BindData method).  In addition to that, we’re resetting the EditIndex and SelectedIndex when the page is changed to avoid cases, for example, where you change pages and are suddenly editing a different item.  BindData could be called in PageIndexChanging, but to show that it the event is raised, we’ve handled that in PageIndexChanged. 1: protected void GridView1_PageIndexChanged(object sender, EventArgs e) 2: { 3: BindData(); 4: } The important point is not which of these two events call BindData, but one of them must or the GridView will not be updated. Sorting the GridView Sorting is a little more complicated than paging.  When bound using a DataSourceID, the GridView internally tracks the current SortExpression and SortDirection, and those are available through properties with the same names.  When you manually bind the GridView, this tracking does not occur, so we must implement some mechanism for tracking those values.  We also want it to be something persistent, so that, for example, if you sort, then page, the sort is maintained.  The way we’ll accomplish this is to create two properties that store those values in ViewState. 1: protected string SortExpression 2: { 3: get 4: { 5: return ViewState["SortExpression"] as string; 6: } 7: set 8: { 9: ViewState["SortExpression"] = value; 10: } 11: } 12:  13: protected SortDirection SortDirection 14: { 15: get 16: { 17: object o = ViewState["SortDirection"]; 18: if (o == null) 19: return SortDirection.Ascending; 20: else 21: return (SortDirection)o; 22: } 23: set 24: { 25: ViewState["SortDirection"] = value; 26: } 27: } We’ll get to how these properties are populated shortly, but first we need to go back to the BindData method.  The original method shown above did not implement any sorting, but now that we have a place to store the SortExpression and SortDirection we need to change that. 1: protected void BindData() 2: { 3: DataClassesDataContext context = new DataClassesDataContext(); 4: var users = from u in context.Users 5: select u; 6:  7: bool sortAscending = 8: this.SortDirection == SortDirection.Ascending ? true : false; 9:  10: switch (this.SortExpression) 11: { 12: case "FirstName": 13: users = sortAscending ? users.OrderBy(u => u.FirstName) : 14: users.OrderByDescending(u => u.FirstName); 15: break; 16: case "LastName": 17: users = sortAscending ? users.OrderBy(u => u.LastName) : 18: users.OrderByDescending(u => u.LastName); 19: break; 20: default: 21: users = sortAscending ? users.OrderBy(u => u.UserID) : 22: users.OrderByDescending(u => u.UserID); 23: break; 24: } 25: GridView1.DataSource = users.ToList(); 26: GridView1.DataBind(); 27: } After specifying the query, we add the appropriate sorting.  We can implement it in this way without a performance impact because the data is not actually retrieved from the database until required, so we can build up a chain of queries up to that point and only the end result will be pulled from the database.  We’re sorting on the appropriate field based on the SortExpression, and setting the direction based on SortDirection by calling either the OrderBy or OrderByDescending methods. Finally we need to populate the SortExpression and SortDirection when they change, and we’ll handle that in the Sorting event. 1: protected void GridView1_Sorting(object sender, GridViewSortEventArgs e) 2: { 3: if (this.SortExpression == e.SortExpression) 4: { 5: this.SortDirection = this.SortDirection == SortDirection.Ascending ? 6: SortDirection.Descending : SortDirection.Ascending; 7: } 8: else 9: { 10: this.SortDirection = SortDirection.Ascending; 11: } 12: this.SortExpression = e.SortExpression; 13:  14: GridView1.EditIndex = -1; 15: GridView1.SelectedIndex = -1; 16: } The new sort expression is available through e.SortExpression based on the user’s selection.  There is a sort direction available through e.SortDirection.  When you bind a GridView through a DataSourceID, the GridView will update this value since it is internally tracking the sort expression, so it knows when to switch between ascending and descending.  When manually databinding the GridView, this will always show ascending, so we need to handle this manually with the following logic: If the SortExpression has not changed, reverse the SortDirection (switching between ascending and descending) If the SortExpression has changed, the SortDirection is ascending After that we save the new SortExpression.  As with paging, we also reset the EditIndex and SelectedIndex when sorting.  Also like paging, so show that it does get called, databinding is handled in the Sorted Event (honestly, we will get to some “-ed” events that are not raised). 1: protected void GridView1_Sorted(object sender, EventArgs e) 2: { 3: BindData(); 4: } Editing Records in the GridView For editing there are events that need to be handled: entering edit mode, cancelling out of edit mode, and updating from edit mode.  First, we’ll handle entering edit mode: 1: protected void GridView1_RowEditing(object sender, GridViewEditEventArgs e) 2: { 3: GridView1.EditIndex = e.NewEditIndex; 4: BindData(); 5: } This is the minimum we need to do to enter edit mode: set the new EditIndex and call BindData (which sets the GridView’s DataSource and calls DataBind). Cancelling out of edit mode is very similar. 1: protected void GridView1_RowCancelingEdit(object sender, GridViewCancelEditEventArgs e) 2: { 3: GridView1.EditIndex = -1; 4: BindData(); 5: } Finally, we’ll handle the RowUpdating method to save the new values.  Most of the automation that’s provided by the GridView when bound using a DataSourceID is lost when manually databinding: The original values are not available through e.OldValues.  The new values are not available through e.NewValues. The DataKeys for the affected row are not available through e.Keys Basically, all the GridView does for you is pass the EditItemIndex through e.RowIndex and dealing with everything else is up to you. 1: protected void GridView1_RowUpdating(object sender, GridViewUpdateEventArgs e) 2: { 3: // e.Keys, e.NewValues, and e.OldValues are only populated if using DataSourceID 4: int id = Int32.Parse(GridView1.DataKeys[e.RowIndex].Value.ToString()); 5:  6: DataClassesDataContext context = new DataClassesDataContext(); 7: var user = (from u in context.Users 8: where u.UserID == id 9: select u).Single(); 10:  11: TextBox firstNameTB = GridView1.Rows[e.RowIndex].Cells[1].Controls[0] as TextBox; 12: user.FirstName = firstNameTB.Text; 13:  14: TextBox LastNameTB = GridView1.Rows[e.RowIndex].FindControl("LastNameTB") as TextBox; 15: user.LastName = LastNameTB.Text; 16:  17: context.SubmitChanges(); 18:  19: GridView1.EditIndex = -1; 20: BindData(); 21: } First, we obtain the UserID of the affected row from the GridView DataKeys collection, which we use to pull the appropriate record from the database.  At that point we need to get the new values so we can update the record, which is done slightly differently for the BoundField and the TemplateField.  For the BoundField, we don’t know the ID of the TextBox since it’s autogenerated by the BoundField, so we obtain a reference to it by getting the first control in the appropriate cell in the edited row.  Since it’s a BoundField, the TextBox is going to be Control[0]. Once we have a reference to the TextBox, we set the new first name. For the TemplateField, we do know the ID of the control since we specified it in the markup, so we obtain a reference to it by calling FindControl on the appropriate row of the GridView.  The GridViewRow is the naming container for the TextBox in this case, so we need to call FindControl on the appropriate row, but we do not need to call it on the specific cell.  Once we have a reference to the TextBox, we set the new last name and save the changes (through the SubmitChanges method). Editing With a BoundField When a BoundField is displaying data (not in edit mode), the appropriate text is added to the Text property of the appropriate cell. When the BoundField is in Edit mode, things are a little different. In that case a TextBox is created and added to the Controls collection for the appropriate cell (and it's the only item added to the Controls collection, so it's Controls[0]). If we were using a TemplateField instead, the compiler translates the markup in the templates to the appropriate types of controls in the same way as the page markup in parsed.  For this example, the ItemTemplate would be transformed into a Literal control, and the EditItemTemplate would be transformed into a Literal control, a TextBox, and a Literal control (the literal controls would include the spacing between the EditItemTemplate tags and the TextBox tags).  In both cases, the controls are added to the controls collection for the cell.  You can see that in the case of a TemplateField, the exact spacing within the control becomes significant, so it’s generally better to get a reference to the control by ID using FindControl than to use the Controls collection. Once the changes are saved, we need to do a little clean up on the GridView.  We reset the EditIndex, and we must again call BindData.  You’ll note we did not accomplish any of this in the RowUpdated event because that event is never fired when manually databinding to a GridView. Deleting Records in the GridView As with updating, most of the automation that’s provided by the GridView when bound using a DataSourceID is lost when manually databinding: The original values are not available through e.Values. The DataKeys for the affected row are not available through e.Keys Basically, all the GridView does for you is pass the RowIndex of the row to delete through e.RowIndex and dealing with everything else is up to you, so it’s very similar to updating. 1: protected void GridView1_RowDeleting(object sender, GridViewDeleteEventArgs e) 2: { 3: // e.Keys and e.Values are only populated if using DataSourceID 4: int id = Int32.Parse(GridView1.DataKeys[e.RowIndex].Value.ToString()); 5: DataClassesDataContext context = new DataClassesDataContext(); 6: var user = (from u in context.Users 7: where u.UserID == id 8: select u).Single(); 9: context.Users.DeleteOnSubmit(user); 10: context.SubmitChanges(); 11:  12: BindData(); 13: } First, we obtain the UserID of the affected row from the GridView DataKeys collection, which we use to pull the appropriate record from the database.  We then delete the record, and call BindData.  As with updating, none of this is handled in the RowDeleted event because that event is never fired when manually databinding to a GridView. Selecting Records in the GridView Selecting is actually the easiest of all of these to implement, all you have to do is – nothing.  Selecting will work without any code.  The reason is that selecting in a GridView does not cause a databind, it simply changes styles related to the selected row.  Note this behavior is not the same for the ListView.  Since the ListView can have a separate template for the selected item, and this may contain different data, the ListView does perform a databind when changing the selected index. Summary This article has detailed the type of code that must be written when manually databinding a GridView with paging, sorting, editing, deleting, and selecting capability. Download sample website: GridViewManualDataBinding_2_0.zip (243 kb) Updated April 26, 2009: Example updated to change one of the BoundFields to a TemplateField to the show the differences in retrieving data between the two types of fields. Updated May 11, 2009: Added VB.NET example.
Tags: ,
Categories: Data Controls
E-mail | Kick it! | DZone it! | del.icio.us Permalink | Comments (37) | Post RSSRSS comment feed

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