Introduction
The .NET Framework provides the concept of 'application configuration file' to store application settings. The config file is a regular XML file and need to be shipped with the application. Since this is a simple XML file, users can make changes to the configuration data using any simple text editor, without the need to recompile the application.
Limitations of Application Configuration File
Config files are very handy and easy to use. But there are a few limitations:
- This is a read only mechanism. You cannot use the .NET
ConfigurationSettings
class to update the app.config file.
Sicne the config file is a simple XML file, you can write your own class to update the data from your application. But in Windows environment, until you restart the application, the changes to the config file will not be recognized by the application. In web applications, if you update the web.config, the ASP.NET worker thread will be recycled.
- Security threat - The config file is accessible to everyone who has access to the file system. It is possible that the user may edit the config file manually and mess up with the content or provide invalid settings.
Alternate approaches
- Registry
- File
- Isolated Storage
- Serialize Custom classes
Each of the above approaches have advantages and disadvantages.
Writing into registry may need more permissions than otherwise required by the application. Also, it is possible that any other application can accidentally or deliberately overwrite the registry data.
Writing into File also requires specific permissions. Also, the question of where to store the file, how to secure it etc. raises.
What is Isolated Storage?
.NET introduces a concept called Isolated Storage. Isolated Storage is a kind of Virtual Folder. Users never need to know where exactly the file is stored. All you do is, tell the .NET framework to store your file in Isolated Storage. The physical location of Isolated Storage varies for each Operating System. But your application simply uses the .NET classes to create and access files, without bothering where it is physically located. And you can have Isolated Storage specific to each Assembly or each Windows user. Since Isolated Storage can be specific to each user or assembly, you don't need to worry about other users accidentally changing your files.
Persisting Application Data - Overview
In this article, I will demonstrate a custom class that can be used to easily store and retrieve application data. This custom class uses various features including hashtable, serialization and isolated storage to manipulate and store application data. The application data is stored in a hashtable as Key-Value pairs. Hashtable supports inserting any kind of objects into it. So this class, which is derived from HashTable
, will support saving any object into the persistent storage. When the Save()
method is called, the hashtable data will be serialized into Isolated Storage. In the constructor of the class, it will load data by deserializing itself from the data stored in Isolated Storage.
Concepts
I have written a custom class to store and retrieve custom application data in an easy and reliable manner, combining the above approaches. My approach makes use of the following .NET Framework concepts:
HashTable
- used to store data as key value pairs.
- Serialization - to persist the
HashTable
into the file system.
- Isolated Storage - to protect data from other users and assemblies.
Code Sample
using System;
using System.Collections;
using System.IO;
using System.IO.IsolatedStorage;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
namespace CustomStorage
{
[Serializable]
public class ApplicationStorage : Hashtable
{
private string settingsFileName =
System.Reflection.Assembly.GetEntryAssembly().GetName().Name + ".dat";
public ApplicationStorage()
{
LoadData();
}
protected ApplicationStorage (SerializationInfo info,
StreamingContext context): base(info, context)
{
}
private void LoadData()
{
IsolatedStorageFile isoStore =
IsolatedStorageFile.GetStore( IsolatedStorageScope.User
| IsolatedStorageScope.Assembly, null, null );
if ( isoStore.GetFileNames( settingsFileName ).Length == 0 )
{
return;
}
Stream stream = new
IsolatedStorageFileStream(settingsFileName,
FileMode.OpenOrCreate, isoStore );
if ( stream != null )
{
try
{
IFormatter formatter = new BinaryFormatter();
Hashtable appData = ( Hashtable ) formatter.Deserialize(stream);
IDictionaryEnumerator enumerator = appData.GetEnumerator();
while ( enumerator.MoveNext() )
{
this[enumerator.Key] = enumerator.Value;
}
}
finally
{
stream.Close();
}
}
}
public void ReLoad()
{
LoadData();
}
public void Save()
{
IsolatedStorageFile isoStore =
IsolatedStorageFile.GetStore( IsolatedStorageScope.User
| IsolatedStorageScope.Assembly, null, null );
Stream stream = new
IsolatedStorageFileStream(settingsFileName,
FileMode.Create, isoStore );
if ( stream != null )
{
try
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize( stream, (Hashtable)this );
}
finally
{
stream.Close();
}
}
}
}
}
Let us analyze the code now.
public class ApplicationStorage : Hashtable
The class definition shows that it inherits the HashTable
. So, you can use all features of a HashTable
. This will help add and remove key value pairs easily. Also, the HashTable
provides a very efficient mechanism to handle objects.
Constructor:
public ApplicationStorage()
{
LoadData();
}
Inside the constructor, we will call the LoadData()
method. When this class is instantiated, it will load the previously stored data into its HashTable
. Let us see the implementation of the method LoadData()
.
private void LoadData()
{
IsolatedStorageFile isoStore =
IsolatedStorageFile.GetStore( IsolatedStorageScope.User |
IsolatedStorageScope.Assembly, null, null );
if ( isoStore.GetFileNames( settingsFileName ).Length == 0 )
{
return;
}
Stream stream = new IsolatedStorageFileStream(settingsFileName,
FileMode.OpenOrCreate, isoStore );
if ( stream != null )
{
try
{
IFormatter formatter = new BinaryFormatter();
Hashtable appData = ( Hashtable ) formatter.Deserialize(stream);
IDictionaryEnumerator enumerator = appData.GetEnumerator();
while ( enumerator.MoveNext() )
{
this[enumerator.Key] = enumerator.Value;
}
}
finally
{
stream.Close();
}
}
}
We are using the IsolatedStorageFile
class provided by the .NET Framework to get access to the 'Store'. The parameters specify that this store is specific to the 'current Windows user' and also to the 'current assembly'. Next, we are checking if the file we are looking for already exists in store. If it doesn't exists, we do not process it.
In the next few steps, we will read data from the stream and deserialize a HashTable
. After deserializing the HashTable
, we will enumerate through the entries and assign to the base class HashTable
.
In short, we are loading the hashtable by deserializing the previously serialized hashtable data in isolated storage.
Next, let us see the Save()
method. As the name indicates, it saves the data in HashTable
into the Isolated Storage.
public void Save()
{
IsolatedStorageFile isoStore =
IsolatedStorageFile.GetStore( IsolatedStorageScope.User |
IsolatedStorageScope.Assembly, null, null );
Stream stream = new
IsolatedStorageFileStream( settingsFileName,
FileMode.Create, isoStore );
if ( stream != null )
{
try
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize( stream, (Hashtable)this );
}
finally
{
stream.Close();
}
}
}
In the first step, we are opening the 'Store' in Isolated Storage. The parameters specify that this store is specific to the 'current Windows user' and also to the 'current assembly'. Next, we create a file in this store. The parameter FileMode.Create
will ensure that the file will be created if it doesn't exist.
In the next step, we will deserialize the HashTable
entries into the stream. This will save all our data in the HashTable
into the Isolated Storage, which can be later deserialized in the LoadData()
method.
Let us see some sample code to consume this little handy tool.
CustomStorage.ApplicationStorage storage =
new CustomStorage.ApplicationStorage();
storage["name"] = "john";
storage["age"] = 23;
storage["address"] = "#10, Vasant Nagar";
storage.Save();
In the above sample code, we are creating an instance of our class ApplicationStorage
and adding key/value pairs to it. Since our class inherits the HashTable
, we can use all methods of HashTable
including Add(...)
, indexer (this)
, Remove()
, Clear()
etc. The Save()
method will serialize the data into Isolated Storage.
Since HashTable
supports any objects, you can use this class to store any kind of serializable objects.
The following sample code demonstrates how you can retrieve data.
CustomStorage.ApplicationStorage storage =
new CustomStorage.ApplicationStorage();
string name = storage["name"].ToString();
int age = int.parse(storage["age"].ToString());
string address = storage["address"].ToString();
When the class is instantiated, it will load the already saved data. Then you can use the indexer property of the HashTable
to retrieve all key/value pairs from it.
Disadvantages
Our class uses Isolated Storage and it has a few limitations.
First thing is, Administrator can set quota per user and per assembly. This will be a limitation and our utility will fail if it exceeds the limit.
Also, smart users can find the location of Isolated Storage. Even though it is a hidden location, it is possible that someone can locate it and remove or corrupt our files. So, it doesn't give very high security for data.