Click here to Skip to main content
15,867,568 members
Articles / Web Development / IIS
Article

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

Rate me:
Please Sign up or sign in to vote.
4.88/5 (17 votes)
8 Jul 2004CPOL8 min read 145.2K   3.6K   73   21
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:

C#
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.

C#
// 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:

C#
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.
C#
// 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#
[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:

C#
// 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

SQL
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

SQL
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
CEO
United Kingdom United Kingdom
I've been coding since approx 1993, starting with C/C++ then soon moving over to Delphi and C++ Builder.

I'm now coding exclusively in VS .NET using C# & have been using this since December 2003.

My interest areas are women, low level/system information/undoc functions, especially network related coding but not specifically in that order Smile | :)

I live in the UK just outside Manchester. Other hobbies include motorcycling, racing, marine fish keeping, anything mechanical etc

Comments and Discussions

 
QuestionExceptional! Pin
Hifni Shahzard20-Mar-14 0:41
Hifni Shahzard20-Mar-14 0:41 
QuestionHow to access the file monitored by filewatcher class Pin
Shouvik2622-Aug-12 2:56
Shouvik2622-Aug-12 2:56 
QuestionError: Control Label2 accessed from a thread other than the thread it was created on Pin
Khadim Ali12-Jul-09 5:41
Khadim Ali12-Jul-09 5:41 
AnswerRe: Error: Control Label2 accessed from a thread other than the thread it was created on Pin
Khadim Ali12-Jul-09 5:58
Khadim Ali12-Jul-09 5:58 
General"Caching Application Block" of the Microsot Enterprise Library Pin
Uwe Keim8-Apr-07 10:06
sitebuilderUwe Keim8-Apr-07 10:06 
GeneralA useful option Pin
Iqbal M Khan15-Mar-07 1:31
Iqbal M Khan15-Mar-07 1:31 
GeneralRe: A useful option Pin
Simon Steed (SimonAntony)15-Mar-07 8:26
Simon Steed (SimonAntony)15-Mar-07 8:26 
GeneralRe: A useful option Pin
RichardM118-Feb-10 15:56
RichardM118-Feb-10 15:56 
GeneralRead from ASP.NET Pin
ryan86753095-Dec-06 5:47
ryan86753095-Dec-06 5:47 
GeneralStored Procedure uspWriteToFile error Pin
jayshah1121-Mar-06 4:12
jayshah1121-Mar-06 4:12 
GeneralRe: Stored Procedure uspWriteToFile error Pin
Simon Steed (SimonAntony)21-Mar-06 7:49
Simon Steed (SimonAntony)21-Mar-06 7:49 
GeneralRe: Stored Procedure uspWriteToFile error Pin
jayshah1121-Mar-06 9:17
jayshah1121-Mar-06 9:17 
QuestionWhy do you use Cache in your example? Pin
changoh9-Aug-04 7:00
changoh9-Aug-04 7:00 
AnswerRe: Why do you use Cache in your example? Pin
lievin10-Aug-04 6:05
lievin10-Aug-04 6:05 
GeneralRe: Why do you use Cache in your example? Pin
Simon Steed (SimonAntony)10-Aug-04 9:13
Simon Steed (SimonAntony)10-Aug-04 9:13 
GeneralRe: Why do you use Cache in your example? Pin
CrashDome7-Apr-06 5:52
CrashDome7-Apr-06 5:52 
GeneralRe: Why do you use Cache in your example? Pin
Lynn Cabanski7-Dec-06 5:13
Lynn Cabanski7-Dec-06 5:13 
GeneralRe: Why do you use Cache in your example? Pin
JR196520-Sep-17 1:47
JR196520-Sep-17 1:47 
GeneralBroken Images Pin
Gary Thom9-Jul-04 3:46
Gary Thom9-Jul-04 3:46 
GeneralRe: Broken Images Pin
Simon Steed (SimonAntony)9-Jul-04 4:54
Simon Steed (SimonAntony)9-Jul-04 4:54 
Thanks Gary,

Hopefully the images are fixed now but struggling with the out of bound data, still getting used to the codeproject submission process & when the site running slow, is hard work Smile | :)

Cheers

Si Blush | :O
GeneralRe: Broken Images Pin
Gary Thom9-Jul-04 5:06
Gary Thom9-Jul-04 5:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.