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:
- update the article and
- 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.
private void Form1_Load(object sender, System.EventArgs e)
{
HttpRuntime httpRT = new HttpRuntime();
FileWatcherClass fd = new FileWatcherClass(@"c:\cust_changed.txt");
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)
{
if(!GetCached())
RefreshData();
}
else
RefreshData();
dataGrid1.DataSource = DataCacheGrid;
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.
private void RefreshData()
{
onRemove = new CacheItemRemovedCallback(this.RemovedCallback);
WebServicecache.Service1 ws = new WebServicecache.Service1();
DataCacheGrid = ws.GetDBData();
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:
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
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:
- As the
HttpRuntime.Cache
object is declared as a static, once it's created in the application, it's available application wide.
- 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 :)