Blog Home  Home Feed your aggregator (RSS 2.0)  
ObjectDataSource in Depth (Part 3) - Manuel Abadia's ASP.NET stuff
 
# Tuesday, February 28, 2006

If you have tried last part example, 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 set the page to one that has rows, before asking for fresh data. By default ObjectDataSource set AffectedRows to -1, so, if we don’t explicitly set the affected rows we don’t get the results you’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 your 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 to 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 the 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 those 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’re passed more parameters. In order to the ObjectDataSourceControl 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 for 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 accordingly 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 in Detail

In Part 2 I introduced the parameter collections for the different CRUD methods and the events that were fired by the control.

For completion I’m going to explain all merging strategies for the parameters. 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 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)

Here’s the list of methods and their merging strategy:

• SelectMethod: A dictionary is created with the parameters in the SelectParameters collection. If we have set the property SortParameterName, a new entry is added to the dictionary. If paging is enabled two more entries are added StartIndexParameterName and MaximumRowsParameterName.

• SelectCountMethod: A dictionary with the parameters in the SelectParameters collection.

• InsertMethod: The insert method is passed a values dictionary with the data to insert. The merging strategy depends on the method used for passing parameters:

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

o 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.

• UpdateMethod: The update method receives 3 dictionaries:

o keys: with the primary key values
o values: with the data to update
o oldValues: with the previous data

The merging strategy depends on the method used for passing parameters and whether optimistic concurrency is enabled:

o Using simple types without optimistic concurrency: A dictionary is created with the parameters in the UpdateParameters collection and then the values dictionary is merged with that dictionary. After that the keys dictionary is merged with the current one, but formatting the keys with the OldValuesParameterFormatString.

o Using simple types with optimistic concurrency: A dictionary is created with the parameters in the UpdateParameters collection and then the values dictionary is merged with that dictionary. After that the oldValues dictionary is merged with the current one, but formatting the oldValues with the OldValuesParameterFormatString. The last merging is between the keys and the current dictionary also applying the OldValuesParameterFormatString to the keys.

o Using custom objects without optimistic concurrency: The oldValues dictionary is merged with a new dictionary. The keys dictionary is merged with the current one. Finally the values dictionary is merged with the current dictionary. The resulting dictionary is used to populate an object that will be the only parameter for the update method.

o Using custom objects with optimistic concurrency: The oldValues dictionary is merged with a new dictionary. The keys dictionary is merged with the current one. The resulting dictionary is used to populate an object that will hold the old data for the update. The values dictionary is merged with the previous dictionary and then used to populate another object that will hold the current data for the update. Both objects will be the parameters for the update method.

• DeleteMethod: The delete method receives 2 dictionaries:

o keys: with the primary key values
o oldValues: with the previous data

The merging strategy depends on the method used for passing parameters and whether optimistic concurrency is enabled:

o Using simple types without optimistic concurrency: A dictionary is created with the parameters in the DeleteParameters collection and then the keys dictionary is merged with that dictionary but formatting the keys with the OldValuesParameterFormatString.

o Using simple types with optimistic concurrency: A dictionary is created with the parameters in the DeleteParameters collection and then the keys dictionary is merged with that dictionary but formatting the keys with the OldValuesParameterFormatString. After that the oldValues dictionary is merged with the current one, again formatting the oldValues with the OldValuesParameterFormatString.

o Using custom objects without optimistic concurrency: The keys dictionary is merged with a new dictionary and the resulting dictionary is used to populate an object that will be the only parameter for the delete method.

o Using custom objects with optimistic concurrency: The keys dictionary is merged with a new dictionary. The oldValues dictionary is merged with the current one, and the resulting dictionary is used to populate an object that will be the only parameter for the delete 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 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 reseted (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 with 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 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 your 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 objects 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 understand 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 was 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.

That concludes this 3 part tutorial. I hope it has been helpful. In a future post I'll summarize the limitations of the ObjectDataSource and how to overcome them all.

Update:
You can access to the other parts here:

ObjectDataSource in Depth (Part 1)
ObjectDataSource in Depth (Part 2)
ObjectDataSource Limitations, Problems and Possible Solutions

Example source code: ObjectDataSourcePart3.zip (178.47 KB)
Copyright © 2014 Manuel Abadia. All rights reserved.
DasBlog 'Portal' theme by Johnny Hughes.