In memory cache for Silverlight 2 and 3

I’ve seen some people talking about Silverlight Caching on the Silverlight forum. And because there’s no solution that can be compared to for example the ASP.NET solution for Caching. Of course it’s not that difficult to keep some data in a simple reference, just a variable, maybe on global maybe on class level. But yes, if you want a little bit more advanced Cache which supports the expiring of data you’ll need something different.

For the general purpose of caching data which can expire after a certain timespan I wrote this solution that can be used in Silverlight 2 and 3. You can view the whole code at the end of this article.

Adding and getting data from Cache

I wanted a solution that could work with general objects the way I work in this example.

Cache.Current.Add("Testkey", 10, TimeSpan.FromMinutes(2));
object cacheData = Cache.Current.Get("Testkey");
if(cacheData!=null)
{
    int cacheDataTyped = (int) cacheData;
}

In those situations you will have to cast the data to the required Type or Structure. This works fine, we’ve done this before while accessing the ASP.NET Cache. But sometimes you’ll want to have it typed immediately. So I want to be able to use this as well.

User user = GetUser();
Cache.Current.Add("Testkeytyped", user, TimeSpan.FromMinutes(4));
User userFromCache = Cache.Current.Get<User>("Testkeytyped");

So I had to add a Generic function Get and a none Generic function Get. I did this, and the code above works fine indeed.

But what about expiring data?

To expire data I wanted to provide a TimeSpan while adding the item to the cache. The first solution I thought of was to remove items upon get if it’s no longer valid. But in the end I thought what if the amount of data is large, in those cases you want it removed as soon as it is expired. So I found a different solution. By making use of the Timer class in Silverlight. I want the timer to tick every minute, and on tick I want it to clean the cache. So the current solutions cleans up items in the cache every minute. And upon getting of items it will check if the item is still valid or not.

Please feel free to use this is solution. It’s not thread-safe, but you can add thread-safe support yourself if you want to.

/// <summary>
/// Class supports in memory cache for Silverlight applications. Silverlight 2 and 3 are supported.
/// Each minute the cache items are iterated for validility, invalid cache items are removed.
/// </summary>
public class Cache : IDisposable
{
    public static Cache Current = new Cache();
    private readonly IDictionary<string, CacheItem> _cacheItems = new Dictionary<string, CacheItem>();

    private readonly TimeSpan _period = TimeSpan.FromMinutes(1);
    private readonly TimeSpan _startTimeSpan = TimeSpan.Zero;
    private readonly TimeSpan _stopTimeSpan = TimeSpan.FromMilliseconds(-1);
    private readonly Timer _timer;
    private TimerState _state = TimerState.Stopped;

    private Cache()
    {
        _timer = new Timer(CleanUpItems, this, _stopTimeSpan, _period);
    }

    /// <summary>
    /// All the CacheItems are in a Dictionary
    /// </summary>
    public IDictionary<string, CacheItem> CacheItems
    {
        get { return _cacheItems; }
    }

    /// <summary>
    /// Get full CacheItem based on key.
    /// </summary>
    /// <param name="key">The key for which a CacheItem is stored.</param>
    /// <returns>The CacheItem stored for the given key, if it is still valid. Otherwise null.</returns>
    public CacheItem this[string key]
    {
        get
        {
            if (CacheItems.ContainsKey(key))
            {
                CacheItem ci = CacheItems[key];
                if (ci.IsValid())
                    return CacheItems[key];
            }
            return null;
        }
        set { CacheItems[key] = value; }
    }

    #region IDisposable Members

    public void Dispose()
    {
        _timer.Dispose();
    }

    #endregion

    /// <summary>
    /// Get data typed directly for given key.
    /// </summary>
    /// <typeparam name="T">The Type for which the data is set. If the type is wrong null will be returned.</typeparam>
    /// <param name="key">The key for which a CacheItem is stored.</param>
    /// <returns>The data typed for the given key, if it is still valid. Otherwise null.</returns>
    public T Get<T>(string key) where T : class
    {
        CacheItem item = this[key];
        if (item != null)
            return item.GetData<T>();
        return null;
    }

    /// <summary>
    /// Get data untyped directly for given key.
    /// </summary>
    /// <param name="key">The key for which a CacheItem is stored.</param>
    /// <returns>The data untyped for the given key, if it is still valid. Otherwise null.</returns>
    public object Get(string key)
    {
        CacheItem item = this[key];
        if (item != null)
            return item.GetData();
        return null;
    }

    private void StartTimer()
    {
        if (_state == TimerState.Stopped)
        {
            _timer.Change(_startTimeSpan, _period);
            _state = TimerState.Started;
        }
    }

    private void StopTimer()
    {
        if (_state == TimerState.Started)
        {
            _timer.Change(_stopTimeSpan, _period);
            _state = TimerState.Stopped;
        }
    }

    /// <summary>
    /// Clean up items that are not longer valid.
    /// </summary>
    /// <param name="state">Expect state to be the cache object.</param>
    private static void CleanUpItems(object state)
    {
        var cache = state as Cache;
        if (cache != null)
        {
            List<KeyValuePair<string, CacheItem>> itemsToRemove =
                cache.CacheItems.Where(i => !i.Value.IsValid()).ToList();
            foreach (var item in itemsToRemove)
            {
                cache.CacheItems.Remove(item.Key);
            }
            if (cache.CacheItems.Count == 0)
                cache.StopTimer();
        }
    }

    /// <summary>
    /// Add a new item to the cache. If the key is already used it will be overwritten. 
    /// </summary>
    /// <param name="key">The key for which a CacheItem is stored.</param>
    /// <param name="value"></param>
    public void Add(string key, CacheItem value)
    {
        if (_cacheItems.ContainsKey(key))
            _cacheItems.Remove(key);
        _cacheItems.Add(key, value);
        StartTimer();
    }

    /// <summary>
    /// Add a new item to the cache. If the key is already used it will be overwritten. 
    /// </summary>
    /// <param name="key">The key for which a CacheItem is stored.</param>
    /// <param name="data">The data to cache.</param>
    /// <param name="validDuration">The duration of the caching of the data.</param>
    public void Add(string key, object data, TimeSpan validDuration)
    {
        Add(key, new CacheItem(data, validDuration));
    }

    /// <summary>
    /// Removes the item for the given key from the cache.
    /// </summary>
    /// <param name="key">The key for which a CacheItem is stored.</param>
    /// <returns></returns>
    public bool Remove(string key)
    {
        bool remove = _cacheItems.Remove(key);
        if (_cacheItems.Count == 0)
            StopTimer();
        return remove;
    }

    #region Nested type: TimerState

    /// <summary>
    /// Used for determing TimerState
    /// </summary>
    private enum TimerState
    {
        Stopped,
        Started
    }

    #endregion
}

/// <summary>
/// Defines an item that's stored in the Cache.
/// </summary>
public class CacheItem
{
    private readonly DateTime _creationDate = DateTime.Now;

    private readonly TimeSpan _validDuration;

    /// <summary>
    /// Constructs a cache item with for the data with a validity of validDuration.
    /// </summary>
    /// <param name="data">The data for the cache.</param>
    /// <param name="validDuration">The duration for the data being valid in the cache.</param>
    public CacheItem(object data, TimeSpan validDuration)
    {
        _validDuration = validDuration;
        Data = data;
    }

    /// <summary>
    /// The data in the Cache.
    /// </summary>
    public object Data { set; private get; }

    /// <summary>
    /// Gets the Data typed.
    /// </summary>
    /// <typeparam name="T">The Type for which the data is set. If the type is wrong null will be returned.</typeparam>
    /// <returns>The data typed.</returns>
    public T GetData<T>() where T : class
    {
        return Data as T;
    }

    /// <summary>
    /// Gets the Data untyped.
    /// </summary>
    /// <returns>The data untyped.</returns>
    public object GetData()
    {
        return Data;
    }

    /// <summary>
    /// Check if the Data is still valid.
    /// </summary>
    /// <returns>Valid if the validDuration hasn't passed.</returns>
    public bool IsValid()
    {
        return _creationDate.Add(_validDuration) > DateTime.Now;
    }
}

Ps. This article is cross posted on: Mark Monster’s blog and Silverlight Help.

Gravatar