One of the reasons why ClickOnce can be installed directly by the end users without the need for administrator assistance is because it is installed into a self-contained ecosystem that's separate from all other programs and, in general, isolated from the rest of the user's computer.
When we need to store data locally, we can run into security problems if we have not specified our application as a full trust application. In these situations, we can take advantage of isolated storage, which is a data storage mechanism that abstracts the actual location of the data on the hard drive, which remains unknown to both users and developers.
When we use isolated storage, the actual data compartment where the data is stored is generated from some aspects of each application so that it is unique. The data compartment contains one or more isolated storage files called stores, which reference where the actual data is stored. The amount of data that can be stored in each store can be limited by code in the application.
The actual physical location of the files will differ, depending upon the operating system running on the user's computer and whether the store has roaming enabled or not. For all operating systems since Vista, the location is in the hidden AppData folder in the user's personal user folder. Within this folder, it will either be found in the Local or Roaming folders, depending on the store's settings:
<SYSTEMDRIVE>\Users\<username>\AppData\Local <SYSTEMDRIVE>\Users\<username>\AppData\Roaming
We can store any type of file in isolated storage, but as an example, let's take a look at how we could utilize it to store text files. Let's first see the interface that we will use:
namespace CompanyName.ApplicationName.Managers.Interfaces
{
public interface IHardDriveManager
{
void SaveTextFile(string filePath, string fileContents);
string ReadTextFile(string filePath);
}
}
And now let's see the concrete implementation for the interface:
using CompanyName.ApplicationName.Managers.Interfaces;
using System.IO; using System.IO.IsolatedStorage; namespace CompanyName.ApplicationName.Managers { public class HardDriveManager : IHardDriveManager { private IsolatedStorageFile GetIsolatedStorageFile() { return IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain, null, null); } public void SaveTextFile(string filePath, string fileContents) { try { IsolatedStorageFile isolatedStorageFile = GetIsolatedStorageFile(); using (IsolatedStorageFileStream isolatedStorageFileStream = new IsolatedStorageFileStream(filePath, FileMode.OpenOrCreate, isolatedStorageFile)) { using (StreamWriter streamWriter = new StreamWriter(isolatedStorageFileStream)) { streamWriter.Write(fileContents); } } } catch { /*Log error*/ } } public string ReadTextFile(string filePath) { string fileContents = string.Empty; try { IsolatedStorageFile isolatedStorageFile = GetIsolatedStorageFile(); if (isolatedStorageFile.FileExists(filePath)) { using (IsolatedStorageFileStream isolatedStorageFileStream = new IsolatedStorageFileStream(filePath, FileMode.Open, isolatedStorageFile)) { using (StreamReader streamReader = new StreamReader(isolatedStorageFileStream)) { fileContents = streamReader.ReadToEnd(); } } } } catch { /*Log error*/ } return fileContents; } } }
As with the other manager classes, we declare the HardDriveManager class in the CompanyName.ApplicationName.Managers namespace. In the private GetIsolatedStorageFile method, we obtain the IsolatedStorageFile object that relates to the isolated storage store that we will save the user's data in by calling the GetStore method of the IsolatedStorageFile class.
This method has a number of overloads that enable us to specify the scope, application identity, evidence, and evidence types with which to generate the unique isolated storage file. In this example, we use the overload that takes the bitwise combination of the IsolatedStorageScope enumeration members and the domain and assembly evidence types, which we simply pass null for.
The scope input parameter here is interesting and requires some explanation. Isolated storage is always restricted to the user that was logged on and using the application when the store was created. However, it can also be restricted to the identity of the assembly, or to the assembly and application domain together.
When we call the GetStore method, it obtains a store that corresponds with the passed input parameters. When we pass the User and Assembly IsolatedStorageScope enumeration members, this acquires a store that can be shared between applications that use the same assembly, when used by the same user. Typically, this is allowed under the Intranet security zone, but not the Internet zone.
When we pass the User, Assembly, and Domain IsolatedStorageScope enumeration members, this acquires a store that can only be accessed by the user, when running the application that was used to create the store. This is the default and most common choice for most applications, and so these are the enumeration members that were used in our example.
Note that if we had wanted to enable the user to use roaming profiles but still be able to access their data from their isolated storage file, then we could have additionally included the Roaming enumeration member with the other members.
Returning to the HardDriveManager class now, in the SaveTextFile method, we first call the GetIsolatedStorageFile method to obtain the IsolatedStorageFile object. We then initialize an IsolatedStorageFileStream object with the filename specified by the filePath input parameter, the OpenOrCreate member of the FileMode enumeration and the storage file object.
Next, we initialize a StreamWriter object with the IsolatedStorageFileStream variable and write the data from the fileContents input parameter to the file specified in the stream using the Write method of the StreamWriter class. Again, we enclose this in a try...catch block and would typically log any exceptions that might be thrown from this method, but omit this here for brevity.
In the ReadTextFile method, we initialize the fileContents variable to an empty string and then obtain the IsolatedStorageFile object from the GetIsolatedStorageFile method. We verify that the file specified by the filePath input parameter actually exists before attempting to access it.
We then initialize an IsolatedStorageFileStream object with the filename specified by the filePath input parameter, the Open member of the FileMode enumeration, and the isolated storage file.
Next, we initialize a StreamReader object with the IsolatedStorageFileStream variable and read the data from the file specified in the stream into the fileContents input parameter using the Read method of the StreamReader object. Once again, this is all enclosed in a try...catch block, and finally, we return the fileContents variable with the data from the file.
In order to use it, we must first register the connection between the interface and our runtime implementation with our DependencyManager instance:
DependencyManager.Instance.Register<IHardDriveManager, HardDriveManager>();
Then we can expose a reference to the new IHardDriveManager interface from our BaseViewModel class and resolve it using the DependencyManager instance:
public IHardDriveManager HardDriveManager { get { return DependencyManager.Instance.Resolve<IHardDriveManager>(); } }
We can then use it to save files to, or read files from, isolated storage from any View Model:
HardDriveManager.SaveTextFile("UserPreferences.txt", "AutoLogIn:True"); ... string preferences = HardDriveManager.ReadTextFile("UserPreferences.txt");
Realistically, if we were to save user preferences in this way, they would typically be in an XML file, or in another format that is more easily parsed. However, for the purposes of this example, a plain string will suffice.
As well as saving and loading files in an isolated storage store, we can also delete them and add or remove folders to better organize the data. We can add further methods to our HardDriveManager class and IHardDriveManager interface to enable us to manipulate the files and folders from within the user's isolated storage store. Let's take a look at how we can do this now:
public void DeleteFile(string filePath) { try { IsolatedStorageFile isolatedStorageFile = GetIsolatedStorageFile(); isolatedStorageFile.DeleteFile(filePath); } catch { /*Log error*/ } } public void CreateFolder(string folderName) { try { IsolatedStorageFile isolatedStorageFile = GetIsolatedStorageFile(); isolatedStorageFile.CreateDirectory(folderName); } catch { /*Log error*/ } } public void DeleteFolder(string folderName) { try { IsolatedStorageFile isolatedStorageFile = GetIsolatedStorageFile(); isolatedStorageFile.DeleteDirectory(folderName); } catch { /*Log error*/ } }
Quite simply, the DeleteFile method accesses the IsolatedStorageFile object from the GetIsolatedStorageFile method and then calls its DeleteFile method, passing in the name of the file to delete, which is specified by the filePath input parameter, within another try...catch block.
Likewise, the CreateFolder method obtains the IsolatedStorageFile object from the GetIsolatedStorageFile method and then calls its CreateDirectory method, passing in the name of the folder to create, specified by the folderName input parameter, within a try...catch block.
Similarly, the DeleteFolder method acquires the IsolatedStorageFile object by calling the GetIsolatedStorageFile method and then calls its DeleteDirectory method, passing in the name of the folder to delete, which is specified by the folderName input parameter, within another try...catch block.
Now, let's adjust our previous example to demonstrate how we can use this new functionality:
HardDriveManager.CreateFolder("Preferences"); HardDriveManager.SaveTextFile("Preferences/UserPreferences.txt", "AutoLogIn:True"); ... string preferences = HardDriveManager.ReadTextFile("Preferences/UserPreferences.txt"); ... HardDriveManager.DeleteFile("Preferences/UserPreferences.txt"); HardDriveManager.DeleteFolder("Preferences");
In this extended example, we first create a folder named Preferences in the isolated storage store and then save the text file in that folder by prefixing the filename with the name of the folder and separated from the name with a forward slash.
At a later stage, we can then read back the contents of the file by passing in the same file path to the ReadTextFile method. If we need to clear up the store afterward, or if the file was temporary, we can delete it by passing the same file path to the DeleteFile method. Note that we must first delete the contents of a folder in the store before we can delete the folder itself.
Also note that we can create subdirectories in the isolated storage store by chaining their names in the file path. For example, we can create a Login folder in the folder named Preferences by simply appending the subdirectory name to the end of the parent folder name and separating them with a forward slash again:
HardDriveManager.CreateFolder("Preferences"); HardDriveManager.CreateFolder("Preferences/Login"); HardDriveManager.SaveTextFile("Preferences/Login/UserPreferences.txt", "AutoLogIn:True");
This concludes our look into isolated storage files in .NET. But before we end this chapter, let's briefly turn our attention to discover how to access our various application versions and, indeed, what they all relate to.