An isolated storage approach for client side caching for feeding a LightSwitch custom control.

introduction

Silverlight has a very nice feature: isolated storage. This could be used for caching data client side. Unfortunately, this feature can not be used in a normal LightSwitch grid, because the databinding of LightSwitch grid is focused on the viewmodel and retrieves the data from the server. Nonetheless, client side caching is a useful technique when dealing with custom controls in LightSwitch. (e.g. a treeview control)

What’s the big picture?

Imagine my application needs a very big data tree which is updated only over night (very a process separate from my lightSwitch app). I want to be able to search in the tree in a very comfortable (read: fast) way.

In that scenario, client side caching can become your friend. I’ll focus in this blog post solely on the client side caching part, and not on the integration with a treeview control. I’m aware of the fact, that this makes things a bit abstract, but I’ll cover soon a post on how to do the treeview control integration.

What kind of low-level client side caching support do we need?

Well, … in LightSwitch terms client side caching is about being able to serialize data in the form of  an IEnumerable<IEntityObject>. Obviously, we need a “serialization technology” which will impose restrictions on how we will serialize the data. The solution I have in mind should be usable in a generic way, so, … it’s kind of IsolatedStorageEntityCollectionCache. Ok, let’s use that as class name :)

The most obvious choice for a serialization mechanism, is the DataContractSerializer. This make we need a dedicated class describing the format of our serialized data. Of course, we want to be able to map the structure of our incoming data to the structure in the DataContract that is necessary for working with the DataContractSerializer.

For our treeview solution we have in mind, this would mean:

[DataContract]
    public class CachedDepartment
    {
        [DataMember]
        public int Id { get; set; }
        [DataMember]
        public int? ParentId { get; set; }
        [DataMember]
        public string Name { get; set; }
    }

This boils down to requirement 1: we need to be able to map our incoming data (that we want to serialize) to a specific datacontract.

Our second requirement is more straightforward: we want to be able to specify a name for our serialized file (our app has potentially multiple client side caches).

Finally, we want that our cache can be tuned in the sense that we want to inject the algorithm to be used to tell use if the cache is expired or not. (e.g. specify that the cache expires after 2 hours, or any other algorithm).

This makes that we’ll instantiate our client side cache as follows:

 

_isolateStorage = new IsolatedStorageEntityCollectionCache<CachedDepartment>("deps.xml",
               entityObject => new CachedDepartment
               {
                   Id = (int)entityObject.Details.Properties["Id"].Value,
                   ParentId = (int?)entityObject.Details.Properties["ParentId"].Value,
                   Name = entityObject.Details.Properties["Name"].Value.ToString()
               },

              isolatedStorageFileCreationDateTimeOffset =>
              {
                  bool result = false;
                  if (isolatedStorageFileCreationDateTimeOffset >= DateTimeOffset.Now.AddHours(-2))
                  {
                      result = true;
                  }
                  return result;
              }
           );

 

So, this is based on following constructor:

public IsolatedStorageEntityCollectionCache(
            string isolatedStorageFileName,
            Func<IEntityObject, DestinationType> mappingSelector,
            Func<DateTimeOffset, bool> expiryDateCalculation)
        {
            _mappingSelector = mappingSelector;
            _isolatedStorageFileName = isolatedStorageFileName;
            _expiryDateCalculation = expiryDateCalculation;
        }

Ok, i admit, injecting lamdba expression in the constructor, is a bit academic.

  • Our first param  allows us to inject a mapping method (for mapping our data to be cached to a DataContract used during the serialization.
  • The second parameter is our file name in the isolated storage and
  • The third parameter is again a Func lamda expression representing the algorithm for calculating the expiry date of our cache.

The full cache provider goes as follows:

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.IsolatedStorage;
using System.Runtime.Serialization;
using System.Linq;
using System.Reflection;
using Microsoft.LightSwitch;

namespace SilverlightClassLibrary
{
    public class IsolatedStorageEntityCollectionCache<DestinationType>
    {
        private Func<IEntityObject, DestinationType> _mappingSelector;
        private Func<DateTimeOffset, bool> _expiryDateCalculation;
        private string _isolatedStorageFileName;

        public IsolatedStorageEntityCollectionCache(
            string isolatedStorageFileName,
            Func<IEntityObject, DestinationType> mappingSelector,
            Func<DateTimeOffset, bool> expiryDateCalculation)
        {
            _mappingSelector = mappingSelector;
            _isolatedStorageFileName = isolatedStorageFileName;
            _expiryDateCalculation = expiryDateCalculation;
        }

        public IEnumerable<DestinationType> RetrieveFromIsolatedStorage()
        {
            using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
            {
                using (IsolatedStorageFileStream isfs = new IsolatedStorageFileStream(_isolatedStorageFileName, FileMode.Open, isf))
                {
                    DataContractSerializer serializer = new DataContractSerializer(typeof(List<DestinationType>));
                    IEnumerable<DestinationType> cachedCollection = (serializer.ReadObject(isfs)) as IEnumerable<DestinationType>;
                    isfs.Close();
                    return cachedCollection;
                }
            }
        }

        public IEnumerable<DestinationType> StoreInIsolatedStorageAndReturnData(IEnumerable<IEntityObject> data)
        {
            IEnumerable<DestinationType> cachedData = data.Select(_mappingSelector).ToList<DestinationType>();

            using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
            {
                using (IsolatedStorageFileStream isfs = new IsolatedStorageFileStream(_isolatedStorageFileName, FileMode.OpenOrCreate, isf))
                {
                    DataContractSerializer serializer = new DataContractSerializer(typeof(List<DestinationType>), getKnownTypes<DestinationType>());
                    serializer.WriteObject(isfs, cachedData);
                    isfs.Close();
                    return cachedData;
                }
            }
        }


        public bool NonExpiredFileExistsInIsolatedStorage()
        {
            bool result = false;
            using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
            {
                if (isf.FileExists(_isolatedStorageFileName))
                {
                    DateTimeOffset creationDateExistingFile = GetIsolatedStorageFileDateTime();
                    result = _expiryDateCalculation(creationDateExistingFile);
                }
            }
            return result;
        }

        public void DeleteFileInIsolatedStorage()
        {
            using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
            {
                if (isf.FileExists(_isolatedStorageFileName))
                    isf.DeleteFile(_isolatedStorageFileName);
            }
        }

        private IEnumerable<Type> getKnownTypes<T>()
        {
            var knownTypes = new List<Type>();
            knownTypes.Add(typeof(T));
            knownTypes.Add(typeof(List<T>));
            return knownTypes;
        }

        private DateTimeOffset GetIsolatedStorageFileDateTime()
        {
            DateTimeOffset result = DateTimeOffset.MinValue;
            using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
            {
                if (isf.FileExists(_isolatedStorageFileName))
                {
                    result = isf.GetCreationTime(_isolatedStorageFileName);
                }
            }
            return result;
        }
        public void IncreaseStorage(long spaceRequest)
        {
            using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
            {
                isf.IncreaseQuotaTo(spaceRequest);
            }
        }
    }
}

 

As said, I’ll focus in a next post on how to use this for a cached treeview control, but I’ll give already to introduction on how to use this.

Since we speak about a custom control, following code can be used in the code behind of the custom control. We have shown already how to instantiate our IsolatedStorageEntityCollectionCache.

The next purpose is to use our cached data for binding them to our custom control:

 if (_isolateStorage.NonExpiredFileExistsInIsolatedStorage())
            {
                var result = _isolateStorage.RetrieveFromIsolatedStorage();

                _dataItemCollection = new DataItemCollection<DataItem>();
                // this _dataItemCollection is specific to our treeview implementation
                // we need to massage a bit our cached data and transform it to structure optimized for binding to our treeview control
                //...
                this.DataContext = _dataItemCollection;

            }

            else
            {
                _screen.Details.Dispatcher.BeginInvoke(() =>
                {
                    screen.Details.Methods["LoadDepartmentTree"].CreateInvocation(null).Execute();
                });
            }

Basically, what we’re doing here is to check if our cache contains data (under the cover, it will be checked if our cache is expired). If there is no non expired cached we’ll trigger a data load (in my case via the LightSwitch viewmodel.

When our data comes in, we want to store these data in our cache:

public void DepartmentTree_Loaded()
        {
            this.Dispatcher.BeginInvoke(() =>
            {
                IEnumerable<IEntityObject> departmentCollection = _screen.Details.Properties["DepartmentFlatTrees"].Value as IEnumerable<IEntityObject>;
                _isolateStorage.StoreInIsolatedStorageAndReturnData(departmentCollection);
                ConvertToDataItemCollection(departmentCollection);

                this.DataContext = _dataItemCollection;
            });
        }

Given the asynchronous nature of loading data, our cache infrastructure is a bit distributed in 2 methods.

Conclusion

The above could be a useful base for client side caching in LightSwitch for a dedicated custom control.