Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

How to use data caching in a .NET Windows Forms application

0.00/5 (No votes)
8 Jul 2004 1  
This article explains how using caching within your Windows Forms application can speed up data access for frequently used data and avoid performance bottlenecks over the network.

Sample image

Sample image

Introduction

Recently, on a large corporate application which uses a front end GUI, middle tier comprising webservices and a database layer, we were encountering slowdowns in the application when using slower network connections, i.e., 256k downwards. While profiling, it was discovered that some of the data we were bringing back repeatedly need not be and so it was decided to investigate using caching of data locally in order to speed this process up.

While there is much information relating to utilizing caching for ASP.NET web applications, I could not find any relating to WinForms applications. During my searches on newsgroups, C# sites etc., I began to work out how to implement caching in a .NET application, and came up with the project that accompanies this article. Considering that I found this interesting, and I'm sure I'm not the only person trying to achieve this, I've placed the project, code and information gathered here for all to make use of.

Bear in mind that I've only been using the .NET framework and C# since Dec 2003 (this is now July 2004), you may find architectural errors - if so, please inform me so that I can:

  1. update the article and
  2. improve my knowledge :)

This is my 1st article, I hope it meets your approval....

Background

The following resources were used while investigating implementing caching (in no particular order):

The last item, 'Caching Architecture Guide for .NET Framework Applications' is an absolute must read if you want to delve deeper into the understanding and concepts of caching.

Using the code

Outline

This project achieves the following:

  • Implements a basic webservice to talk to the Northwind database via a small data access DLL and display the results in a DataGrid in a WinForm.
  • The 1st time the data is requested, the time in ticks to retrieve this data from the DB is noted and the cache is built.
  • Any subsequent calls to request the data will result in being pulled from the cache.
  • If the cache timeout has expired then the next call to refresh the data will obtain it from the DB, rebuilding the cache again.
  • If any data is changed within the Customers table, the cache will be notified, expired, and refreshed, resulting in the new data being displayed in the grid.

NameSpaces

In order to provide access to the Cache object, we need to declare a couple of namespaces.

  • System.Web - allows us to use the HttpRuntime class which 'Provides a set of ASP.NET run-time services for the current application' (courtesy of MSDN).

    and

  • System.Web.Caching - allows us to use the CacheItemRemovedCallback delegate which enables the cache to notify the application of any changes.

The code

I'm presuming that you already understand the basics on how to connect to a database, creating webservices, WinForms etc., so I'll just cut straight to the chase and explain the caching code. If anyone wants me to expand on this, I'll add it at a later date.

In order to use the Cache object in a WinForms app, we need to create an instance of this Cache. In ASP.NET applications, we get it for free and can simply call:

Cache.Add(Cache.Add("Key1", "Value 1", null, DateTime.Now.AddSeconds(60), 
TimeSpan.Zero, CacheItemPriority.High, onRemove)

However, in a WinForms app, we have no context of this, so we need to create one. To do this, we use the HttpRuntime class in the System.Web namespace. We also need to implement a FileWatcher object (discussed more later).

In our app, we do all this in the Form_Load event.

// Create our cache object and file monitor object

private void Form1_Load(object sender, System.EventArgs e)
{
    HttpRuntime httpRT = new HttpRuntime(); // our cache object


    // Create our filewatcher class

    FileWatcherClass fd = new FileWatcherClass(@"c:\cust_changed.txt"); 
    // txt file to monitor for changes, would be created by db when data changed


    fd.OnFileChange += new 
      WindowsApplication1.FileWatcherClass.FileChange(this.FileHasChanged);
}

FileWatcherClass

This class is defined within FileWatcherClass.cs and is directly ripped out of the Microsoft 'Caching Architecture Guide for .NET Framework Applications'. It takes one parameter in the constructor which is the file to monitor. Note that this file must already exist. If the watcher class detects any changes to this file, a delegate is fired, which in our case, clears the old cache and rebuilds it again with fresh new data.

Cache object

Once the form is loaded, clicking on the 'Load' button will run the following block of code:

if(DataCacheGrid != null) // do we have any cached data?

{
    if(!GetCached()) // apparently we do so get it

        RefreshData(); // oops it's old so refresh the dataset

}
else
    RefreshData(); // just refresh the set


// display the data

dataGrid1.DataSource = DataCacheGrid; // our grid

We have a member variable DataCacheGrid declared which is used to hold our data returned from the webservice. If this happens to be null, then we call the RefreshData() method to do a direct call to the DB, this in turn does the following:

  • Creates an OnRemove event handler to inform the application when the cache is expired.
  • Connects to the webservice and gets a copy of the data.
  • Adds this data to the Cache object and sets a time limit of 2 minutes to it.
// Builds our dataset by calling the webservice,

// this is the only place that

// the web service is called from

private void RefreshData()
{
    // our expiry handler

    onRemove = new CacheItemRemovedCallback(this.RemovedCallback);
    
    // connect to the webservice and get the data

    WebServicecache.Service1 ws = new WebServicecache.Service1();
    DataCacheGrid = ws.GetDBData();
    
    // add the data to the cache, setting an expiry time of two mins

    HttpRuntime.Cache.Add("DataGridCache", DataCacheGrid, 
        null, DateTime.Now.AddMinutes(2), TimeSpan.Zero, 
        CacheItemPriority.High, onRemove);
}

We need to explain adding to the Cache a little more. Delving into the MSDN helps reveal the following about it:

[C#]
public object Add(
   string key,
   object value,
   CacheDependency dependencies,
   DateTime absoluteExpiration,
   TimeSpan slidingExpiration,
   CacheItemPriority priority,
   CacheItemRemovedCallback onRemoveCallback
);

Parameters:

  • key

    The cache key used to reference the item.

  • value

    The item to be added to the cache.

  • dependencies

    The file or cache key dependencies for the item. When any dependency changes, the object becomes invalid and is removed from the cache. If there are no dependencies, this parameter contains a null reference (Nothing in Visual Basic).

  • absoluteExpiration

    The time at which the added object expires and is removed from the cache.

  • slidingExpiration

    The interval between the time the added object was last accessed and when that object expires. If this value is the equivalent of 20 minutes, the object expires and is removed from the cache 20 minutes after it is last accessed.

  • priority

    The relative cost of the object, as expressed by the CacheItemPriority enumeration. The cache uses this value when it evicts objects; objects with a lower cost are removed from the cache before objects with a higher cost.

  • onRemoveCallback

    A delegate that, if provided, is called when an object is removed from the cache. You can use this to notify applications when their objects are deleted from the cache.

As the 1st time we hit the 'Load' button, we are obtaining the data from the DB, you will notice the amount of ticks used to perform this process. Now, click on the button again, and this time you will notice how much faster it is as you are now retrieving from the cache rather than passing through the web service and onto the DB.

Obviously, at this point, we are calling the GetCached() method:

// This method simply returns the cached data,

// if we happen to be null thanks to our

// RemovedCallBack, then we return false which tells

// the calling method to do a full refresh from the db

private bool GetCached()
{
    DataCacheGrid = (DataSet)HttpRuntime.Cache.Get("DataGridCache");
    
    if(DataCacheGrid == null)
        return false;
    else
        return true;
}

As you can see, to get a copy of the data within the Cache, you simply cast the type into your object (in our case, a DataSet into DataCacheGrid) and call Cache.Get(NAME_OF_CACHE).

I addition, I've added a small check to detect if our Cache happens to be null. This can happen as we have declared an OnRemove RemovedCallback event which will nullify the cache once the time has expired. This could happen as we call into the GetCached() method. If the return result of this method is false then we obtain a new copy from the DB and rebuild the cache.

That's it, well nearly.

OK, now you can see how to implement caching into your app and expire it at a specified interval. Only one problem, what happens if the data in the DB changes after we've cached it locally - how can you ensure you get the latest copy of it?

Well thankfully, I've also thought of this :). Basically there are a few methods of achieving this, all are described in the MS caching document described earlier on. I considered using two of the methods, SQL Notifications and File Notifications.

SQL Server Notification Services is an add-on for SQL Server which allows all manner of interesting notification implementations. I found it was quite tricky to setup, and was not suitable for our needs as it ties you in deeply to SQL Server. For this reason, I decided not to pursue it any further.

File Notifications is another matter though. This involves using triggers and stored procs on your database tables to write/update a file (text file etc.) whenever any changes have been made to it. This was ideal for me and is why the FileWatcher class is being used.

Basically, we are monitoring for any changes in a file called c:\cust_changed.txt. This is created or updated whenever you modify the contents of your Customers table in the Northwind database, well it is once you add the following trigger and stored proc to it!

Add the following stored procedure to the Northwind database

CREATE PROCEDURE dbo.uspWriteToFile
@FilePath as VARCHAR(255),
@DataToWrite as TEXT
-- @DataToWrite as VARCHAR(8000)

AS
SET NOCOUNT ON
DECLARE @RetCode int , @FileSystem int , @FileHandle int

EXECUTE @RetCode = sp_OACreate 'Scripting.FileSystemObject' , @FileSystem OUTPUT
IF (@@ERROR|@RetCode > 0 Or @FileSystem < 0)
RAISERROR ('could not create FileSystemObject',16,1)

EXECUTE @RetCode = sp_OAMethod @FileSystem , 'OpenTextFile' , 
                               @FileHandle OUTPUT , @FilePath, 2, 1
IF (@@ERROR|@RetCode > 0 Or @FileHandle < 0)
RAISERROR ('Could not open File.',16,1)

EXECUTE @RetCode = sp_OAMethod @FileHandle , 'Write' , NULL , @DataToWrite
IF (@@ERROR|@RetCode > 0)
RAISERROR ('Could not write to file ',16,1)

EXECUTE @RetCode = sp_OAMethod @FileHandle , 'Close' , NULL
IF (@@ERROR|@RetCode > 0)
RAISERROR ('Could not close file',16,1)

EXEC sp_OADestroy @FileSystem
RETURN( @FileHandle )
ErrorHandler:
EXEC sp_OADestroy @FileSystem
RAISERROR ('could not create FileSystemObject',16,1)
RETURN(-1)
GO

Add the following trigger to the Northwind database

CREATE TRIGGER [CustomersTableChangedTrig] ON [dbo].[Customers] 
FOR INSERT, UPDATE, DELETE 
AS EXEC .uspWriteToFile 'c:\cust_changed.txt', 'Customers table updated'

To test this works:

  • Create a blank file in c:\ called cust_changed.txt.
  • Run the application.
  • Click on the Load button to load the data from the DB. You can then click on it again and the data will now come from the cache.
  • Modify some data within the Customers table using Query Analyzer etc., the DataGrid should instantly update itself.

Points of Interest

Couple of interesting points to note here:

  1. As the HttpRuntime.Cache object is declared as a static, once it's created in the application, it's available application wide.
  2. As the FileWatcher class is running in a separate thread, you cannot access the RefreshData() method directly. Instead, you must declare a delegate and use BeginInvoke on that delegate to perform your updates.

That's all, I hope this helps you to understand caching a little more than at the beginning of the article. If there is any feedback good or bad, please let me know.

History

  • July 2004 - Initial version released to CodeProject - preparing oneself for oncoming flack :)

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here