Introduction

In the previous version of the .NET Framework, it was difficult to integrate our own domain objects into the presentation layer, and nearly impossible to obtain an integration level like the DataSet. With the new data binding infrastructure and the ObjectDataSource, this is easier.

ObjectDataSource lets us integrate our own objects in the data binding framework without much work, but we need to understand how this beast works.

Background

Knowing how the new data binding infrastructure works helps, but it's not required to understand most of the article. If you want to improve your knowledge about the new data binding infrastructure, look at the references at the bottom of the article.

The Basics

ObjectDataSource needs an object that will take care of the CRUD operations (Create, Retrieve, Update, and Delete) of your domain objects.

You specify the type of this object to the ObjectDataSource using the TypeName property. This object (from now, the data mapper object) has to be designed with some guidelines in mind, to be used properly by the ObjectDataSource:

Each time the ObjectDataSource needs to make a CRUD operation, it will try to find (using reflection) the method to call (of course, you need to set some properties to help it).

If it’s a static method, it will get the parameters and call it.

If it’s an instance method, it will create an object of the class, it will call the method, and then destroy the instance (there is an exception to this, but more about it later).

If you don’t want the control to create and destroy instances, you can override the default behaviour, capturing the Creating and Disposing events. If you supply a value for the argument ObjectInstance, the control will use this object instead of creating a new instance. If you set the Cancel argument of the Disposing event, the control won’t try to dispose the object.

The control doesn't cache reflection data, so every time a CRUD operation is performed, it has to search for the requested method and invoke it.

We have to specify our CRUD methods using the properties SelectMethod, InsertMethod, UpdateMethod, and DeleteMethod (there’s a property called SelectCountMethod that will be explained later) if we want them to be called by the ObjectDataSource. Of course, if we only want to retrieve data, we only need to specify the SelectMethod (we’re required to specify at least the Select method, the others are optional).

ObjectDataSource lets us use two modes for passing parameters for the data mapper’s methods:

simple types

custom objects

If we’re using the first mode, the method should have one parameter for each bound value, and optionally, some extra parameters you can specify (more on this later). In the second mode, the parameter is an object with at least a property for each bound value, and optionally, some extra parameters you can specify. The control has a property called DataObjectTypeName that, when set, specifies that we’re using the second mode and the object’s type used for the parameter. If we don’t set it or if it’s null or empty, we’re using the first mode.

Most of the extra parameters I was talking about can be specified for each CRUD method, in a related collection: SelectParameters for the SelectMethod (the SelectMethod can also use the FilterParameters, but I’ll talk more about that later), InsertParameters for the InsertMethod, UpdateParameters for the UpdateMethod, and DeleteParameters for the DeleteMethod. To make things more complicated, the control lets us handle optimistic offline concurrency if we set the property ConflictDetection to ConflictOptions.CompareAllValues, instead of the default value ConflictOptions.OverwriteChanges. If we want to use optimistic offline concurrency, the number of parameters passed to the data mapper’s CRUD methods increases. If that wasn’t enough, the control lets us perform sorting, filtering, and paging, and even it has some caching support. It can be a bit intimidating the first time you use it, but after some practice, it becomes easy to use. To continue the explanation, I’ll be showing an example of how to use it. Our goal is to show a list of products and be able to edit or delete one of them using a GridView. Our products will be instances of the Product class, that has four properties: ID, Name, Description, and Price. We have a table called Products, with a column for each property.

I’ll be using custom objects for the parameters (the Product class) instead of simple types. My data mapper class will be called ProductsDAL. Let's start with the retrieve method (paging is covered later):

If a DataSet is returned, the ObjectDataSourceControl will work with a view of the first table in the DataSet. If a DataTable is returned, the control will work with a view of the DataTable. If the object returned from the select method is not a DataView, a DataSet, a DataTable, or an ICollection, it is wrapped in an object array of one element and handled as an IEnumerable type.

You may wonder why I didn’t make the LoadAll a static method. The answer is because I need an instance method to support paging efficiently (as I will show later). Now, I have to create a new web form, so I add an ObjectDataSource, and configure it using the smart tag: in the first dialog, I chose the ProductsDAL class, and in the Select tab, the LoadAll method. After clicking OK, we can look at the generated markup to see that what the wizard did for us was to set the TypeName property to ProductsDAL and the SelectMethod to LoadAll.

After that, drag a GridView to the form, choose the ObjectDataSource as its data source, then set AutoGenerateColumns to false, and go to the Columns collection to set the following order for the columns: ID, Name, Description, and Price. Set the ID column to be read-only, and close the Columns editor. The last thing to do is to set the GridView’s DataKeyNames property to ID, as the Product table's primary key is the ID column.

You should see a table with the products if you run the application.

To update the database, we write the following method in the ProductsDAL class:

In order to modify the example to support updating, we can use the wizard again to update the ObjectDataSource, or better specify the following properties:

UpdateMethod = Edit

DataObjectTypeName = Product

The Select method is the same for both modes to pass parameters. However, the Insert, Update, and Delete methods are different, and that’s why it’s needed to specify the DataObjectTypeName now. Every time they’re called, new instances of that type are created using the default constructor. We have to set the GridView’s AutoGenerateEditButton property to true in order to allow the grid to be editable.

If we run the application now, we could be lucky enough and be able to edit the value. However, if our current culture does not format decimals the same way the InvariantCulture does, we’ll get an error:

Input string was not in a correct format.
[…]
1,00 is not a valid value for Decimal.
[…]

If we are having this problem (a bug in ObjectDataSource), write the following at the top of the Page_Load method:

and set ObjectDataSource’s DeleteMethod to Delete, and GridView’s AutoGenerateDeleteButton to true.

If we set a break point in the Delete method, you’ll notice that even a Product object is passed to the method, the only property that has been set is the ID. That’s by design. The only properties set in the product are the ones specified in the GridView’s DataKeyNames, so if we forget to set that property, the Delete method will fail.

If we prefer to pass parameters to our data mapper using simple types, we have to delete the property DataObjectTypeName and change the method for update and delete, since now, it will not accept a Product as the parameter:

Recall that in our GridView, we had four bound fields (ID, Name, Description, and Price), and one was marked as ReadOnly (ID). To the Insert, Update, and Delete operations, the framework passes some IOrderedDictionary containing the values related to the current operation (more about this later). For the Update operation, there’s a dictionary containing the control values extracted from the Bind expressions that were in the bound control. As a bound field is just a wrapper around a Bind expression in edit mode and an Eval expression in normal mode, when Update gets called, the framework extracts the following dictionary from the GridView (I’m editing the first product):

As the ID is a ReadOnly field, it doesn’t have an underlying Bind expression, so when ExtractValues is called on the IBindingTemplate, the result doesn’t have an entry for ID. However, as we specified the ID as primary key for the GridView using the DataKeyNames, when the GridView calls the Update method, it also passes the primary keys as parameters. ObjectDataSource has a property called OldValuesParameterFormatString that it uses internally to format some dictionary entries like the keys, so as we have set OldValuesParameterFormatString="old_{0}", the keys dictionary passed to the Update method after formatting the name:

{ “old_id”, “1”}

That’s why the Update method takes the following parameters:

int old_id

string name

string description

decimal price

For the Delete method, only the primary key is needed.

Paging

In order to add paging support, first, we have to enable it in the GridView, setting the AllowPaging property to "true". Also, set the PageSize to something low, and insert more rows to the Products table.

Now, we have to set the EnablePaging property in the ObjectDataSource to let it know that we want to support paging. If we have enabled paging, our Select method will need to handle two additional parameters. Their names are configurable using the StartRowIndexParameterName and MaximumRowsParameterName properties. If we don’t set those properties, the default values are used: startRowIndex and maximumRow. For the example, I used startIndex and maxRows respectively.

Then, we have to modify the Select method to implement a paging mechanism. When the GridView needs data, it will call the ObjectDataSourceView’s Select method with the index for the initial item it wants and the maximum number of items it wants (the initial values will depend on what you set on GridView’s PageIndex and PageSize properties). There is a last thing we need to do to support paging properly. A pager needs to know how many pages it has to show. In order to calculate that, it needs to know the total number of entries.

There’s a property called SelectCountMethod where we specify the name of a method that returns the total number of registers. I’ve seen a lot of people complaining about having another method to return that value instead of doing it in the SelectMethod because they have to do the same query twice. I don’t know why they choose to make two distinct methods, but fortunately, we can avoid executing the same query twice.

Previously, I said that if the method to call is an instance method, ObjectDataSource creates an instance of our data mapper class, then calls the method, and then disposes the instance. There’s an exception to this. If we enable paging, and our SelectMethod and SelectCountMethod are instance methods, the same instance is used when calling both methods so we can save the total number of registers in a field in the SelectMethod, and then we can return it in the SelectCountMethod. Before going to code, I’m going to explain the method I have used for paging in this example. It’s not the best one, but works well for not very big tables, and it’s easy to read. What I do is create a temporary table where I’ll assign a correlative number (starting at 0) to all my primary keys in the Products table. Then, I’ll select only the rows I need from the temporary table, joining it with the Products table, to return all the information needed for the products I’m interested in, and also the total number of rows of the temporary table.

Sorting and Filtering

To add sorting, the only thing we have to do is to configure the GridView’s AllowSorting property to true and set the ObjectDataSource’s SortParameterName to sortedBy. Now, the Select method needs another parameter called sortedBy that will contain the sorting order. In the source code, you can see the source code for the LoadAll method with sorting.

If our Select method returns a DataView, DataTable, or DataSet, and we want to use the sorting capabilities of the DataView, we don’t have to specify the SortParameterName because the SortExpression passed to the Select method in the DataSourceSelectArguments is used to set the sort property on the DataView that is finally used. If we set the SortParameterName in this case, the sorting will not work properly (the control will set the SortExpression to the empty string) because it thinks that we're doing our own sorting.

If our Select method returns a DataTable or DataSet, we can also enable filtering. If our Select method returns a DataView, filtering will not work (probably a bug). To use filtering, we have to set ObjectDataSource’s FilterExpression property to a string, with placeholders for parameters (for example, Name LIKE {0}). Each placeholder in the FilterExpression will be substituted for the corresponding filter parameter from the FilterParameters collection ({0} for FilterParameters[0], {1} for FilterParameters[1], etc.).

Now that parameters have appeared, let’s talk a little bit about them. For each CRUD method, you have an associated parameter collection. Insert, Update, and Delete methods are passed one or more dictionaries with key/value pairs. Each CRUD method uses the associated parameter collection and the associated dictionaries to obtain a final dictionary of key/value pairs that will correspond to the parameters and values passed to its associated method (the parameters are case insensitive). Each method has a specific parameter merging strategy.

The last thing you should know about ObjectDataSource’s parameters is that SelectParameters and FilterParameters are persisted in the ViewState and re-evaluated on the LoadComplete stage. Any change on them at any time will trigger a DataSourceViewChanged event since the control captures the ParametersChanged events for both collections.

Events

The ObjectDataSource control has events for the data mapper’s object creation and disposal, and for CRUD events.

The creation and disposal events were explained before, so the only thing I’ll tell you about them is that if our data mapper object implements IDisposable, after the ObjectDisposing event, the Dispose method will be called (if we don’t set the argument’s Cancel property to true).

Each CRUD method has two related events, one that fires before the call to the CRUD operation, and the other after the CRUD method has been called. All events firing before the operation have an argument where you can set the Cancel property to true in order to stop processing. The Select operation is a special case as it can fire up to eight events (three of them are creation/disposal events). First the Selecting event is fired, and if the user doesn’t cancel the Select operation, then the control calls our Select method and fires the Selected event. After that, if paging is enabled and a SelectCountMethod is defined, the controls fires the Selecting event again, then calls our SelectCountMethod and then fires the Selected event (if we don’t set Cancel to true in the Selecting event). If we have filtering enabled, the Filtering event will be fired before the filtering takes place. If we’re handling the Selecting or Selected events, we can use the ExecutingSelectCount property from the ObjectDataSourceSelectingEventArgs class to know when we’re being called.

For each “before” event, there’s a property called InputParameters that contains a dictionary with the parameters that will be passed to the CRUD method. We can change any value there. However, we can add or remove items only if we're passing simple type parameters to our data mapper.

In the “after” event, we can get the return value for the called method using the ReturnValue, and there’s also a property called OutputParameters that is a dictionary containing all the parameters from our data mapper's method that were marked as out or ref.

AffectedRows

If you have been trying what I explained above, you may have noticed a bug in it. If we have a Select method with paging enabled, and we delete all the rows in the current page, the GridView disappears instead of going to the previous page as you’d probably expect. Why? Well, when we delete a row in the GridView, it calls the DataSourceView’s Delete method, and after the deletion has been performed, a callback is called to notify the GridView that the Delete operation was completed. The callback’s type is:

The first parameter, affectedRecords, plays a key role here. If the Delete operation has affected one or more records, then the GridView will check if the current page has any row displayed, and sets the page to one that has rows, before asking for fresh data. By default, ObjectDataSource sets AffectedRows to -1, so, if we don’t explicitly set the affected rows we don’t get the results we’re expecting.

To properly set the affected rows, we can handle the Deleted event and set the event’s AffectedRow property. If we’re using ADO.NET in our data access layer, we can make our Insert, Update, and Delete methods to return the number of affected rows because that’s what ExecuteNonQuery will return. As we can access our data access method return value from the Deleted event, an easy way to handle the Deleted event is:

Having to handle this event every time you use ObjectDataSource is sad. It would be cool if ObjectDataSource had a property to automatically set the AffectedRows to ReturnValue, but this is not the case.

Optimistic Concurrency

If two different users are editing the same row and one of them updates it, when the second one updates the row, the first update is lost. There are a lot of ways to detect this, but I’m going to explain only the one that ObjectDataSource has support for. By default, the ObjectDataSource doesn’t help us to detect concurrency conflicts, but if we set the property ConflictDetection to CompareAllValues, our Update and Delete method will be passed the old values too, so we can be more specific in the UPDATE and DELETE statements to avoid updating or deleting a row if the values stored in the table are not the same that we read. For example, instead of doing this:

UPDATE Products SET name=@name, description=@description, price=@price
WHERE id=@old_id AND name=@old_name
AND description=@old_description
AND price=@old_price

If we have enabled conflict detection, the signatures for our Update and Delete methods change because we have passed more parameters. In order for the ObjectDataSource control to be able to call our Update and Delete methods using Optimistic Concurrency, we should set the OldValuesParameterFormatString, because the control uses that to recognize the old parameters. OldValuesParameterFormatString should have a placeholder that refers to the parameter name with the new data. In the example, the format expression I use is: old_{0}, so for the parameter called name, the old parameter will be called old_name.

If we’re using a custom object for passing parameters to our data mapper’s methods, the Update method now expects another parameter, from the same type as our custom objects containing the old values. The parameter names for this method are important but the order does not matter as long as one of them is named according to the OldValuesParameterFormatString with respect to the other. The following signatures are equivalent:

Parameter Merging

To understand better how ObjectDataSource works, I’m going to explain the merging strategy for the parameters in the Insert operation. If you want a complete description of all merging strategies, take a look at the references below.

Some things to consider:

We can add, remove, or modify parameters in the event fired before the method is called (adding and removing capabilities are available only if we’re using simple types instead of custom objects).

When two dictionaries are merged, one acts as the source and the other as the destination. The source entries will be copied to the destination, but if there is a parameter in the associated ParameterCollection for the operation we’re performing with the same name as the current entry in the source dictionary, the value copied to the destination is the result of evaluating the current value of that parameter.

When I’m saying that dictionary A merges with B, I’m stating that A is the source and B is the destination.

The Insert method is passed a values dictionary with the data to insert.

The merging strategy depends on the method used for passing parameters:

Using simple types: A dictionary is created with the parameters in the InsertParameters collection, and then the values dictionary is merged with that dictionary.

Using custom objects: The values dictionary is merged with a new dictionary. The resulting dictionary is used to populate an object that will be the only parameter for the Insert method.

Caching

ObjectDataSource can cache the value returned from the SelectMethod if we set the property EnableCaching to "true". To control how long the data is cached, there are some properties: CacheDuration, CacheExpirationPolicy, and CacheKeyDependency.

The first one is the number of seconds in which the control caches the data (0 means as much as possible), but the exact caching behaviour is controlled by the CacheExpirationPolicy property (that can be set to Absolute or Sliding expiration). If CacheExpirationPolicy is set to Absolute, the first time the SelectMethod is called, the data is cached and stored in the cache as long as CacheDuration seconds. If Sliding expiration is used, the data will be stored in the cache as long as CacheDuration seconds if there’s no access to the data, but if the data is accessed before being removed from cache, the expiration time is reset (so now, it can be cached as long as CacheDuration seconds again).

Sometimes, we may need to invalidate the cached data for the ObjectDataSource. To do so, we can use the CacheKeyDependency property. When the control caches the data returned from the SelectMethod, it will check the value of CacheKeyDependency, and if it’s set, the cached data will have a dependency on that cache key. When we want to remove the cached data, we can use the Cache.Remove method using the key specified in the CacheKeyDependency.

If we have caching enabled, when the Select method is executed, the cache is accessed before firing the Selecting event, and if the data we're searching is in the cache, the Select method returns the cached data. So if we're doing any preprocessing of the input parameters in the Selecting event, caching doesn't work because the cache key created depends only on the select parameters (and its values) and paging values (as caching works with paging). Also keep in mind that caching doesn't work if we have a sort parameter.

All the cached entries have a dependency on a cache key that's based on the select method name and the select parameters (and its values), and when a control does an insert, update, or delete operation, this dependency is removed from the cache, removing its dependent entries.

As you can see, this caching schema has some limitations but I think it has been designed this way to avoid filling the cache with lots of old data and to be able to remove modified data easily. Unfortunately, we can't extend how the control caches data.

Design Time Attributes

If we select the “Configure Data Source” option in the ObjectDataSource’s smart tag, we can see a wizard to configure the data mapper class and CRUD methods for your ObjectDataSource without “getting dirty” in code.

If our project is big, we will have a lot of classes in the dropdownlist where we will have to choose our data mapper. We can apply the DataObjectAttribute at the class level for our data mappers, so if we check the option “Show only data components”, we’ll only see classes marked with that attribute.

For methods, there’s a similar thing. There’s an attribute called DataObjectMethodAttribute that takes an attribute to specify the method type. When we’re in the wizard and a class has at least one method that has that attribute, we’ll only see a list of methods with that attribute. If no methods have the attribute for the method type we’re configuring, all methods that appear to be applicable are shown.

You may have noticed that the wizard has tabs for Select, Update, Insert, and Delete methods, but not for select count. Also, the DataObjectMethodAttribute doesn’t support the SelectCount method type, so I suppose it doesn’t make sense for them either to have two distinct select methods when one would be enough.

The documentation states that, thanks to the DataObjectMethodAttribute applied to the CRUD methods, they’re more easily identified, but that’s not true. This attribute is useful to solve possible ambiguities when searching for a method. What ObjectDataSource does when looking for a compatible method is to check the number of parameters and if they match the expected number and then compare the parameter names. If they match, the method is saved. If more than one method can be called for a CRUD method, an error is thrown. However, if we need to have several methods with the same number of parameters and the same parameter names (the order and type doesn’t matter), we can use the DataObjectMethodAttribute to specify different method types for them to avoid conflicts.

Source Code

In the source code, there is a webform for each possible strategy:

using simple types without optimistic concurrency

using simple types with optimistic concurrency

using custom objects without optimistic concurrency

using custom objects with optimistic concurrency

As the GridView doesn’t call the Insert method, I added the possibility to insert new products. I used a DetailsView for the custom object samples, and some textboxes for the simple types.

The simple types examples are interesting because the insertion uses the InsertParameters collection with an output parameter, and the insertion is done by hand, calling the Insert method from the ObjectDataSource. This shows how you can call ObjectDataSource CRUD methods directly instead of using a control that understands the new data binding model.

The last thing to note is that I have set to "false" the EnableSortingAndPagingCallbacks for the examples where optimistic concurrency is enabled because there’s a bug in the GridView. You can reproduce the bug this way:

Go to the last page of the grid.

Insert elements until the grid gets a new page.

Go to the newly created page and click Delete.

You’ll see that delete doesn’t work for the first time because the GridView is sending the wrong oldValues (from the previous page, not from the current one) to the DeleteMethod.

If you click Delete again, it will work because the control state is correct after the postback caused by the first delete, but is wrong before (the client callbacks don’t restore the state as expected).

If you set EnableSortingAndPagingCallbacks to false, the first time you click Delete, it works as expected.

Limitations, Problems, and Possible Solutions

I'll summarize here the most important problems/limitations of the ObjectDataSource:

Having to write a SelectCount method when your data source does support paging.

All calls to the methods specified by the XXXMethod properties are made using reflection but the reflection data isn't cached, so for medium/big applications, this could be a bottleneck.

It can't be extended without having to rewrite almost all of it.

The control can't be used in some cultures because of the conversions that it makes using the invariant culture instead of the current culture.

The AffectedRows property is very important in order to get the GridView working properly, so it's a pain to have to handle the inserted, updated and deleted events every time you use the ObjectDataSource.

Filtering support is minimal, and reserved only if you return a DataSet, DataTable, or DataView.

The cache supports neither filtering nor paging.

If you're using custom objects, you can change the parameter dictionary for the Insert, Update, or Delete methods.

The only real solution here is to code your own data source control, but it isn't an easy task. If you need a better ObjectDataSource control, you can check my ExtendedObjectDataSource control that overcomes these limitations: The ExtendedObjectDataSource control.

History

03/22/2006 - Initial version.

References

This article is a copy paste of some posts from my blog about the ObjectDataSource. A few parts are more detailed in this posts:

developing data access layer (DAL) classes is not an issue. The problem rises when you use RectangleDAL classes with a ObjectDataSource object, caused by TopLeft. the prperty is shown easily in a FormView or a GridView, but editing will result in an Excpetion saying "cannot convert string to Point for TopLeft"

The question is: Is there a solution to add such feature to a business object to be converted from string?

You need to implement a type converter for your Point class if you want to be able to edit it using the ObjectDataSource. Think about it as the inverse of the ToString operation, although a TypeConverter can be used to convert from and to many types, not only string.