navigation
 Saturday, April 28, 2007

Behind this dropdownlist is some of the coolest code I have written for ages!

 

 

How can that be? Well, I learnt a lot about generics and anonymous delegates in the process… if you want to see some totally geeky code, read on,

Of course, there is a chance that for most of you this is all old hat, in which case you can go back to sleep…it was new to me for sure.

 

Still awake?

 

So, first of all, I had a lot of fun retrieving the data that populates this list.

The data that drives this DropDownList is a list of what I call FeatureGroups (the list has 2 elements, Defects and  Manage Projects), which has a Master Detail relationship to Features (a FeatureGroup contains 0 or more Features). It is in fact a List<AFFeatureGroup>, where each FeatureGroup contains a List<Feature>. Feature references back to its containing FeatureGroup through a FeatureGroupID member.

 

When I go to populate this list of FeatureGroups, I want to do it in one database call, so what I fetch is a flattened out Result Set that contains { FeatureGroup.ID, FeatureGroup.Name, Feature.ID, Feature.Name }.

I then need to iterate through that result set and for each FeatureGroup.ID I find, see if I already have created a FeatureGroup for that ID, if not, create it, then add the Feature to that new or existing feature group.

 

This is a standard pattern, you would have a list of your FeatureGroups in one hand, and be adding to it and searching in it as you go while looping through the flattened result set. Well here is how to spice it up!

 

It turns out that the generic List<T> type has a Find<T>( Predicate<T> match ). The predicate method is a method that takes a Predicate<T> and returns a bool. This method passed in as this parameter is called once on each item in the list, and the first time it returns true, that will be the item returned by Find. And, when invoking Find, you can pass in an anonymous delegate. In other words, you don’t have to pass in a new Predicate<T>( myCompareMethod ), you can just pass in the implementation of myCompareMethod…

This is how it looks:

 

private static List<AFFeatureGroup> GetFeatureGroupsFromFeatures( List<AFFeature> features )
{
  List<AFFeatureGroup> result = new List<AFFeatureGroup>();
  foreach (AFFeature f in features)
  {
    AFFeatureGroup fg = result.Find(
      delegate(AFFeatureGroup featureGroup) { return featureGroup.ID == f.FeatureGroupID; });
    if (fg == null)
    {
      fg = new AFFeatureGroup(f.FeatureGroupID, f.FeatureGroupName);
      result.Add(fg);
    }
    fg.Features.Add(f);
  }
  return result;
}

 

Let me break this down:

 

-          The method receives a List of AFFeatures, this is the flattened out result set containing one Feature in each row with its FeatureGroup as well.

-          The first line, result = new List<…> just creates an empty list of AFFeatureGroups that I will be returning once populated

-          Then,  I loop through all the features that I have fetched

-          In the loop, I call the Find method of List<AFFeatureGroup>, passing  my find implementation. Note two things:

o   a) I declare that the anonymous delegate accepts a AFFeatureGroup, which is mandated by the signature of Predicate<T>, as this code will be called once for each AFFeatureGroup in the list until a match is found

o   b) Inside the implementation curly braces, I can compare the featureGroup.ID of this passed in AFFeatureGroup to the value of f.FeatureGroupID. Think about that some! This is the powerful part! It’s kind of like in Delphi when you could pass in the address of a local nested method and that method would have access to any local variables or parameters in the enclosing method scope. The variable f is in the loop that encloses the call to Find, so in effect, what the compiler does is it captures the current value of f when creating the anonymous method, and makes it available inside the method body! Basically you have a dynamically created nested local method inside of the for loop. This is awesome compiler magic!

-          OK, so if a match is not found, I create a new AFFeatureGroup, and I then add it to the result list

-          If a match was found, or if I just created one, I can now add the feature itself to the feature group

 

And that is it: with this simple set of code I am doing a pretty complex construction of objects! This is what the language features of C# 3.0 will be taking one step further, where I will be able to use type inference to get rid of even more code above, for instance the declaration delegate(AFFeatureGroup featureGroup) can go away and I will just have a lambda expression there instead.

 

So, now for geek part 2…. I now have a method that will take a flattened list and build my two dimensional structure, but now I want to cache that list. Well, first I started with the classic approach, I look in the Cache object for a certain key, and if I don’t find it, I go fetch the data from the database (using the method in part 1), and store it in the cache. But this gets so repetitive, and I also wanted to centralize some of the aspects of the caching, like for how long the cached data is kept.

 

I started off along the lines that my colleague Adam Markowitz pioneered in the Velocity project, where you basically can create a CacheManager class that uses generics to do a type safe cache search. But then I figured if List<T> can accept a Predicate<T> in its Find method, why can’t I write a Cache.Get<T> that also accepts a T Fetcher<T> that will be invoked if there is no cache hit? Of course, Fetcher<T> might need some parameters, so I decided I would need two types of delegate, one that takes no parameters, and one that takes an instance of some arbitrary type D as a parameter (I could have been lazy and used an object instead of D, but what I love about Generics is the type safety it gives you without the performance drawbacks) :

 

public delegate T Fetcher<T, D> ( D data );
public delegate T ParameterlessFetcher<T>();
 

 

Now, my CacheManager can accept one of these 2 delegate types as a parameter in its GetFromCache method, and invoke it if the Cache doesn’t have the sought after data. For instance using the parameterless delegate:

 

public static T GetFromCache<T>(string key, ParameterlessFetcher<T> fetcher)
{
  T res = GetFromCache<T>(key);
  if (res == null)
  {
    res = fetcher();
    AddToCache(key, res);
  }
  return res;
}

 

The GetFromCache and AddToCache are the actual Cachce accessors:

 

public static void AddToCache(string key, object dataToCache)
{
  HttpContext.Current.Cache.Insert(key, dataToCache, null, DateTime.MaxValue, m_defaultTimeSpan);
}

public static T GetFromCache<T>(string key)
{
  return (T) HttpContext.Current.Cache[key];
 

 

Note that I centralize the timespan handling so that this is consistent:

 

private static TimeSpan m_defaultTimeSpan = new TimeSpan(0, 20, 0);

 

Here is how the parameter version looks:

 

public static T GetFromCache<T,D>(string key, Fetcher<T,D> fetcher, D data )
{
  T res = GetFromCache<T>(key);
  if (res == null)
  {
    res = fetcher(data);
    AddToCache(key, res);
  }
  return res;
}

 

And, putting it all together:

 

using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.Caching;

namespace Falafel.ActiveFocus.Common
{
 public class AFCacheManager
 {
  private static TimeSpan m_defaultTimeSpan = new TimeSpan(0, 20, 0);

  public delegate T Fetcher<T, D> ( D data );
  public delegate T ParameterlessFetcher<T>();

  public static T GetFromCache<T,D>(string key, Fetcher<T,D> fetcher, D data )
  {
    T res = GetFromCache<T>(key);
    if (res == null)
    {
      res = fetcher(data);
      AddToCache(key, res);
    }
    return res;
  }

  public static T GetFromCache<T>(string key, ParameterlessFetcher<T> fetcher)
  {
    T res = GetFromCache<T>(key);
    if (res == null)
    {
      res = fetcher();
      AddToCache(key, res);
    }
    return res;
  }

  public static void AddToCache(string key, object dataToCache)
  {
    HttpContext.Current.Cache.Insert(key, dataToCache, null, DateTime.MaxValue, m_defaultTimeSpan);
  }

  public static T GetFromCache<T>(string key)
  {
    return (T) HttpContext.Current.Cache[key];
  }
 }
}
 

 

Now, what is exciting here is that I have a completely generic cache manager that can store and retrieve anything in a typesafe manner, using a delegate to do any fetching needed! Here is how I now use it to fetch that cached list of FeatureGroups:

 

public static List<AFFeatureGroup> CacheAllFeatureGroupsAndFeatures()
{
  return AFCacheManager.GetFromCache<List<AFFeatureGroup>>(
    CACHEKEY_FEATUREGROUPS,
    delegate() { return GetFeatureGroupsFromFeatures(new AFFeatureData().GetFeatures()); });
}

 

This is just amazingly concise to me! All I am supplying is :

 

-          the type of the cached object (a List<AFFeatureGroup>)

-          the Cache key to use (the constant CACHEKEY_FEATUREGROUPS)

-          the code to call when missing the data in the cache

 

Note that the code is now another anonymous delegate, it actually calls the method I explained in step 1 above that parses out the flat data. Here you also see how that method in turn gets the data by calling another method, GetFeatures. That in turn actually uses a whole similar approach of generics, that I wrote, which gives as a typesafe  way to convert result sets to lists of objects, but I won’t go there now!

 

And finally, to get down to that dropdownlist:

 

private void PopulateFeatureGroups()
{
  List<AFFeatureGroup> fglist = AFFeatureGroupData.CacheAllFeatureGroupsAndFeatures();
  ddlFeatureGroups.Items.Add(new ListItem("(All)", AFConsts.EMPTY_ITEM_VALUE.ToString()));
  foreach (AFFeatureGroup fg in fglist)
    ddlFeatureGroups.Items.Add(new ListItem(fg.Name, fg.ID.ToString()));
}

And it’s a wrap!

 

And what did I gain from all this convoluted programming asides from elegance and brevity?

I gained a reusable cache that can be tested and trusted and will save me a bunch of time caching other objects in a consistent and type safe manner. Now wasn’t that a geeky way to fill a drop down list?