Wednesday, February 27, 2008

ObjectContainerDataSource, FormViews and LLBLGen Entities

I've always run into issues with databinding in ASP.NET 2. It is nice and straightfoward with basic examples, but there always seems to be some scenario that causes an event to fire at an unexpected time, stuffing things up.

I am currently using the Web Client Software Factory (WCSF) by Microsoft patterns & practices and one of the little gems in this is the ObjectContainerDataSource. This datasource implements data binding in a way that easily integrates with the MVP pattern. That is, the Select/Update/Delete events can be passed on and handled by the Presenter. If you are doing web development and are not aware of the WCSF, it is worth checking out.

This all seems fine, until I wanted to bind to an LLBLGen entity (LLBLGen is an easy-to-use and inexpensive O/R mapper). The LLBLGen entity had a Timestamp field on it that was used for optimistic concurrency - it was read-only and the optimistic concurrency checking would ensure that the Timestamp field had not changed on the database since the entity was fetched.

On an Update operation I found the ObjectContainerDataSource was creating a new instance of my entity and updating only the properties that were databound in the FormView on my page. No primary keys, no foreign keys, no timestamp fields; by the time I was trying to handle the Update event in the Presenter, I had an instance that supposedly new, with no relationships. Argh!

I found some related entries in the WCSF Discussions, but no one had a good solution that suited me and my LLBLGen entity.

One of the good things about the WCSF is that you get code, and the ObjectContainerDataSource is provided in its own project. I've now made some modifications to it that works the way I believe it should. Basically, instead of creating a new instance of the type you are binding to and updating the bound properties on that, the current instance (that is cached on the ObjectContainerDataSource after the Selecting event is called) is used and its properties are updated with the new databound values. This gives you the state of the entity as it was when you fetched it with the exception of the changes made through databinding. This is the behaviour I would have expected initially.

Note: to get access to the WCSF source, you'll need to install the source separately after you have installed the WCSF. Default folder is:

C:\Program Files\Microsoft Web Client Factory\Source Code\

So, the new ExecuteUpdate method in ObjectContainerDataSourceView.cs looks like this:


protected override int ExecuteUpdate(IDictionary keys, IDictionary values, IDictionary oldValues)
{
Guard.CollectionNotNullNorEmpty(keys, String.Format(CultureInfo.CurrentCulture,
Properties.Resources.NoKeysSpecified), "keys");
Guard.ArgumentNotNull(values, "values");

ObjectContainerDataSourceUpdatingEventArgs updatingEventArgs =
new ObjectContainerDataSourceUpdatingEventArgs(DictionaryHelper.GetReadOnlyDictionary(keys),
values, oldValues);
OnUpdating(updatingEventArgs);
if (updatingEventArgs.Cancel)
return 0;
//
// -------------------------------------------------------------
// This section has been modified to keep the old instance and
// update the appropriate fields on it that changed through
// databinding. If no old instance found, a new instance is
// created.
//
int rowsAffected;
object newInstance;
object oldInstance = FindInstance(keys);
if (oldInstance != null)
{
TypeDescriptionHelper.BuildInstance(values, oldInstance);
rowsAffected = 1;
newInstance = oldInstance;
}
else
{
newInstance = CreateInstance();
TypeDescriptionHelper.BuildInstance(keys, newInstance);
TypeDescriptionHelper.BuildInstance(values, newInstance);
rowsAffected = 0;
}
//
// -------------------------------------------------------------
//
OnDataSourceViewChanged(EventArgs.Empty);

ObjectContainerDataSourceStatusEventArgs updatedEventArgs =
new ObjectContainerDataSourceStatusEventArgs(newInstance, rowsAffected);
OnUpdated(updatedEventArgs);

return rowsAffected;
}

The Delete operation is affected in a similar manner, so it has also been updated:

protected override int ExecuteDelete(IDictionary keys, IDictionary oldValues)
{
Guard.CollectionNotNullNorEmpty(keys, String.Format(CultureInfo.CurrentCulture,
Properties.Resources.NoKeysSpecified), "keys");

ObjectContainerDataSourceDeletingEventArgs deletingEventArgs =
new ObjectContainerDataSourceDeletingEventArgs(DictionaryHelper.GetReadOnlyDictionary(keys),
oldValues);
OnDeleting(deletingEventArgs);
if (deletingEventArgs.Cancel)
return 0;

int rowsAffected;
object instance = FindInstance(keys);
if (instance == null)
{
rowsAffected = 0;
//
// -------------------------------------------------------------
// This section has been modified to only create a new instance
// if an old one cannot be found. Moved up from below this if()
// statement.
//
instance = CreateInstance();
//
// -------------------------------------------------------------
//
}
else
{
Data.Remove(instance);
rowsAffected = 1;
}
TypeDescriptionHelper.BuildInstance(oldValues, instance);
TypeDescriptionHelper.BuildInstance(keys, instance);
OnDataSourceViewChanged(EventArgs.Empty);

ObjectContainerDataSourceStatusEventArgs deletedEventArgs =
new ObjectContainerDataSourceStatusEventArgs(instance, rowsAffected);
OnDeleted(deletedEventArgs);

return rowsAffected;
}

These changes should work with any class, not just LLBLGen entities. For me, retaining the Timestamp value was the most important (for optimistic concurrency as I mentioned earlier), and there is no way to set that externally. I am going to go out on a limb here and say that this probably applies to most O/R mappers.

I'd be glad to here from anyone who has had similar issues or has found a better solution.

10 comments:

Anonymous said...

The ability to store the Timestamp value is taken care of in the control you bind to. For example if you bind to a datagrid then you store the concurrency value in the DataKeyNames property (e.g. DataKeyNames="ID,Concurrency") of the datagrid.

You can then retrieve the Concurrency value on either the updating or updated event of the ObjectContainerDataSource.

__Allan

Adrian Brown said...

Yes, but you still can't push the Timestamp value into the new entity instance as it is read-only.

gideon said...

Good work. ObjectDataSourceContainer is a gem. When I came across a similar problem I used the DataKeyNames approach, but I did not have a read only property to contend with. But thanks to your example, I now realise that you can look at the source of the WCSF.

With my usage of the ObjectContainerDataSource, I have moved past the 'non-trivial' and am left scratching my head in some situations. The example I want your opionion on is this:

Edit/Insert/Delete works all wonderfully, but what if you want to add a different type of action to the Data. In my example, I have a DetailsView where I have Edit/Insert/Delete all working, but now I want to support for a few other actions. Say, Approve Record. What I would really love is to be able to get at the Instance of the data bound object. I.e. for the objectContainerDataSource_Updated event, you get a nice e.Instance attribute to access.

I could just reconstruct the Object myself (like I have in the past) but there must be a nicer way, seeing as the ObjectContainerDataSource already does it though the ObjectContainerDataSourceView .CreateInstance()

Adrian Brown said...

Hi Gideon,

Well, in the ObjectContainerDataSource's implementation of DataSourceView there is a protected property called Data that contains a list of the objects managed by the view. In other words, the instances you are binding to. The standard actions (Select/Insert/Update/Delete) access this and find the instance based on key values. You could potentitally add functionality for other actions. However, an action such as Approve Record sounds very much like a business process, and I'm not sure I'd want to pollute the DataSource with this kind of thing.

I'd be inclined to keep it external to the datasource. You should have the instance anyway as you will have provided it to the Selecting event on postback.

gideon said...

Hi Adrian,

Thanks for getting back to me and thanks for the advice.

Yes, i thought about persisting the instance(s) that I got from the select method (using the ViewState for example), but I am wary of doing this - it should be the OCDS who does this sort of thing.

I agree that the Approve action is a domain specific action - it was not my intention to ask you (or anyone else) to add this specific method to OCDS.

I have also posed a similar question on codeplex (Codeplex thread). On there I describe the approach that I am taking: In summary, I am overloading the delete event to handle any other sort of events as well as delete (overloading is not accurate, bastardising is more accurate).

Anonymous said...

Adrian - marvellous work with these amendments. The "Update" version is working perfectly for me.

However I have an issue with your version of ExecuteDelete(), at line:

TypeDescriptionHelper.BuildInstance(keys, instance);

I am getting the exception:

The property 'ParentID' is part of the object's key information and cannot be modified.

"ParentID" is named as my object's key. It seems that ExecuteDelete() is trying to insert a value for the object key, which the framework does not like...

Adrian Brown said...

Hi Andrew,

I haven't looked at this for a while, but I am thinking that the line:

TypeDescriptionHelper.BuildInstance(keys, instance);

can be moved up to the line after the instance is created. i.e.:

instance = CreateInstance();
TypeDescriptionHelper.BuildInstance(keys, instance);

If the instance is not being constructed, the keys shouldn't be updated anyway.

Hope this helps.

Cheers,
Adrian.

Anonymous said...

Adrian, thanks for sharing this with the community.

In the following thread I share some insights about the behavior discussed in your post:

http://www.codeplex.com/websf/Thread/View.aspx?ThreadId=32461

This may clarify why we did the ObjectContainerDataSource the way it is.

Cheers,
Mariano Szklanny

Milan Jaric said...

Good work. Just post I needed. I have issue with abstract classes and inheritance.

TNX

Anonymous said...

Good fill someone in on and this mail helped me alot in my college assignement. Thank you for your information.

Powered By Blogger