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):
public List LoadAll()
{
List products = new List();
SqlConnection conn = new SqlConnection(
ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString);
SqlCommand command = new SqlCommand("SELECT * FROM Products", conn);
conn.Open();
SqlDataReader dr = command.ExecuteReader();
while (dr.Read()) {
Product prod = new Product();
prod.Id = (int)dr["id"];
prod.Name = (string)dr["name"];
prod.Description = (string)dr["description"];
prod.Price = (decimal)dr["price"];
products.Add(prod);
}
dr.Close();
conn.Close();
return products;
}
The Select method can return any of the following types:
- A
DataView
- A
DataSet
- A
DataTable
- An
IEnumerable
object
- Another object
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:
public static void Edit(Product prod)
{
SqlConnection conn = new SqlConnection(
ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString);
SqlCommand command = new SqlCommand("UPDATE Products " +
"SET name=@name, description=@description," +
" price=@price WHERE id=@id", conn);
command.Parameters.Add(new SqlParameter("@name", prod.Name));
command.Parameters.Add(new SqlParameter("@description", prod.Description));
command.Parameters.Add(new SqlParameter("@price", prod.Price));
command.Parameters.Add(new SqlParameter("@id", prod.Id));
conn.Open();
command.ExecuteNonQuery();
conn.Close();
}
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:
System.Threading.Thread.CurrentThread.CurrentCulture =
System.Globalization.CultureInfo.InvariantCulture;
This is a hack, but will let us continue using the example. Later, I will explain how you can fix this.
To delete a product, we need to add another method to the ProductsDAL
class:
public static void Delete(Product prod)
{
SqlConnection conn = new SqlConnection(
ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString);
SqlCommand command = new SqlCommand("DELETE Products WHERE id=@id", conn);
command.Parameters.Add(new SqlParameter("@id", prod.Id));
conn.Open();
command.ExecuteNonQuery();
conn.Close();
}
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:
public static void Edit(int old_id, string name,
string description, decimal price);
public static void Delete(int old_id);
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):
{ “name”, “Product 1”}
{ “description”, “Description 1” }
{ “price”, “1.00” }
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.
The Select methods look like this:
public List LoadAll(int startIndex, int maxRows)
{
List products = new List<PRODUCT>();
SqlConnection conn = new SqlConnection(
ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString);
string commandText = @"
-- create a temp table for paging
CREATE TABLE #PageIndexForTable (
IndexId int IDENTITY (0, 1) NOT NULL,
id int
)
-- insert into our temp table
INSERT INTO #PageIndexForTable ( [id] ) SELECT [id] FROM Products;
SET @totalRecords = @@ROWCOUNT
-- get the requested registers
SELECT src.[id], src.[name], src.[description],
src.[price] FROM Products src, #PageIndexForTable p
WHERE src.[id] = p.[id] AND p.IndexId >= @StartIndex
AND p.IndexId < (@startIndex + @maxRows)";
SqlCommand command = new SqlCommand(commandText, conn);
command.Parameters.Add(new SqlParameter("@startIndex", startIndex));
command.Parameters.Add(new SqlParameter("@maxRows", maxRows));
command.Parameters.Add(new SqlParameter("@totalRecords", SqlDbType.Int));
command.Parameters["@totalRecords"].Direction =
ParameterDirection.Output;
conn.Open();
SqlDataReader dr = command.ExecuteReader();
while (dr.Read()) {
Product prod = new Product();
prod.Id = (int)dr["id"];
prod.Name = (string)dr["name"];
prod.Description = (string)dr["description"];
prod.Price = (decimal)dr["price"];
products.Add(prod);
}
dr.Close();
conn.Close();
_count = (int)command.Parameters["@totalRecords"].Value;
return products;
}
public int CountAll()
{
return _count;
}
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:
public delegate bool
DataSourceViewOperationCallback(int affectedRecords, Exception ex);
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:
protected void DataSourceDeleted(object sender,
ObjectDataSourceStatusEventArgs e)
{
e.AffectedRows = (int)e.ReturnValue;
}
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
we can do 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:
public static void Edit(Product prod, Product old_prod);
public static void Edit(Product old_prod, Product prod);
For the Delete method, the signature doesn’t change, but the object that’s passed to it now has all bound fields filled with the old data (remember that, previously, it only had the primary key set).
If we’re using simple types, the signatures are:
public static void Edit(string name, string description,
decimal price, int old_id, string old_name,
string old_description, decimal old_price);
public static void Delete(int old_id, string old_name,
string old_description, decimal old_price);
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:
If you want to know more about data source controls and the new data binding infrastructure, take a look here: