ASP.NET developers implement caching data frequently to avoid requesting the database each time. It is always useful to think that your mobile users don’t have a super 3G/WIFI connection every time.
If as in ASP.NET, you want to add an expiration, meaning that the object is available for a certain period, you must pass by specific code, it’s not possible with standard API.
To achieve this functionality, you must develop a wrapper, so let’s take a look on the code:
The first step is to create a class named cache that contains a singleton, it also contains values for NoAbsoluteExpiration and NoSlidingExpiration.
public sealed class Cache
{
public static readonly DateTime NoAbsoluteExpiration = DateTime.MaxValue;
public static readonly TimeSpan NoSlidingExpiration = TimeSpan.Zero;
// Acces to the isolatedstorage
readonly IsolatedStorageFile _myStore = IsolatedStorageFile.GetUserStoreForApplication();
// singleton
private static Cache _current;
public static Cache Current
{
get { return _current ?? (_current = new Cache()); }
}
Like the ASP.NET API, we write a method to add an item to the cache and to set the timeouts.
An object in the cache is defined by a key (to return it), the object that will be serialized and the timespan/datetime parameters.
/// <summary>
/// Adds the specified key.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
/// <param name="absoluteExpiration">The absolute expiration.</param>
/// <param name="slidingExpiration">The sliding expiration.</param>
public void Add(string key, object value, DateTime absoluteExpiration, TimeSpan slidingExpiration)
{
lock (_sync)
{
if (Contains(key))
Remove(key);
if (absoluteExpiration == NoAbsoluteExpiration)
Add(key, DateTime.UtcNow + slidingExpiration, value);
if (slidingExpiration == NoSlidingExpiration)
Add(key, absoluteExpiration, value);
}
}
Then we add the object in the isolated storage. For each key, a directory with the key as name is created, then we create a file named with the expiration date with the Windows file time UTC format (A Windows file time is a 64-bit value that represents the number of intervals of 100 nanoseconds that have elapsed since 12: 00 midnight, 1 January 1601).
/// <summary>
/// Adds the specified key.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="expirationDate">The expiration date.</param>
/// <param name="value">The value.</param>
private void Add(string key, DateTime expirationDate, object value)
{
lock (_sync)
{
if (!_myStore.DirectoryExists(key))
_myStore.CreateDirectory(key);
else
{
// If the key exist, we delete the directory and the file
string currentFile = _myStore.GetFileNames(string.Format("{0}\\*.cache", key).FirstOrDefault();
if (currentFile != null)
_myStore.DeleteFile(string.Format("{0}\\{1}", key, currentFile));
_myStore.DeleteDirectory(key);
_myStore.CreateDirectory(key);
}
string fileName = string.Format("{0}\\{1}.cache", key, expirationDate.ToFileTimeUtc());
if (_myStore.FileExists(fileName))
_myStore.DeleteFile(fileName);
using (var isolatedStorageFileStream = new IsolatedStorageFileStream(fileName, FileMode.OpenOrCreate, _myStore))
{
DataContractSerializer s = new DataContractSerializer(value.GetType());
s.WriteObject(isolatedStorageFileStream, value);
}
}
}
Then the last step is to retrieve the value in the cache, if the cache has expired, the method returns null.
/// <summary>
/// Gets the specified key.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">The key.</param>
/// <returns></returns>
public T Get<T>(string key)
{
lock (_sync)
{
// Getting th file
string currentFile = _myStore.GetFileNames(string.Format("{0}\\*.cache", key).FirstOrDefault();
if (currentFile != null)
{
// We check if the cache has not expired
var expirationDate =
DateTime.FromFileTimeUtc(long.Parse(Path.GetFileNameWithoutExtension(currentFile)));
if (expirationDate >= DateTime.UtcNow)
{
// read the file from the isolatedstorage
using (var isolatedStorageFileStream = new IsolatedStorageFileStream(
string.Format(@"{0}\{1}", key, currentFile), FileMode.Open, _myStore))
{
DataContractSerializer s = new DataContractSerializer(typeof(T));
var value = s.ReadObject(isolatedStorageFileStream);
isolatedStorageFileStream.Close();
return (T)value;
}
}
// If the cache has expired, we delete the file from the isolatedstorage
Remove(key);
}
return default(T);
}
}
Now that we have the foundations for a cache system, we can use it easily with a bit of Rx. In this example, we have a GetAll method that returns data from a WCF service. The MyServiceClient was generated by Visual Studio.
public void GetAll(Action<ObservableCollection<MyEntity>> callback)
{
// We check if the cache has not expired
if (Cache.Current.Contains("KEY1"))
{
// W read the cache in a new thread then we call the callback in the dispatcher thread
Observable.Start(() =>
Cache.Current.Get<ObservableCollection<MyEntity>>("KEY1"), Scheduler.ThreadPool).
ObserveOn(Scheduler.Dispatcher).Subscribe(callback);
return;
}
// New WCF client
MyServiceClient client = new MyServiceClient();
Observable.FromEvent<GetAllCompletedEventArgs>(client, "GetAllCompleted")
.ObserveOn(Scheduler.ThreadPool)
.Select(s =>
{
// In a new thread, we had the object received by WCF in the cache,
//the life time is 1 day
if (s.EventArgs.Error == null)
{
Cache.Current.Add("KEY1", s.EventArgs.Result, Cache.NoAbsoluteExpiration, TimeSpan.FromDays(1));
}
return s;
})
.ObserveOn(Scheduler.Dispatcher).Subscribe(s =>
{
// Then in the dispacher thread, we call the callback
callback(s.EventArgs.Result);
});
// Async call for the GetAll method
client.GetAllAsync();
}
This code is used for the Warnygo project, and is of course available in full: Phone7.Fx.Preview.zip
If you have feedback, you are welcome !