Caching Data Locally

As you saw from some of the chapters earlier in this book, RIA Services provides an easy and elegant means for obtaining data from and persisting data to a server. However, what if a connection to the server is suddenly not available? Do you want all the users of your application to be forced to wait until they are (or the server is) back online again? Or do you want them to be able to continue working (as much as possible) in the meantime until a connection to the server is restored?

This type of scenario is known as “occasionally connected applications” and is generally handled by downloading and caching commonly used data from the server locally that the users may require when they are offline. This cache will need to be regularly updated and hold enough data to enable the users to continue working until they get back online again. While the application is offline, it will need to cache the changes, additions, and deletions the user has made to the data and synchronize these back to the server when the application is next online.

images Note In addition, you may wish to cache data on the client that rarely or never changes to reduce the amount of network traffic that passes between the server and the client.

Let's take a look at some strategies for caching data on the client and catering for scenarios where the application may be going back and forth between connected and disconnected modes.

Caching to Isolated Storage

As a part of its sandbox, to protect your computer from rogue Silverlight applications, Silverlight doesn't allow you direct access to the user's file system unless you are running with elevated trust—discussed later in this chapter. However, it does provide you with a virtual file system known as isolated storage that enables you to cache data and files locally. As its name suggests, isolated storage is a virtual file system that is stored on disk but isolated from the computer's main file system and that of other applications. Files can be maintained in this store within directories in the same way as you normally organize files in a file system.

images Note An isolated storage store is not specific to the browser that the user is running the application within. The same store is used across browsers and when running outside the browser. However, each Windows user on the computer will have their own isolated storage store.

Isolated storage is partitioned into two sections: an application store and a site store. The application store can be used to store files available to your application only, while the site store can be used to store files available across Silverlight applications originating from the same domain. Each isolated storage store is given a size quota that can be increased, with the permission of the user, if required.

Let's take a look at the various ways you can persist and retrieve data from an isolated storage store.

Storing Settings in Isolated Storage

The easiest way to persist and retrieve data from an isolated storage store is by using the IsolatedStorageSettings class, which can be found under the System.IO.IsolatedStorage namespace. This class enables you to easily maintain a collection of settings in isolated storage as key/value pairs that can be maintained as a file in either of the two stores (application and site) discussed earlier. Instead of having to manage serializing and deserializing the settings to and from a file, this class abstracts that away from you so that you need to maintain the settings only as a collection of key/value pairs, and it will handle persisting and retrieving the collection to/from isolated storage for you.

The IsolatedStorageSettings.ApplicationSettings property provides a means to maintain settings in the application store, and the IsolatedStorageSetting.SiteSettings property provides a means to maintain settings in the site store. Both of these properties return an object of type IsolatedStorageSettings that is a wrapper around a Dictionary of type <string, object>. Both are used in exactly the same manner as a Dictionary (and hence each other), so we will focus on demonstrating maintaining application settings here.

images Note Apart from application settings, another perfect use for storing as settings is dictionary data (used to populate drop-down lists, etc). For dictionaries that rarely or never change, caching them in isolated storage will result in less network traffic needing to pass between the server and the client and a much more responsive application that doesn't need to wait to load these from the server.

Persisting a Value to Isolated Storage

Each setting is a key/value pair; the key needs to be a string, but the value can be any type of object. To persist a value to isolated storage, you simply need to assign it to the indexer of this class, using the key as its index:

IsolatedStorageSettings.ApplicationSettings["Key"] = "Value";

images Note You don't need to use the Add method when adding a new setting to the dictionary. Simply assigning the value as demonstrated will take care of this and save you the trouble of needing to determine whether the key exists in the dictionary and add/update it accordingly.

As mentioned, the IsolatedStorageSettings class is a Dictionary of type <string, object>, meaning that any type of object (not just strings) can be persisted as an isolated storage setting and that object will be automatically serialized to and deserialized from isolated storage.

The changes to the settings are maintained in memory and will be automatically saved to isolated storage when the application is closed. However, if the application were to crash before this time, the changes made would be lost. Therefore, it's recommended that you call the Save method of the IsolatedStorageSettings object to explicitly save the current settings collection to isolated storage after modifying the collection:

IsolatedStorageSettings.ApplicationSettings.Save();

images Note When you first try to access the settings, all the settings data will be loaded from isolated storage into memory and deserialized. The more data that your store in the settings, the longer this will take, and the more memory it will use. Therefore, store an excessive amount of data as settings in isolated storage is not recommended, because this will have an adverse effect on the performance of your application and potentially of the user's computer. An alternative is to serialize large collections/objects and persist them as individual files within the isolated storage store. This will enable them to be loaded only when required and disposed of (removing them from memory) once you no longer need them. We'll look at this idea later in this chapter.

Retrieving a Value from Isolated Storage

When you want to retrieve a setting from isolated storage, you will need to check that it exists first. You can use the Contains method to do this and then retrieve its value, if it exists, using its key as the index, like so:

if (IsolatedStorageSettings.ApplicationSettings.Contains("Key"))
{
    string value = IsolatedStorageSettings.ApplicationSettings["Value"].ToString();
    // Do something with the value
}

Alternatively, you can use the TryGetValue method that bundles checking that the setting exists and retrieving its value if it does into a single task. With the out parameter being a generic type, it will also save the need for casting the returned object:

string value;
bool settingExists =
    IsolatedStorageSettings.ApplicationSettings.TryGetValue("Key", out value);
Binding to Settings

You can bind to isolated storage settings using indexed property binding, as demonstrated in Chapter 11. The following XAML demonstrates binding to a setting named Setting1:

<TextBox Name="Setting1TextBox" Text="{Binding [Setting1], Mode=TwoWay}" />

Due to the non-creatable nature of the IsolatedStorageSettings class, as it has a private constructor, you will need to assign the DataContext of the control or of a control further up in the object hierarchy in the code behind, as follows:

Setting1TextBox.DataContext = IsolatedStorageSettings.ApplicationSettings;

images Note As a general rule, you are better off wrapping the settings in properties and exposing them from a class instead of attempting to bind to them directly.

Enumerating the Settings

If you want to enumerate through the existing settings, the Keys property will expose what keys exist in the store, and you can then get their values as required:

foreach (string key in IsolatedStorageSettings.ApplicationSettings.Keys)
{
    object value = IsolatedStorageSettings.ApplicationSettings[key];
    // Do something with the value
}
Removing Settings

You can remove a setting using the Remove method of the dictionary, passing it the key of the setting to remove:

IsolatedStorageSettings.ApplicationSettings.Remove("Key");

You can remove all the settings in a dictionary by calling its Clear method:

IsolatedStorageSettings.ApplicationSettings.Clear();

Storing Files in Isolated Storage

Isolated storage is a virtual file system, meaning that you can create directories and files within isolated storage in much the same way as you would with the operating system's file system. For example, you may wish to store images, documents, and even .xap files downloaded from the server (providing dynamically loaded content, discussed in Chapter 17) in isolated storage. Let's look at how to perform some of these tasks in an isolated storage store.

Opening an Isolated Storage Store

The first step is to open the store that you want to work with. To open the application store, you use the GetUserStoreForApplication method of the IsolatedStorageFile class. Alternatively, you will need to use the IsolatedStorageFile class's GetUserStoreForSite method to open the site store.

images Note For the purposes of this demonstration, you will be working with the application store, but working with the site store is performed in exactly the same manner after opening the site store instead.

The following code demonstrates opening the application store:

using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
{
    // Work with the files and directories in the store here
}

images Note As a general rule, you should use a using block, as demonstrated previously, to ensure that the store instance is disposed of properly once you are finished working with it.

Working with Directories

Once you have opened a store you can create, move, and delete directories using the methods on the object instance you have of the IsolatedStorageFile class. For example, you can create a directory named "Images" like so:

store.CreateDirectory("Images");

If you want to create a subdirectory named "Icons" within the "Images"  directory, you will need to include the full path to create, as follows:

store.CreateDirectory(@"ImagesIcons");

You can determine whether a directory exists using the DirectoryExists method:

bool dirExists = store.DirectoryExists("Images");

images Note You should attempt to keep the paths to your files as short as possible. The full path to the file, including the path to the isolated storage store on the disk, must be less than 260 characters. Exceeding this length will result in an exception being thrown. The number of characters available for your directory and file names will depend on what operating system the user is running, so as a general rule, it's best to keep the paths of the files within the isolated storage store to a maximum of 70 characters in length.

Enumerating Through Files and Directories

The IsolatedStorageFile class provides a method to enable you to enumerate through the files and directories within an isolated storage store. For example, you can obtain a string array containing the names of the directories using the GetDirectoryNames method, like so:

string[] directories = store.GetDirectoryNames();

Note that only the directories within the root of the store are returned in this example. To get a list of the subdirectories within a given directory, you need to pass in the path to that directory as a search pattern followed by a wildcard. The following example demonstrates getting the names of the directories within the "Images" folder:

string[] directories = store.GetDirectoryNames(@"Images*");

Similarly, you can get a list of the files in the isolated storage store using the GetFileNames method:

string[] fileNames = store.GetFileNames();

As with getting the subdirectories within a directory, to get the files within a directory, you will need to pass in the path to that directory as a search pattern followed by a wildcard:

string[] fileNames = store.GetFileNames(@"Images*");
Working with Files

Various methods are provided to work with files, such as creating, copying, moving, deleting, and opening a file. For example, you can determine whether a file exists using the FileExists method:

bool fileExists = store.FileExists("Test.txt");
Writing a Text File

The following code demonstrates writing a string to a text file in isolated storage:

using (IsolatedStorageFileStream fileStream =
    store.OpenFile("Test.txt", FileMode.Create))
{
    using (StreamWriter writer = new StreamWriter(fileStream))
    {
        writer.WriteLine("Text written from Silverlight");
        writer.Close();
    }
    fileStream.Close();
}

images Note You will need to add a using directive to the System.IO namespace for this and the following examples to work.

Writing a Binary File

The following code demonstrates writing a byte array (named fileContents) to a file in isolated storage:

using (IsolatedStorageFileStream fileStream =
    store.OpenFile("Test.jpg", FileMode.Create))
{
    fileStream.Write(fileContents, 0, fileContents.Length);
    fileStream.Close();
}
Reading a Text File

The following code demonstrates reading the contents of a text file from isolated storage into a string:

string fileContents = null;

using (IsolatedStorageFileStream fileStream =
    store.OpenFile("Test.txt", FileMode.Open))
{
    using (StreamReader reader = new StreamReader(fileStream))
    {
        fileContents = reader.ReadToEnd();
        reader.Close();
    }

    fileStream.Close();
}

// Do something with the file contents
Reading a Binary File

The following code demonstrates reading the contents of a binary file from isolated storage into a byte array:

byte[] fileContents = null;

using (IsolatedStorageFileStream fileStream =
    store.OpenFile("Test.jpg", FileMode.Open))
{
    fileContents = new byte[fileStream.Length];
    fileStream.Read(fileContents, 0, (int)fileStream.Length);
    fileStream.Close();
}

// Do something with the file contents
Serializing an Object to a File

Generally, you will be maintaining data in objects and collections within your application, and you may wish to persist this data to isolated storage. Rather than writing the data to a file using a custom format and then needing to parse it and rehydrate the corresponding objects or collections when you want to load the data again, you can simply serialize or deserialize the data to or from XML, using the DataContractSerializer class to help you.

Let's say you have a collection of Product objects that you want to serialize to a file within an

isolated storage store. The following code demonstrates serializing and writing a collection of Product objects to isolated storage:

using (IsolatedStorageFileStream fileStream =
    store.OpenFile("ProductCollection.xml", FileMode.Create))
{
    DataContractSerializer serializer =
            new DataContractSerializer(typeof(List<Product>));
    serializer.WriteObject(fileStream, productCollection);
}

images Note You will need to add a using directive to the System.Runtime.Serialization namespace for this and the following example to work.

Deserializing and Rehydrating an Object from File

The following code demonstrates deserializing a collection of Product objects read from a file in isolated storage, and rehydrating the collection:

using (IsolatedStorageFileStream fileStream =
    store.OpenFile("ProductCollection.xml", FileMode.Open))
{
    DataContractSerializer serializer =
        new DataContractSerializer(typeof(List<Product>));
    List<Product> productCollection =
        serializer.ReadObject(fileStream) as List<Product>;

    // Do something with the collection

    fileStream.Close();
}

Disk Space Quota

When a Silverlight application is first run, it is given a quota of 1MB of space to use in its isolated storage store. This quota is assigned on a per-domain basis, meaning that the quota is shared between all the Silverlight applications originating from the same domain, even though each application has its own separate isolated storage store. Attempting to exceed this quota will result in an exception being raised.

images Note Whenever you work with isolated storage, you should place the code within a try … catch block to handle this possibility. Even if your application only expects to use a very small amount of isolated storage space, it can still exceed the quota, as the quota is shared among all applications originating from the same domain. Another application from the same domain may have used most of or the entire quota already, which will affect the amount of space your application has left to use.

You can determine how much quota you have free by checking the AvailableFreeSpace property of the IsolatedStorageFile object. This value returns the number of bytes still available for use in the isolated storage store:

long freeSpace = store.AvailableFreeSpace;

You can also retrieve the total quota assigned to the domain via the store's Quota property, and retrieve the amount of quote already used via the store's UsedSize property.

When a Silverlight application is installed to run outside the browser, its quota will be automatically increased from the initial 1MB to 25MB. However, you can request additional space at any time if it is required. This request requires that the user provide permission for the domain to be assigned further quota by displaying a dialog, as shown in Figure 16-6.

images

Figure 16-6. The dialog for the application requesting increased isolated storage quota

To request additional quota, you can simply call the IncreaseQuotaTo method of the IsolatedStorageFile object, passing it the amount of space that you want (in bytes). For example, you can use the following code to request your quota to be increased to 50MB of isolated storage space:

bool increasePermitted = store.IncreaseQuotaTo(50 * 1024 * 1024);

The method returns a Boolean value indicating whether permission for the additional space has been granted or denied by the user. Requesting a smaller quota than that already granted will throw an ArgumentException exception stating that “The new quota must be larger than the old quota,” so be prepared for this scenario and put the call in a try … catch block, or make sure the value you are setting is greater than that of the store's Quota property.

images Note The call to the IncreaseQuotaTo method must originate from a user-initiated event. In other words, you can request additional isolated storage quota only in an event raised by some sort of user input, such as the Click event. Any attempt to request additional isolated storage quota in an event handler not initiated by user input, such as in response to the Loaded event of a control, will simply be denied. This rule applies only when the application is running in sandboxed mode. When the application is running with elevated trust permissions (described later in this chapter), this limitation is lifted.

Encrypting Data

Something to be aware of when writing data to isolated storage is that it is easily accessible by burrowing into the user's profile folders. The isolated storage store is maintained in a hidden folder with the exact same file/directory structure as the folders were created in the store, as demonstrated in Figure 16-7.

images

Figure 16-7. An isolated storage folder on disk

The location of the isolated storage stores on disk differs between operating systems, but in Windows Vista or 7 you can find the area by typing %userprofile%AppDataLocalLowMicrosoftSilverlightis into the Windows Explorer address bar. When you write files to isolated storage, you are simply writing them to a folder somewhere underneath this path.

As you can see in Figure 16-7, the path to the isolated storage store is somewhat obfuscated, but if you create a file in the isolated storage store with a unique name, you can find it fairly easily (and hence the isolated storage store) using the search functionality in Windows Explorer. You can then open the files, inspect their contents, and modify them if you wish.

The point you need to note from this example is that nothing you write to isolated storage should be considered secure, and thus you should not write sensitive data to isolated storage or assume that it can't be modified outside your application.

You may wish to consider encrypting your files to help prevent these sorts of issues if they are of concern to you. The following MSDN article should point you in the right direction for doing so: http://msdn.microsoft.com/en-au/library/dd153755.aspx.

Compressing Data

With a default quota of 1MB, depending on the type and number of files you want to cache in isolated storage, you may wish to consider attempting to make the most efficient use of the available space within isolated storage by compressing the files, saving the need to request additional quota from the user soon after creating the store.

Silverlight doesn't have any built-in compression routines, but you can use an open source library that provides this functionality instead. Two of the best options available to you are Silverlight ports of the SharpZipLib and DotNetZip projects. SharpZipLib has a GPL license, and DotNetZip has a less-restrictive Ms-PL license.

You can download the Silverlight port of the SharpZipLib project from http://slsharpziplib.codeplex.com. However, the DotNetZip project has no official port attempt (the original project can be found at http://dotnetzip.codeplex.com). If you download the Document Toolkit Extensions project however, you will find a Silverlight port of both projects in it that is available at http://documenttoolkit.codeplex.com.

Refer to the documentation of the project that you choose to use for details of how to use it.

The Microsoft Sync Framework

The Microsoft Sync Framework web site describes the framework as “a comprehensive synchronization platform that enables collaboration and offline access for applications, services, and devices with support for any data type, any data store, any transfer protocol, and any network topology.” In simpler terms, you can use it as a framework for synchronizing data between a Silverlight application and the server, providing a range of new possibilities for occasionally connected Silverlight applications. Data is maintained in a local store for use by the Silverlight application while it is offline, and changes in the data during the time offline will be synchronized back with the server once it's back online (and vice versa).

Unfortunately, at the time of this writing, the release of version 4 of the framework that enhanced support for offline clients has been postponed, with no potential release date set, because the team instead decided to focus its efforts on the SQL Azure Data Sync product. However, the team did release the Microsoft Sync Framework Toolkit, containing source code that enabled Silverlight and Windows Phone 7 clients, among others, to use version 2.1 of the Sync Framework. You can download the toolkit at http://code.msdn.microsoft.com/silverlight/Sync-Framework-Toolkit-4dc10f0e.

The home page for the Sync Framework on the Microsoft web site is http://msdn.microsoft.com/en-us/sync/default.aspx.

Caching to a Client-Side Database

Isolated storage provides a way for caching files on the client, but it is not a suitable means of caching data—it's certainly in no way comparable with the features that a database provides.

You could store a collection of objects in isolated storage as was demonstrated using the IsolatedStorageSettings class, but it's not a very efficient way of caching a large collection. When you first attempt to get an instance of the application or site settings object, all of the settings will be loaded from isolated storage into memory. To get a single object would require loading all the settings into memory, and to simply insert, update, or delete a setting would require all the settings to be loaded and then persisted back to isolated storage.

We also discussed and demonstrated serializing objects as files to isolated storage as well, but for large sets of data, this can be an inefficient means of maintaining the data also.

Unfortunately, Silverlight has no built-in client-side database engine to solve these problems, which can be an issue when it comes to implementing occasionally connected applications. There are a number of potential solutions however (of differing degrees of suitability), discussed briefly in the following sections.

Sterling

Sterling is a free, lightweight NoSQL object-oriented database engine, written by Jeremy Likness. This is probably your best option if your Silverlight application needs a client-side database. It stores the database in isolated storage but can use the local file system instead if the application is running with elevated trust. It supports keys and table indexes for fast querying of data and allows you to write queries using LINQ and can be used by Windows Phone 7 and .NET applications, in addition to Silverlight applications. The home page for the Sterling database is at www.sterlingdatabase.com, and you can download it (including its source code) from http://sterling.codeplex.com. Both sites have links to articles and documentation on how you can use the Sterling database engine in your application.

Silverlight Database

The Silverlight Database (SilverDB) engine also serializes the tables and their data to a file in isolated storage, but without the key and index support that Sterling provides. It is worth investigating if you want to provide a simple predefined means to persisting and retrieving data in isolated storage. You can get this library at http://silverdb.codeplex.com.

SQLite

SQLite is a popular database engine with a small footprint and an extremely permissive license that has been around for quite some time now. It is written in C and ported to many environments (the original project is here: www.sqlite.org). If you need a relational database, unlike the object-oriented database Sterling implements, you might like to consider using one of its Silverlight ports.

Noah Hart ported SQLite to C#. He named the engine C#-SQLite (http://code.google.com/p/csharp-sqlite), and it runs against the full .NET Framework but not Silverlight. This project was ported to Silverlight by Tim Anderson as a proof of concept (not in a production-ready state) and can be downloaded from www.itwriting.com/blog/1695-proof-of-concept-c-sqlite-running-in-silverlight.html.

A separate, more recent attempt has been made at porting SQLite to Windows Phone 7 and Silverlight, which you might like to also investigate (http://wp7sqlite.codeplex.com).

SQLite was a part of the now-deprecated Google Gears, and this provides another possibility for use as a database engine and store. You can create and store data in a SQLite database using the JavaScript API provided by Google Gears. An example of doing so is demonstrated at www.mojoportal.com/silverlight-google-gears-awesome-at-least-in-firefox.aspx. Note that since JavaScript is required to communicate with the database, it can be used only within the browser (that is, it will not be accessible to applications running outside the browser).

Sharp HSql

Craig Dunn produced a proof-of-concept port of the Sharp HSql database engine to Silverlight (that was itself a port of the HSQLDB database engine written for Java) and has made it available at http://conceptdev.blogspot.com/2009/07/sql-in-silverlight.html.

Commercial Options

As you can see from the descriptions of the previous database engines, apart from Sterling, none of the free database engines listed is considered production ready. However, you may wish to consider the following commercial offerings:

  • Perst is an open source database engine released under the GPL license, so it's not a commercial offering per se. However, for use in closed-source applications, you will need to buy a license, in which case it becomes a commercial offering. A single developer license for its Silverlight edition (at the time of this writing) is $495. You can purchase and download it from www.mcobject.com/perst.
  • Siaqodb is a database engine targeting a number of platforms. One of its key features is its Sync Framework provider, enabling client-side databases to be easily synchronized with a server-side database. A single developer license for its Silverlight edition (at the time of this writing) is $186. You can purchase and download it from http://siaqodb.com.
  • EffiProz Embedded C# Database is a database engine targeting both Silverlight and Windows Phone 7. A single developer license for its Silverlight edition (at the time of this writing) is $490. You can purchase and download it from www.effiproz.com/product_sl.aspx.

Communicating with Local Database Engines

When your application is running with elevated trust on Windows systems, you will have access to COM. You can take advantage of the ADO COM objects to interact with the various database engines installed on the user's machine, such as SQL Server, SQL Compact, and Microsoft Access. We'll look at how you can use the ADO COM objects from Silverlight later in this chapter.

Detecting Network Connection Availability

To effectively support occasionally connected scenarios, you need the ability to detect whether the application is online or offline and cater for the current state accordingly. You also need the ability to detect when this state changes (from offline to online) and start synchronizing cached data back with the server.

Silverlight has the built-in ability to detect network availability and any changes in it, and you can find the classes to help you do so under the System.Net.NetworkInformation namespace. You can use the NetworkInterface class to help determine whether a network connection is available, and you can use the NetworkChange class to notify you when this state changes.

For example, the following code determines if a network connection is available:

bool isNetworkAvailable = NetworkInterface.GetIsNetworkAvailable();

images Note The NetworkInterface.GetIsNetworkAvailable method may report that you have a connection available, but there's no guarantee that you actually have a connection to the Internet or that the server you want to communicate with is accessible. It simply reports whether or not there is a network adapter available that currently has an IP address; it won't if it has no network connection.

You can monitor for changes in network availability by handling the NetworkAddressChanged event of the NetworkChange class like so:

NetworkChange.NetworkAddressChanged += NetworkChange_NetworkAddressChanged;

private void NetworkChange_NetworkAddressChanged(object sender, EventArgs e)
{
    // Handle network state changed as required
}

images Note The NetworkAddressChanged event doesn't tell you whether or not a network connection is now available—merely that the IP address of a network adapter has changed. You will need to use it in conjunction with the NetworkInterface.GetIsNetworkAvailable method to determine the new network connection state.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset