In part one of this little series, I introduced the first player in our little play, AcquringObjectDataSource
. Quite literally, the only reason that class was necessary was to insure that we create a slightly smarter ObjectDataSourceView
. This part of the series is all about adding the "acquire" behavior. What I'm after is a simple way of getting the choice as to whether the DataObjectType
instance is created ex-nilo via Activator.CreateInstance
(the standard behavior of ObjectDataSourceView
, or created by a factory class, or recalled from some cache or datastore. In my case, I want to reload the object instances from the database via Wilson's O/R Mapper, after first having checked the ASP.Net Session
to see if we've got partial changes to carry through.
The View does the work
The first thing that becomes painfully obvious when building on ODS is that the ODSV object does the heavy lifting. Almost everything in ODS that isn't related to the creation of the view, or the maintenance of the cache is merely a call-through proxy to the ODSV. The main operations are obviously the CRUD operations. These operations are carried out but the ExecuteInsert
, ExecuteSelect
, ExecuteUpdate
and ExecuteDelete
, respectively. The read method (ExecuteSelect
) already acts as expected, so we'll ignore that method. The others mutating methods are handed an IDictionary
of keys, old values and new values (each method gets some of these, as appropriate to the use). The dictionaries are used to fill in values for a new object (in case of insert), an old object (in case of delete), or both (in case of update). This is, as stated, where my pain begins. I want to insure that we hit whatever the original data source is if needed, not create the objects out of the ether. In each of the mutating methods it builds up a list of parameter values by merging in any hard-coded parameters values, the keys IDictionary
and any values IDictionary
(s). This is done by a private
method of ODSV called BuildDataObject
. This simple method looks like this in Reflector (I've highlighted the line that grieves my soul):
private object BuildDataObject(Type dataObjectType, IDictionary inputParameters)
{
object obj1 = Activator.CreateInstance(dataObjectType);
PropertyDescriptorCollection collection1 = TypeDescriptor.GetProperties(obj1);
foreach (DictionaryEntry entry1 in inputParameters)
{
string text1 = (entry1.Key == null) ? string.Empty : entry1.Key.ToString();
PropertyDescriptor descriptor1 = collection1.Find(text1, true);
if (descriptor1 == null)
{
throw new InvalidOperationException(SR.GetString("ObjectDataSourceView_DataObjectPropertyNotFound", new object[] { text1, this._owner.ID }));
}
if (descriptor1.IsReadOnly)
{
throw new InvalidOperationException(SR.GetString("ObjectDataSourceView_DataObjectPropertyReadOnly", new object[] { text1, this._owner.ID }));
}
object obj2 = ObjectDataSourceView.BuildObjectValue(entry1.Value, descriptor1.PropertyType, text1);
descriptor1.SetValue(obj1, obj2);
}
return obj1;
}
If it weren't for that simple little
Activator.CreateInstance
, my life would have been easy... but, alas, I must toil against the oppressors. I could easily replace that method, but for several things. First, it is not virtual, so my version would not get called. Second, it is private, so I can't very well call down to reuse the behavior. Third, it uses
ObjectDataSourceView.BuildObjectValue
, which is
of course private
.
Virtual is where the action is
What I need to do is make sure that I have an integration-point where I can embrace and extend the Microsoft implementation. I want to make sure that some event or virtual method is called to create / rehydrate / lookup the actual data object. How about something blindingly obvious like:
public virtual object AcquireDataObject(Type dataObjectType, IDictionary inputParameters)
{
return Activator.CreateInstance(dataObjectType);
}
This has the advantage of being hookable, while still letting the original naive implementation to be used.
[Note: If Microsoft had simply done this and ONLY this to the original ODSV class, I wouldn't have needed any of this, not that I'm bitter]. We pass the
IDictionary
of input parameters to the creation method to give it a place to extract the key values, if needed.
Of course, it can't be that easy for me. Rather, I get bit by the fact that since BuildDataObject is non-virtual
, private
and pretty much incestuously coupled with everything else in ODSV, I'm going to have to do much more work. Specifically, I need to insure that my version of BuildDataObject
gets called, so that I can replace that first line with a call to the new virtual AcquireDataObject
method. This means that I need to override the mutating methods ExecuteInsert
, ExecuteUpdate
and ExecuteDelete
. Since I only care about the case where a DataObjectType
has been specified, I can down-call if that value isn't set, and have a vastly simplified body for each of these that only handles the DOT case. Of course, to simply reimplement those three methods I have to call a boat-load of other methods in ODSV, which are (you guessed it) private
. Namely I need access to the Owner
field, the GetType
method, the TryGetDataObjectType
method, the MergeDictionaries
method, the GetResolvedMethodData
method, the previously mentioned BuildObjectValue
method, the InvokeMethod
method, and ickly-enough the private ObjectDataSourceMethod
nested class; whose Parameters
field we also need to fiddle with. Lastly, we need to be able to extract the AffectedRows
of the private ObjectDataSourceResult
nested class. That's a lot of tendrils stuck in to ODSV, but I for the want of a single virtual
, that is the path I must tread.
Where we are now
So, without any further delay, may I introduce to you the fair and fleeting AcquiringObjectDataSourceView
, whose merest existence I groan at:
public class AcquiringObjectDataSourceView : ObjectDataSourceView
{
const BindingFlags NeedlesslyPrivate = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
const BindingFlags NeedlesslyPrivateStatic = BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly;
static readonly FieldInfo s_Owner;
static readonly MethodInfo s_GetType;
static readonly MethodInfo s_TryGetDataObjectType;
static readonly MethodInfo s_MergeDictionaries;
static readonly MethodInfo s_GetResolvedMethodData;
static readonly MethodInfo s_InvokeMethod;
static readonly MethodInfo s_BuildObjectValue;
static readonly FieldInfo s_AffectedRows;
static readonly FieldInfo s_Parameters;
static AcquiringObjectDataSourceView()
{
s_Owner = typeof(ObjectDataSourceView).GetField("_owner", NeedlesslyPrivate);
Type[] getType = new Type[] { typeof(string) };
s_GetType = typeof(ObjectDataSourceView).GetMethod("GetType", NeedlesslyPrivate, null, getType, null);
Type[] tryGetDataObjectType = new Type[] { };
s_TryGetDataObjectType = typeof(ObjectDataSourceView).GetMethod("TryGetDataObjectType", NeedlesslyPrivate, null, tryGetDataObjectType, null);
Type[] mergeDictionaries = new Type[] { typeof(ParameterCollection), typeof(IDictionary), typeof(IDictionary) };
s_MergeDictionaries = typeof(ObjectDataSourceView).GetMethod("MergeDictionaries", NeedlesslyPrivateStatic, null, mergeDictionaries, null);
Type[] getResolvedMethodData = new Type[] { typeof(Type), typeof(string), typeof(Type), typeof(object), typeof(object), typeof(DataSourceOperation) };
s_GetResolvedMethodData = typeof(ObjectDataSourceView).GetMethod("GetResolvedMethodData", NeedlesslyPrivate, null, getResolvedMethodData, null);
Type[] buildObjectValue = new Type[] { typeof(object), typeof(Type), typeof(string)};
s_BuildObjectValue = typeof(ObjectDataSourceView).GetMethod("BuildObjectValue", NeedlesslyPrivateStatic, null, buildObjectValue, null);
Type objectDataSourceMethod = typeof(ObjectDataSourceView).GetNestedType("ObjectDataSourceMethod", BindingFlags.NonPublic);
s_Parameters = objectDataSourceMethod.GetField("Parameters", NeedlesslyPrivate);
Type[] invokeMethod = new Type[] { objectDataSourceMethod };
s_InvokeMethod = typeof(ObjectDataSourceView).GetMethod("InvokeMethod", NeedlesslyPrivate, null, invokeMethod, null);
Type objectDataSourceResult = typeof(ObjectDataSourceView).GetNestedType("ObjectDataSourceResult", BindingFlags.NonPublic);
s_AffectedRows = objectDataSourceResult.GetField("AffectedRows", NeedlesslyPrivate);
}
public AcquiringObjectDataSourceView(ObjectDataSource owner, string name, HttpContext context)
: base(owner, name, context)
{
}
protected override int ExecuteDelete(IDictionary keys, IDictionary oldValues)
{
if (!this.CanDelete)
{
throw new NotSupportedException(ExposedSR.GetString(ExposedSR.DeleteNotSupported, this.Owner.ID));
}
// we only change the behavior of sources that provide a DataObjectTypeName
if (String.IsNullOrEmpty(this.DataObjectTypeName))
return base.ExecuteDelete(keys, oldValues);
Type sourceType = this.GetType(this.TypeName);
Type dataObjectType = this.TryGetDataObjectType();
IDictionary deleteParameters = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
if (this.ConflictDetection == ConflictOptions.CompareAllValues)
{
if (oldValues == null || oldValues.Count == 0)
{
throw new InvalidOperationException(ExposedSR.GetString(ExposedSR.Pessimistic, ExposedSR.GetString(ExposedSR.Delete), this.Owner.ID, "oldValues"));
}
MergeDictionaries(this.DeleteParameters, oldValues, deleteParameters);
}
MergeDictionaries(this.DeleteParameters, keys, deleteParameters);
object dataObject = this.BuildDataObject(dataObjectType, deleteParameters);
object deleteMethod = this.GetResolvedMethodData(sourceType, this.DeleteMethod, dataObjectType, dataObject, null, DataSourceOperation.Delete);
IOrderedDictionary parameters = ExtractMethodParameters(deleteMethod);
ObjectDataSourceMethodEventArgs args = new ObjectDataSourceMethodEventArgs(parameters);
this.OnDeleting(args);
if (args.Cancel)
{
return 0;
}
object result = this.InvokeMethod(deleteMethod);
this.Owner.InvalidateCache();
this.OnDataSourceViewChanged(EventArgs.Empty);
return ExtractAffectedRows(result);
}
protected override int ExecuteInsert(IDictionary values)
{
if (!this.CanInsert)
{
throw new NotSupportedException(ExposedSR.GetString(ExposedSR.InsertNotSupported, this.Owner.ID));
}
// we only change the behavior of sources that provide a DataObjectTypeName
if (String.IsNullOrEmpty(this.DataObjectTypeName))
return base.ExecuteInsert(values);
Type sourceType = this.GetType(this.TypeName);
Type dataObjectType = this.TryGetDataObjectType();
if (values == null || values.Count == 0)
{
throw new InvalidOperationException(ExposedSR.GetString(ExposedSR.InsertRequiresValues, this.Owner.ID));
}
IDictionary insertParameters = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
MergeDictionaries(this.InsertParameters, values, insertParameters);
object dataObject = this.BuildDataObject(dataObjectType, insertParameters);
object insertMethod = this.GetResolvedMethodData(sourceType, this.InsertMethod, dataObjectType, null, dataObject, DataSourceOperation.Insert);
IOrderedDictionary parameters = ExtractMethodParameters(insertMethod);
ObjectDataSourceMethodEventArgs args = new ObjectDataSourceMethodEventArgs(parameters);
this.OnInserting(args);
if (args.Cancel)
{
return 0;
}
object result = this.InvokeMethod(insertMethod);
this.Owner.InvalidateCache();
this.OnDataSourceViewChanged(EventArgs.Empty);
return ExtractAffectedRows(result);
}
protected override int ExecuteUpdate(IDictionary keys, IDictionary values, IDictionary oldValues)
{
if (!this.CanUpdate)
{
throw new NotSupportedException(ExposedSR.GetString(ExposedSR.UpdateNotSupported, this.Owner.ID));
}
// we only change the behavior of sources that provide a DataObjectTypeName
if (String.IsNullOrEmpty(this.DataObjectTypeName))
return base.ExecuteUpdate(keys, values, oldValues);
object updateMethod;
Type sourceType = this.GetType(this.TypeName);
Type dataObjectType = this.TryGetDataObjectType();
if (this.ConflictDetection == ConflictOptions.CompareAllValues)
{
if (oldValues == null || oldValues.Count == 0)
{
throw new InvalidOperationException(ExposedSR.GetString(ExposedSR.Pessimistic, ExposedSR.GetString(ExposedSR.Update), this.Owner.ID, "oldValues"));
}
IDictionary oldParameters = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
MergeDictionaries(this.UpdateParameters, oldValues, oldParameters);
MergeDictionaries(this.UpdateParameters, keys, oldParameters);
object oldDataObject = this.BuildDataObject(dataObjectType, oldParameters);
IDictionary newParameters = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
MergeDictionaries(this.UpdateParameters, oldValues, newParameters);
MergeDictionaries(this.UpdateParameters, keys, newParameters);
MergeDictionaries(this.UpdateParameters, values, newParameters);
object newDataObject = this.BuildDataObject(dataObjectType, newParameters);
// if oldDataObject and newDataObject are the same object, this is a bit odd... but since we
// built them old-then-new, the resulting object has the correct values. In general we aren't
// going to be running that way.
updateMethod = this.GetResolvedMethodData(sourceType, this.UpdateMethod, dataObjectType, oldDataObject, newDataObject, DataSourceOperation.Update);
}
else
{
IDictionary updateParameters = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
MergeDictionaries(this.UpdateParameters, oldValues, updateParameters);
MergeDictionaries(this.UpdateParameters, keys, updateParameters);
MergeDictionaries(this.UpdateParameters, values, updateParameters);
object dataObject = this.BuildDataObject(dataObjectType, updateParameters);
updateMethod = this.GetResolvedMethodData(sourceType, this.UpdateMethod, dataObjectType, null, dataObject, DataSourceOperation.Update);
}
IOrderedDictionary parameters = ExtractMethodParameters(updateMethod);
ObjectDataSourceMethodEventArgs args = new ObjectDataSourceMethodEventArgs(parameters);
this.OnUpdating(args);
if (args.Cancel)
{
return 0;
}
object result = this.InvokeMethod(updateMethod);
this.Owner.InvalidateCache();
this.OnDataSourceViewChanged(EventArgs.Empty);
return ExtractAffectedRows(result);
}
public virtual object AcquireDataObject(Type dataObjectType, IDictionary inputParameters)
{
return Activator.CreateInstance(dataObjectType);
}
// Oh, if only this was a virtual on ObjectDataSourceView!
private object BuildDataObject(Type dataObjectType, IDictionary inputParameters)
{
object dataObject = AcquireDataObject(dataObjectType, inputParameters);
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(dataObjectType);
foreach (DictionaryEntry entry in inputParameters)
{
string propertyName = (entry.Key == null) ? string.Empty : entry.Key.ToString();
PropertyDescriptor descriptor = properties.Find(propertyName, true);
if (descriptor == null)
{
throw new InvalidOperationException(ExposedSR.GetString(ExposedSR.DataObjectPropertyNotFound, new object[] { propertyName, this.Owner.ID }));
}
if (descriptor.IsReadOnly)
{
throw new InvalidOperationException(ExposedSR.GetString(ExposedSR.DataObjectPropertyReadOnly, new object[] { propertyName, this.Owner.ID }));
}
object propertyValue = BuildObjectValue(entry.Value, descriptor.PropertyType, propertyName);
descriptor.SetValue(dataObject, propertyValue);
}
return dataObject;
}
#region Proxies for private methods
private AcquiringObjectDataSource Owner
{
get { return (AcquiringObjectDataSource)s_Owner.GetValue(this); }
}
private Type GetType(string typeName)
{
return (Type)s_GetType.Invoke(this, new object[] { typeName });
}
private Type TryGetDataObjectType()
{
return (Type)s_TryGetDataObjectType.Invoke(this, null);
}
private static void MergeDictionaries(ParameterCollection reference, IDictionary source, IDictionary destination)
{
s_MergeDictionaries.Invoke(null, new object[] { reference, source, destination });
}
private static object BuildObjectValue(object value, Type destinationType, string paramName)
{
return s_BuildObjectValue.Invoke(null, new object[] { value, destinationType, paramName });
}
private object GetResolvedMethodData(Type type, string methodName, Type dataObjectType
, object oldDataObject, object newDataObject, DataSourceOperation operation)
{
return s_GetResolvedMethodData.Invoke(this, new object[] { type, methodName, dataObjectType, oldDataObject, newDataObject, operation });
}
private IOrderedDictionary ExtractMethodParameters(object method)
{
return (IOrderedDictionary)s_Parameters.GetValue(method);
}
private object InvokeMethod(object method)
{
return s_InvokeMethod.Invoke(this, new object[] { method });
}
private int ExtractAffectedRows(object result)
{
return (int)s_AffectedRows.GetValue(result);
}
#endregion
}
Once again, I'm doing the reflection work once in a static constructor. In my next (and final?) post on this mess, I'll show you how my DynamicCall stuff makes this code look much nicer and makes it tons type-safer (hmmm... is that something I want to have forever Google-able?). There are a couple of notes.
- In all but the insert case, I have a list of keys provided by my caller in a seperate collection. It would be nice to be able to pass that along to the
AcquireDataObject
method, but it turns out that this isn't necessarily possible or sufficient. Since the ObjectDataSource
forwards any explicitly-defined parameters through to the ObjectDataSourceView
the key collection given to the update and delete methods may or may not contain the actual key. Rather, the key could be coming from the InsertParameters
, DeleteParameters
or UpdateParameters
collections (which get merged-in in the respective mutation methods). - It would be nice for the
AcquireDataObject
method to be told why we were trying to acquire the object (e.g. are we doing an insert, update or delete). I didn't make that change in this verison simply to make it easier the correlation to the Microsoft verison, but in the final post, I've added that argument. This allows calling the correct factory method or any other operation-specific behavior. - The calls to
this.Owner.InvalidateCache()
replaces the bit of class-envy in the original Microsoft version that I talked about in part one.