Take This… ObjectDataSource!
September 19, 2006 on 2:30 pm | In .NET Coding |Now with Vitamin C!
THIS IS COMPLETELY REVISED, so it bears almost no resemblence, except for the only slightly less than clever title of the article, to it’s former bug-ridden self. All of this code is now included in RegGen 0.4.6 available (again) at http://sourceforge.net/projects/refgen.
First thing’s first, you need an interface to denote cachable objects, and to uniquely identify things, they need, of course an ID. So lets force you to call it ID. Also, for sorting of any kind to work, you also need it to be comparable to itself, so we thus have:
/// <summary>
/// For things that have ID properties.
/// </summary>
public interface IIdentifiable<T>
where T: IComparable<T>
{
T ID { get; set; }
}
Next we need to break the access code into two categories: Raw, and possibly cahce buffered.
The Raw Data Source (fancy because of the capital letters) needs to be able to do some basic functions, which if you’re using ADO, are basically automatic. If you’re using a webservice, well you should know how to write that sort of code anyway, or you have no business using webservices as your data source! (haha, just kidding, here’s a link to msdn article on it)
public interface IDataStore<T,K>
where T: IComparable<T>
where K: IIdentifiable<T>
{
List<K> GetAll();
List<K> GetList(T[] Keys);
List<K> GetLimitedRange(T StartKey, int MaxRange);
List<K> GetRange(T StartKey, T EndKey);
K Get(T id);
bool Update(K Obj);
K Insert(K Obj);
void Delete(T ID);
}
Then, we need something that’s standardized to use the cache. I like to make this easy to remember by making the names slightly longer. You may have a better idea for the names of the methods, and if you do, that’s fine, but this works for me:
public interface IModel<T, K>
where T : IComparable<T>
where K : IIdentifiable<T>
{
List<K> GetThemAll(bool fromDataStore);
List<K> GetThese(T[] Objs);
List<K> GetThese(K[] Objs);
List<K> GetBetween(T MinID, T MaxID);
List<K> GetPage(T StartID, int Max);
K GetOne(K Obj);
K GetOne(T ID);
void UpdateOne(K Obj);
K AddOne(K Obj);
void DeleteOne(K Obj);
void DeleteOne(T ID);
System.Data.DataTable GetDataTable();
System.Data.DataTable GetDataTable(T[] IDs);
void RefreshInvalids();
void Invalidate(T ID);
void InvalidateAll();
}
So what are these for exactly? I mean, how do we get from those to a caching datasource? You probably figure that I’m going to show you, so if you did, you’re correct. Here’s a generic class that implements both the IDataStore and the IModel to give us an abstract class from which you can derive your custom business objects.
public abstract class Model<T,K> : IModel<T,K>, IDataStore<T,K>
where T: IComparable<T>
where K: IIdentifiable<T>, new()
{
static Cache<T, K> cachedItems;
static List<T> InvalidKeys;
static bool isNew;
static Model()
{
cachedItems = new Cache<T,K>();
InvalidKeys = new List<T>();
isNew = true;
}
#region IModel<T,K> Members
public List<K> GetThemAll(bool fromDataStore)
{
List<K> items;
if (fromDataStore)
{
items = GetAll();
foreach (K item in items)
{
AddToCache(item);
}
}
else
{
RefreshInvalids();
items = new List<K>();
foreach (K item in cachedItems.GetValues())
{
items.Add(item);
}
}
List<K> toReturn = new List<K>();
foreach (K item in items)
{
toReturn.Add(item);
}
return toReturn;
}
private void AddToCache(K item)
{
cachedItems.Add(item.ID, item);
}
private bool isValid(T k)
{
return (!InvalidKeys.Contains(k) && cachedItems.ContainsKey(k));
}
private K resolve(T id)
{
K obj = Get(id);
if (obj != null)
{
cachedItems.Add(id, obj);
if (InvalidKeys.Contains(id)) InvalidKeys.Remove(id);
}
return cachedItems[id];
}
public List<K> GetThese(T[] Objs)
{
List<K> toReturn = new List<K>();
foreach (T item in Objs)
{
if (isValid(item))
{
toReturn.Add(cachedItems.GetValue(item));
continue;
}
resolve(item);
toReturn.Add(cachedItems.GetValue(item));
}
return toReturn;
}
public List<K> GetThese(K[] Objs)
{
List<K> toReturn = new List<K>();
foreach (K item in Objs)
{
if (isValid(item.ID))
{
toReturn.Add(cachedItems.GetValue(item.ID));
continue;
}
resolve(item.ID);
toReturn.Add(cachedItems.GetValue(item.ID));
}
return toReturn;
}
public List<K> GetBetween(T MinID, T MaxID)
{
List<K> all = GetThemAll(true);
List<K> toReturn = new List<K>();
// wow! this is cool!
all.FindAll(delegate(K Obj) { return Obj.ID.CompareTo(MaxID) >= 0 != Obj.ID.CompareTo(MaxID) <= 0;}).ForEach(toReturn.Add);
return toReturn;
}
public List<K> GetPage(T StartID, int Max)
{
List<K> all = GetThemAll(false);
all.Sort();
int count = 0;
List<K> toReturn = new List<K>();
// I'm really liking anonymous delegates for this stuff!
all.FindAll(delegate(K item) { return (item.ID.CompareTo(StartID) >= 0 && ++count < Max); }).ForEach(delegate(K item) { toReturn.Add(item); });
return toReturn;
}
/// <summary>
/// Given an object, call GetOne with the ID -- this doesn't update it, just gets it (so the select method can pass an object as a parameter)
/// </summary>
/// <param name="Obj"></param>
/// <returns></returns>
public K GetOne(K Obj)
{
return GetOne(Obj.ID);
}
/// <summary>
/// Get an object from the cache, or from the data store, adding it to the cache on the way.
/// </summary>
/// <param name="ID"></param>
/// <returns></returns>
public K GetOne(T ID)
{
RefreshInvalids();
if (!cachedItems.ContainsKey(ID))
{
K storedObj = Get(ID);
if (storedObj != null)
{
AddToCache(storedObj);
return storedObj;
}
// the id you asked for isn't in the underlying data source
return default(K);
}
return cachedItems[ID];
}
/// <summary>
/// Update the object in the underlying data store, and update it in the cache
/// </summary>
/// <param name="Obj"></param>
public void UpdateOne(K Obj)
{
if (Update(Obj))
AddToCache(Obj);
}
/// <summary>
/// Add a new object to the underlying data store, and add it to the cache
/// </summary>
/// <param name="Obj"></param>
/// <returns></returns>
public K AddOne(K Obj)
{
K newObj = Insert(Obj);
if (newObj != null)
AddToCache(newObj);
return newObj;
}
public void DeleteOne(K Obj)
{
DeleteOne(Obj.ID);
}
public void DeleteOne(T ID)
{
// remove it from the cache, if present
if (cachedItems.ContainsKey(ID))
cachedItems.Remove(ID);
// remove it from invalid keys, if present
if (InvalidKeys.Contains(ID))
InvalidKeys.Remove(ID);
// tell the underlying data store to kill it.
Delete(ID);
}
public System.Data.DataTable GetDataTable()
{
bool fromDataSource = isNew;
isNew = false;
return ObjectDataTableFactory<T, K>.GetFilledTable(GetThemAll(fromDataSource));
}
public System.Data.DataTable GetDataTable(T[] IDs)
{
List<K> items = GetThese(IDs);
return ObjectDataTableFactory<T, K>.GetFilledTable(items);
}
public void RefreshInvalids()
{
foreach (T invalidKey in InvalidKeys)
{
K refreshed = Get(invalidKey);
if (refreshed != null)
{
cachedItems[invalidKey] = refreshed;
InvalidKeys.Remove(invalidKey);
continue; // skip up and out to the next guy
}
cachedItems.Remove(invalidKey);
}
}
public void Invalidate(T ID)
{
InvalidKeys.Add(ID);
}
public void InvalidateAll()
{
foreach (T key in cachedItems.GetKeys())
{
if ( ! InvalidKeys.Contains(key) )
InvalidKeys.Add(key);
}
}
#endregion
/// <summary>
/// These routines (IDATASTORE) are the ones that you actually implement in your business object,
/// These are meant to connect to your webservice, SQL database, or whatever you're using for data.
/// But if you want to access the data buffered through the cache, call the ones from IModel.
/// </summary>
/// <returns></returns>
#region IDataStore<T,K> Members
public abstract List<K> GetAll();
public abstract List<K> GetList(T[] Keys);
public abstract List<K> GetLimitedRange(T StartKey, int MaxRange);
public abstract List<K> GetRange(T StartKey, T EndKey);
public abstract K Get(T id);
public abstract bool Update(K Obj);
public abstract K Insert(K Obj);
public abstract void Delete(T ID);
#endregion
}
As you can see I leave the implementation of the IDataStore to you, but the IModel is pretty much going to work no matter what your backend storage is like.
As I’ve said before, this will allow for you to actually retreive VALUES in the update event args and the insert event args of your update procs for the object datasource. In a normal, non-static backing class for the ODS, you get fresh baked objects (and thus EMPTY values) which basically renders it completely useless. You’ll want to bind your data source for a Master grid view to an object datasource who’s select method is the GetDataTable method, and your Details object data source (that does the actual inserting, updating, and deleting methods for your DetailsView or FormView to the GetOne, InsertOne, UpdateOne, and DeleteOne methods. Don’t worry about parameters, since there are signatures that accept the (K Obj) for each, the ODS will kindly pick the right one for you. One more thing to mention, set EnableCaching to False.
A quick note: the Model tries to fill all of the records the first time you hit it. Note the use of the ‘isNew’ flag. I wasn’t quite sure how most people would like to handle that, so I just defaulted it to that behavior. If you don’t like that (and I can see a bunch of scenarios where you wouldn’t) you can change that, but most likely if you’re randomly frustrated with anything about this code, that’ll be what it is. Also take note that the Cache
I hope you like it, and again, it works now. Cheers!
No Comments yet »
RSS feed for comments on this post. TrackBack URI
Leave a comment
Powered by WordPress with Pool theme design by Borja Fernandez.
Entries and comments feeds.
Valid XHTML and CSS. ^Top^
