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.