(X) Hide this
    • Login
    • Join
      • Generate New Image
        By clicking 'Register' you accept the terms of use .

Caching of, in, and around your Silverlight application (part 3)

(4 votes)
Kevin Dockx
>
Kevin Dockx
Joined Nov 15, 2010
Articles:   19
Comments:   8
More Articles
5 comments   /   posted on Dec 01, 2010

This article is compatible with the latest version of Silverlight.

This is the third in a 3-part series of articles about caching & Silverlight.

This is the third part in a 3-part series on Silverlight and Caching. The first part was about XAP & Assembly caching, which you can find here. In the part, we learned how we can leverage the Isolated Storage for caching in the same and between different Silverlight applications. You can find that article here. In this third and final part, we’ll look at the server side of things, and an extra piece of code to throw in the mix: a Silverlight caching provider.

The source code for this part can be found here.

 

Server side caching with a WCF Service

In the previous part, we’ve seen how we can make sure a certain server side method is only called once: when the data isn’t available on the client. But next to caching data on the client, it can also be cached on the server. But what’s the advantage of this? Sure, caching your data on the server might mean you only need to call your database once (assuming we’re fetching data from a database) – the next times, the data will come from the server side cache. But if we’re already caching the data on the client, aren’t we better off with that? With server side caching, you still need to call your service method every time, with client side caching, you only call it once, right?

Well: image a multi-user environment – pretty typical for most applications. If we use the example from the previous article, our GetCities operation, including the actual fetching of the data (which, in the example, is just using some dummy data, but in real-life would probably come from some kind of database), would be called once for each user. If you cache the data server side after the first data fetch, the first user call to the operation would result in a hit on your database. All the following calls, from different users, would result in getting the data from the server side cache instead of the database.

As you can see, this can offer tremendous advantages in multi-user environments. In a real-life environment, you should always combine client side caching with server side caching.

In a WCF service, you leverage the HttpContext’s Cache for this. Let’s adjust our example method:

[OperationContract] public List<City> GetCitiesCachedServerSide() { string cacheKey = "CachedCities"; // create a new generic list to hold the cities. List<City> cityList = new List<City>(); // check if the list is in cache if (HttpContext.Current.Cache.Get(cacheKey) == null) { cityList.Add(new City() { ID = "ANT", Description = "Antwerp" }); cityList.Add(new City() { ID = "BRU", Description = "Brussels" }); cityList.Add(new City() { ID = "GHE", Description = "Ghent" }); cityList.Add(new City() { ID = "AMS", Description = "Amsterdam" }); cityList.Add(new City() { ID = "PAR", Description = "Paris" }); cityList.Add(new City() { ID = "LON", Description = "London" }); cityList.Add(new City() { ID = "BAR", Description = "Barcelona" }); // add list to cache for 5 minutes HttpContext.Current.Cache.Add(cacheKey, cityList, null, DateTime.Now.AddMinutes(5),
Cache.NoSlidingExpiration, CacheItemPriority.Default, null); } else { // get list from cache cityList = (List<City>)HttpContext.Current.Cache[cacheKey]; } return cityList; }

As you can see, we check if the list of countries already exists in the cache. If it doesn’t, we add it, making sure each next call will fetch the data from the cache.

internal void GetFromWCFCachedServerSide()
{
    // call WCF Service
    CityServiceReference.CityServiceClient client = new CityServiceClient();
    client.GetCitiesCachedServerSideCompleted += (send, args) =>
    {
        if (args.Error == null)
        {
            Cities = args.Result;
            userIsolatedStorageSettings["CachingCities"] = new ObservableCollection<City>(args.Result);
            userIsolatedStorageSettings.Save();
        }
    };
    client.GetCitiesCachedServerSideAsync();
}

You’ve got a few options when adding an item to cache:

  • absoluteExpiration: enables you to define when an item in cache should become invalid, and thus removed from the cache. If you’re using sliding expiration, this should be set to NoAbsoluteExpiration.
  • slidingExpiration: defines the interval between when an object was last accessed and when it must be removed from the cache. If you’re using absolute expiration, this should be set to NoSlidingExpiration.
  • cacheItemPriority: the relative cost of an item in your cache. Items with lower cost are removed first.
  • onRemoveCallback: a callback method which will be called when an item is removed from cache.
  • dependencies: CacheDependency for the item you’re adding in the cache. If a dependency is changed, the item in cache automatically expires.

 

Server side caching with WCF RIA Services

A lot of Silverlight applications are built with WCF RIA Service. Rightfully so, as it offers tremendous advantages and is quite extensible. With WCF RIA Services, you’ve got a client side DomainContext, in which you can load your entities – and it keeps your entities there by default. This already results in quite a lot less data fetching operations when used correctly: often, the client side DomainContext is kept in an application-wide accessible LocalStateContainer (a simple static class) as a static property. The ViewModels that need data can directly access it from the DomainContext instance, keeping the need to refetch data to a minimum, and at the same time offering you the advantage that your screens stay “in sync” (as they’re all referring to the same data – change that data in one screen, and it’s automatically updated in a different screen using the same entities).

What’s less know is that you can also cache your WCF RIA Services result sets on the server – again, offering big advantages in a multi-user environment. The magic word here is the OutputCach attribute: decorate a query method with this method, and (depending on the options you provide), the resultset is cached for a certain amount of time. In the demo code, I’ve added a DomainService which returns a list of cities – this time, using WCF RIA Services instead of regular WCF. The resultset will be cached on the server:

[OutputCache(OutputCacheLocation.Server, 180, UseSlidingExpiration = true)]
public IQueryable<CityForRIA> GetCities()
{
    // create a new generic list to hold the cities
    List<CityForRIA> cityList = new List<CityForRIA>();
    cityList.Add(new CityForRIA() { ID = "ANT", Description = "Antwerp" });
    cityList.Add(new CityForRIA() { ID = "BRU", Description = "Brussels" });
    cityList.Add(new CityForRIA() { ID = "GHE", Description = "Ghent" });
    cityList.Add(new CityForRIA() { ID = "AMS", Description = "Amsterdam" });
    cityList.Add(new CityForRIA() { ID = "PAR", Description = "Paris" });
    cityList.Add(new CityForRIA() { ID = "LON", Description = "London" });
    cityList.Add(new CityForRIA() { ID = "BAR", Description = "Barcelona" });
    return cityList.AsQueryable<CityForRIA>();
}

And this is how we call the EntityQuery:

internal void GetFromRIAServCachedServerSide()
{
    var loadOp = LocalStateContainer.CityDomainContext
.Load<CityForRIA>(LocalStateContainer.CityDomainContext.GetCitiesQuery());
    loadOp.Completed += (send, args) =>
    {
        if (loadOp.HasError == false)
        {
            CitiesFromRIAServices = LocalStateContainer.CityDomainContext.CityForRIAs;
        }
    };
}

The OutputCache attribute can be provided with different options, of which these are the most important ones:

  • cacheLocation: defines where items can be cached: client, server, or both.
  • duration: the duration, in seconds, before an item will expire.
  • UseSlidingExpiration: if this is set to true, the duration will be reset each time an item from cache is accessed. If set to false, the item will expire after the duration – in absolute terms – has passed.
  • VaryByHeaders: if the message headers are different, a new item will be added to the cache instead of returning the already existing one.

 

Providing the missing parts: a Silverlight Cache Provider.

Throughout this article series, we’ve talked about various ways of caching. Those familiar with the ASP .NET Cache might have noticed something is missing in the Silverlight framework: yes, you can leverage the Isolated Storage for caching, but in a true caching scenario, you’d want to be able to make sure items in cache expire after a while. And are refetched automatically when needed, maybe? You might also want to make sure this is done with as little effort as possible throughout the development of your application. In ASP .NET, you’ve got the Cache store for this, but an equivalent doesn’t really exist in Silverlight: you have to write that yourself.

In the next few paragraphs, we’re going to do just that: write a Silverlight enabled cache provider.

What we want to end up with is:

  • A way to add items to your cache
  • A way to make sure these items expire after a while
  • A way to make sure these items are automatically refetched when needed
  • … and it should be easy to use

We’ll start out by defining the contracts. These contracts can be pretty simple – most of the work should be done in the specific implementations, which can vary depending on how you want to fetch the data (WCF, WCF RIA Services, …) and how you want to save the data (in memory, in Isolated Storage, …).

To start, we’ll need an ICache contract, which is our cache store.  In the contract, we define a CacheStoreKey, a Put method and a Get method.  Important to notice is this Get method should have a callback, as we’re working async if the item data isn’t loaded yet. 

public interface ICache
{
    void Put(ICacheItem cacheItem);
    void Get(string key, EventHandler<GenericEventArgs<ICacheItem>> callback);
    string CacheStoreKey { get;}
}

Next, we’ll need an ICacheItem contract, which should have a key, a function used to check if the data is available (and, eventually, fill the item with data) with a callback (used to pass through the Get method callback from the cache store), and a timespan which tells us how long the item is valid in cache.

public interface ICacheItem
{
    void CheckAndFillOrUpdate(EventHandler<GenericEventArgs<ICacheItem>> callback);
    string Key { get; }
    TimeSpan ExpiresAfter { get; }
}

The specific implementation of the ICache contract will provide the cache dictionary used to keep the items.  This results in the following code:

public class InMemCache : ICache
{
    Dictionary<string, ICacheItem> CacheDictionary = new Dictionary<string, ICacheItem>();
    public InMemCache(string cacheStoreKey)
    {
        _cacheStoreKey = cacheStoreKey;
        CacheDictionary = new Dictionary<string, ICacheItem>();
    }
    public void Put(ICacheItem cacheItem)
    {
        CacheDictionary.Add(cacheItem.Key, cacheItem);
    }
    public void Get(string key, EventHandler<GenericEventArgs<ICacheItem>> callback)
    {
        if (CacheDictionary.ContainsKey(key))
        {
            var currentItem = CacheDictionary[key];
            currentItem.CheckAndFillOrUpdate(callback);
        }
    }
    private string _cacheStoreKey;
    public string CacheStoreKey
    {
        get
        {
            return _cacheStoreKey;
        }
    }
}

The idea is that putting an item in cache will not fetch it immediately: putting an item in cache is equal to defining how it will be fetched.

The ICacheItem implementation is a bit more complex.  For this example, we’ll assume we’re working with WCF RIA Services, as this is the framework of choice often used in Silverlight applications. This means our implementation should be able to accept a function to load our items (which accepts an EntityQuery returns a LoadOperation) and the EntityQuery itself.  You could probably simplify this somewhat with reflection, but as we’re writing a cache provider performance is important, and using reflection is typically quite resource intensive.  Although you could argue that, to simplify the use of this provider, using reflection can be justified – it’s up to you.

This is how the implementation of ICacheItem looks:

public class InMemCacheItem<T, S> : ICacheItem where S : Entity
{
    public T Value;
    public DateTime? LastCheck;
    public Func<EntityQuery<S>, LoadOperation<S>> FillAndUpdateFunction { get; set; }
    public EntityQuery<S> EntityQuery { get; set; }
    private string _key;
    public string Key
    {
        get
        {
            return _key;
        }
    }
    private TimeSpan _expiresAfter;
    public TimeSpan ExpiresAfter
    {
        get
        {
            return _expiresAfter;
        }
    }
    public InMemCacheItem(string key, TimeSpan expiresAfter,
        Func<EntityQuery<S>, LoadOperation<S>> fillAndUpdateFunction,
        EntityQuery<S> entityQuery)
    {
        this._key = key;
        this._expiresAfter = expiresAfter;
        this.FillAndUpdateFunction = fillAndUpdateFunction;
        this.EntityQuery = entityQuery;
    }
    public void CheckAndFillOrUpdate(EventHandler<GenericEventArgs<ICacheItem>> callback)
    {
        if ((LastCheck == null) || (DateTime.Now.Subtract(ExpiresAfter) > LastCheck))
        {
            // refetch
            var lo = FillAndUpdateFunction.Invoke(EntityQuery);
            EventHandler handler = null;
            handler += (send, args) =>
            {
                lo.Completed -= handler;
                this.Value = (T)lo.Entities;
                LastCheck = DateTime.Now;
                // exec callback
                callback.Invoke(this, new GenericEventArgs<ICacheItem>(this));
            };
            lo.Completed += handler;
        }
        else
        {
            // the data is in cache and isn't expired, just execute the callback
            callback.Invoke(this, new GenericEventArgs<ICacheItem>(this));
        }
    }
}

All the pieces are in place, we can start using this provider.  When you put an item in cache, you tell it how it should fetch its data:

CacheProvider.Put(
    new InMemCacheItem<IEnumerable<CityForRIA>, CityForRIA>(
        "Cities"
        , TimeSpan.FromSeconds(100)
        , (query) => {
        return (LoadOperation<CityForRIA>)LocalStateContainer.CityDomainContext.Load(query);
        }
        , LocalStateContainer.CityDomainContext.GetCitiesForClientCacheQuery()
    ));

As said, what we’ve done here is the initialization of a cache item: this is not the data fetch itself, it just defines how data should be fetched when needed.

Fetching the data is done with the Get method on the cache store:

internal void GetWithCacheProvider()
{
    // get from cache
    CacheProvider.Get("Cities", 
         (send, args)
            =>
        {
            // the callback
            CitiesFromRIAServices = new ObservableCollection<CityForRIA>(
                ((InMemCacheItem<IEnumerable<CityForRIA>, CityForRIA>)args.Value).Value);
        });
}

What will happen is: if the data for an item in the dictionary is available and hasn’t expired it will come from that item’s value property: the callback will be executed immediately. If that isn’t the case, the data will be fetched using the method we’ve provided on initialization. After the data has been fetched, the Value property will be set, and the callback from the Get method we’ve provided will be executed.

With these few classes, we’ve now got a basic Silverlight cache provider.  Due to the fact that we’re working on interfaces, not specific implementations of these interfaces, you can easily work from this example to create your own provider and extend it to your liking. Like that, you could serialize items to and from Isolated Storage next to using the Dictionary. You could enable multiple Cache stores at the same time. You could make sure your items are saved between different Silverlight sessions, or even shareable between different Silverlight sites by using the caching techniques from part 2 – it’s all up to you and depends on the requirements of your application.

 

Conclusion

In the last part of these article series, we’ve seen how we can cache server side, and we’ve brought all the previous techniques together by writing an extensible Silverlight cache provider.  You should now have a good understanding on what caching in Silverlight means, and how you can profit from it in your applications.

I hope you’ve enjoyed the article series– I know I enjoyed writing it :-)

 

About the author

Kevin Dockx lives in Belgium and works at RealDolmen, one of Belgium's biggest ICT companies, where he is a technical specialist/project leader on .NET web applications, mainly Silverlight, and a solution manager for Rich Applications (Silverlight, Windows Phone 7 Series, WPF, Surface). His main focus lies on all things Silverlight, but he still keeps an eye on the new developments concerning other products from the Microsoft .NET (Web) Stack. As a Silverlight enthusiast, he's a regular speaker on various national and international events, like Microsoft DevDays in The Netherlands, Microsoft Techdays in Portugal or on BESUG events (the Belgian Silverlight User Group). Next to that, he also authored a best-selling Silverlight book, Packt Publishing's Silverlight 4 Data and Services Cookbook, together with Gill Cleeren. His blog, which contains various tidbits on Silverlight, .NET, and the occasional rambling, can be found at http://blog.kevindockx.com/.


Subscribe

Comments

  • -_-

    RE: Caching of, in, and around your Silverlight application (part 3)


    posted by Nguyen Thanh Tung on Jan 07, 2011 12:14

    Thanks Kevin, great article.

     This's my need.

  • jsneeringer

    Re: Caching of, in, and around your Silverlight application (part 3)


    posted by jsneeringer on Dec 08, 2011 03:07

    Thanks Kevin.  Great articles. I've been looking for something like this.

    A question:  Is there a way to cache result sets on the client side without caching all of them?  A DataPager makes a good example.  If there is a lot of data in the table on the server, the DataPager will fetch only what it needs to display, right?  There might not be room client-side to store all the data, but if the user goes back and forth among the pages, it would be better not to go back to the server each time.  Therefore it would be nice to cache a few items with an LRU algorithm.

    Any thoughts?

  • KevinDockx

    Re: Caching of, in, and around your Silverlight application (part 3)


    posted by KevinDockx on Dec 08, 2011 11:50

    Hello jsneeringer,

    interesting problem :-)  You might want to look into WCF RIA Services for this, as this is supported by that framework through the OutputCache attribute: this allows you to specify the location of the cache.  If you set this to "client", the server-side method will not be hit the second time you call it.  

    There's one issue with this though: OutputCache is disabled for queries that specify additional LINQ queries to the EntityQuery - so you cannot use this when you're extending the query from the client (you can, however, use this with additional parameters on your service method).  

    As far as caching algoritms like LRU are concerned, I'm not aware of any implementations of this out-of-the-box in SL, so that'll require some work...

  • jsneeringer

    Re: Caching of, in, and around your Silverlight application (part 3)


    posted by jsneeringer on Dec 08, 2011 16:49

    Thanks so much!  I'll look into OutputCache.

  • PramodHegde

    Re: Caching of, in, and around your Silverlight application (part 3)


    posted by PramodHegde on Sep 11, 2012 11:28

    Hi Kevin,
                 I am working on Windows 8 native app. I get the data from atom feeds and populate it in the Page. I have a navigational service using which I navigate from one page to the other. I have to cache these Pages, so that if I navigate to the page which I previously visited, then the page should get the cached data. Would CacheProvider class help in this regard?

Add Comment

Login to comment:
  *      *       

From this series