Share via


Advanced Basics

Remembering User Information in Visual Basic .NET

Duncan Mackenzie

Code download available at:AdvancedBasics0504.exe(130 KB)

Setting Up and Retrieving Application Settings
Why Shouldn't You Use App.Config?
Creating a Settings Class
Serializing Your Settings to Disk
Isolated Storage
Saving and Loading Within Your Application
What About Visual Basic 2005?
Conclusion

Many applications need to store user-specific settings to be persisted between sessions. But how do you go about saving and restoring these settings in your Microsoft® .NET Framework-based application? It's not all that easy to find the correct answer. You'll find a wide variety of solutions on newsgroups and forums, but only a few of the posted solutions illustrate the proper way to handle this requirement.

First, let's define the two main types of settings that an application is likely to have: application settings and user settings. Application settings are shipped with the app, affect every user, and are intended to be modified only as an administrative task when the overall behavior of the application needs to change.

Consider a commercial human resources application. When it is deployed within your particular company, the application settings are customized to specify the database server that your users are going to connect to, where to find your company's logo to add to the UI, and so on. These are application settings, not user settings, and can be installed right into the same directory as the program itself. These settings are likely installed in a directory where only an administrator has permission to edit or delete files (c:\Program Files\, for example), which helps to reinforce that these are definitely not user settings. The application should only need to read these settings, not to change them.

User settings are preferences that are user-specific and need to be editable from your code. For the mythical HR application, user settings would include the size/position of the application window, the startup state of the application, and even rapidly changing information such as the last five employees viewed (for that useful "Recently Viewed Items..." File menu option). All of these settings need to be retrieved at the start of the application session, edited as necessary while it is running, and persisted to disk to be available for the next time the user fires up your program.

Setting Up and Retrieving Application Settings

Creating an application configuration file is relatively well documented in the .NET Framework SDK, but I thought I would provide you with a brief overview. The first step is to create a new settings file inside your project by selecting Add New Item from the Project menu and choosing the Application Configuration File option. Leave the file with its default name of App.Config; it will be moved into the output directory and renamed appropriately (to yourappname.exe.config) when you build your project. Within the new file you can define custom configuration sections (with whatever structure you need for your data), or add your own settings just by using the appSettings block that can hold a list of name value pairs. Storing a couple of simple values produces a file that looks like this: It is easy to retrieve values stored in the appSettings section through the classes and methods of the System.Configuration namespace. You don't even need to load the file yourself; it is automatically discovered and loaded if it is named and located correctly (yourappname.exe.config in the application directory): Dim companyName As String _ = ConfigurationSettings.AppSettings("Company Name")

As I mentioned, creating and accessing application configuration files is well documented (search on "System.Configuration" in the .NET Framework SDK), so I won't go into any more detail on this topic here.

Why Shouldn't You Use App.Config?

I often see the question "How do I write to my app.config?" on a forum or newsgroup, and it is easy to understand how the developer has arrived at this point. After developers writing within .NET have used the application configuration file and its easy programmatic interface, they wonder why they can't just use the same file to store user settings. Why have two separate ways to configure an application? Shortly after, they discover that the System.Configuration namespace does not provide any method for writing to the app.config file, which should be a warning sign, but that is only a small barrier to the determined programmer. The Web is searched, newsgroup questions are posted, and then direct loading and editing using XmlDocument, or even regular file IO, usually follows.

This is not a good idea. Your application should not write to this file for at least the following three reasons:

Permissions Installing, removing, and editing application files is an administrative task, requiring different permissions from those of a normal user. In Windows® XP, one way that is indicated is through the default permissions on the Program Files directory: read-only to users, but read/write to members of the Administrators group. If your app is installed into the Program Files directory and uses its app.config file to store user preferences, then the act of saving changes to those settings will require administrative rights on the local machine. Although many developers (and users) run as administrators of their own machines, this will certainly be an issue in most corporate environments. Beware that you should not allow unfettered access to Program Files; there are good reasons to restrict write access.

No user isolation The application configuration file applies to every user on the machine, so if multiple users are sharing the same computer, everyone will be sharing the same set of options. The implications could range from the just annoying (one user likes to have their window maximized to start, and another wants it windowed) to an extreme functionality and security issue (the application needs your POP3 server information, and every user of the machine can read that information at will). Even if your current user base is all one user per machine, you can bet it won't stay that way; it is worth noting that even a Guest account on the system might have read access to Program Files directory (and therefore the application settings file), exposing any potentially sensitive information stored into that file.

The app.config file is part of the application's installationThis file is part of the application itself and will be overwritten by new installations and likely removed if the application is ever uninstalled. Any user data stored into this file would be lost, which could be very serious depending on the type of data stored.

For these reasons, and probably a few more, you shouldn't use the app.config file to store user settings, but don't worry, saving the user's preferences is not as hard as you might believe.

Creating a Settings Class

The key to easily saving your user-specific settings is to define a class that represents all of that data. This class will be your interface with all of the user settings, and it is the first thing you should build once you determine that you need to store user data at all. Here, I am going to use a simple Windows Form that can open files from disk; the user settings file will store the user's choice of background color, the last window position, and a list of recently opened files. Describing all this data into a class, I came up with the definition shown in Figure 1.

Imports System.Drawing Imports System.IO Imports System.IO.IsolatedStorage Imports System.Environment Imports System.Collections.Specialized Public Class Settings Private m_BackgroundColor As String Private m_RecentFiles As New StringCollection Private m_LastWindowBounds As Rectangle _ = New Rectangle(0, 0, 200, 200) Private m_LastWindowState As Windows.Forms.FormWindowState _ = FormWindowState.Normal _ Public Property LastWindowBounds() As Rectangle Get Return m_LastWindowBounds End Get Set(ByVal Value As Rectangle) m_LastWindowBounds = Value End Set End Property Public Property LastWindowPos() As Point Get Return m_LastWindowBounds.Location End Get Set(ByVal Value As Point) m_LastWindowBounds.Location = Value End Set End Property Public Property LastWindowSize() As Size Get Return m_LastWindowBounds.Size End Get Set(ByVal Value As Size) m_LastWindowBounds.Size = Value End Set End Property Public Property LastWindowState() As Windows.Forms.FormWindowState Get Return m_LastWindowState End Get Set(ByVal Value As Windows.Forms.FormWindowState) m_LastWindowState = Value End Set End Property Public Property RecentFiles() As StringCollection Get If m_RecentFiles Is Nothing Then m_RecentFiles = New StringCollection End If Return m_RecentFiles End Get Set(ByVal Value As StringCollection) If Value Is Nothing Then m_RecentFiles = New StringCollection Else M_RecentFiles = Value End If End Set End Property Public Property BackgroundColor() As String Get Dim colorToReturn As String If m_BackgroundColor Is Nothing OrElse _ m_BackgroundColor = String.Empty Then colorToReturn = ColorTranslator.ToHtml( _ Color.FromKnownColor(KnownColor.Control)) Else colorToReturn = m_BackgroundColor End If Return colorToReturn End Get Set(ByVal Value As String) If Not ColorTranslator.FromHtml(Value).IsEmpty Then m_BackgroundColor = Value Else m_BackgroundColor = String.Empty End If End Set End Property End Class

The BackgroundColor property is relatively complex because I wanted to save the color as the HTML representation, but otherwise the class is a simple set of properties. I implemented the list of Recent Files as a StringCollection instead of as an array because it is easier to add new files to a collection, but either approach works.

Serializing Your Settings to Disk

Once you have your class defined, the next step is to write code to read/write this information to a file on disk. It would be possible to generate and parse an XML file using the System.Xml.XmlReader and System.Xml.XmlWriter classes, but it is a bit simpler to just use XML Serialization. Serialization takes care of everything necessary to turn an instance of your Settings class into XML, and knows how to convert it back as well. The serialization approach is quite robust, capable of dealing with missing values, the addition (perhaps in future versions of your application) of new properties, and more, so it ends up being a great choice. You can save your settings class to disk through an instance of the System.Xml.Serialization.XmlSerializer class, and you can use the same class to deserialize your data back from disk as well. The basic code for both operations is shown in Figure 2.

Public Sub SaveSettings() Dim xs As New XmlSerializer(GetType(Settings)) Dim sw As New IO.StreamWriter("C:\settings.xml") xs.Serialize(sw, currentSettings) sw.Close() Dim sr As New IO.StreamReader("C:\settings.xml") currentSettings = CType(xs.Deserialize(sr), Settings) sr.Close() End Sub

Figure 2 is just a simple example; I like to put the real saving/loading code right into my settings classes, as a combination of Shared (static for you C# types) and Instance methods. I provide a few different options, a routine to load from a provided stream, one that takes care of getting a stream on its own, and the same options for saving the settings back to disk.

The act of serializing or deserializing requires only two things. First, you need an instance of the XmlSerializer class, created by passing your class's type to the constructor: Dim xs As New XmlSerializer(GetType(Settings)) You also need a Stream, TextWriter, or XmlWriter instance representing the output for serialization, or a Stream, TextReader, or XmlReader for the input to be deserialized. Dim sw As New IO.StreamWriter("C:\settings.xml")

Where you save your settings on disk is another important decision. I suggest that you choose either the user's application data directory or the application-specific area of Isolated Storage. The first option stores the settings into a location under "C:\Documents and Settings\userid\Application Data\" (the drive letter and exact location can vary from machine to machine), which is a perfect location for settings as it is user-writeable and consistent with the storage of most other application options. You can obtain a full path to this location using either Application.UserAppDataPath or System.Environment.GetFolderPath. The Application method will return a path like the one shown here that includes the AssemblyCompany, AssemblyProduct, and AssemblyVersion values from your AssemblyInfo.vb file: C:\Documents and Settings\\Application Data\ company\product\version That path is easy to get, but it is a version-specific location. As an alternative, the value returned from GetFolderPath doesn't have your company and product name information in it: C:\Documents and Settings\userid\Application Data If you go with the first location, any future release of your product (assuming you increment the version number) will start up as if it has no existing user preferences, which would be a problem for some applications; if you go with the second, your files might conflict with those of another application. I prefer to save my files up one level from the first location, so that it is a version-independent settings file. Getting my desired path could be accomplished a few different ways (taking the parent of the UserAppDataPath, for example), but the following code snippet works well for me: Dim filePath As String filePath = Path.Combine(GetFolderPath( _ SpecialFolder.ApplicationData), Application.CompanyName) filePath = Path.Combine(filePath, Application.ProductName) filePath = Path.Combine(filePath, FILENAME) Dim settingsFile As New FileStream(filePath, FileMode.Create)

You could perform standard string concatenations (or use a StringBuilder) instead, but I prefer to use Path.Combine whenever I'm working with path information.

Isolated Storage

The other ideal location to store user settings is isolated storage. Isolated storage is an abstraction over an area of the user's application data that provides a unique file storage area tied to the current user and some aspect of the running application. I normally use isolated storage tied to the user and the current assembly identity, using the classes in System.IO.IsolatedStorage to open a stream to a new or existing file within that storage area: Dim ifs As IsolatedStorageFile ifs = IsolatedStorageFile.GetUserStoreForAssembly() Dim settingsFile As New IsolatedStorageFileStream( _ FILENAME, FileMode.Create, ifs) Return settingsFile

It is possible to find this file on disk if you are willing to browse through the various folders under "C:\Documents and Settings\userid\Local Settings\Application Data\IsolatedStorage" (location may vary). While it's certainly not hidden completely, it is less likely that a user will be able to easily discover this file, compared to one written directly to a normal file location.

For most of my applications, I use the application data location for my settings file. Isolated storage works better in some limited- trust situations where regular file access is restricted. In order to write to the application data location, your application requires FileIOPermission, but to open a file stream within isolated storage requires IsolatedStorageFilePermission. In the default security configuration in the .NET Framework 1.1, only apps with Full Trust have file IO permissions, while even applications launched from Internet sites have permission to write to isolated storage.

For the security-related reasons discussed here, there are applications that will need to use isolated storage and others where it doesn't really matter as much. To make my settings code more reusable, I added a Boolean constant that toggles between the two types of file storage: Public Const USE_ISOLATED_STORAGE As Boolean = False

Saving and Loading Within Your Application

In your application's main form or function, you need to load the settings file from disk when your application is started, and then persist it (only really necessary if it has been changed) when your application ends. All of the functionality you need is attached right to your class, so all that is really required is that you call those functions and work with the class to edit the user's preferences as needed. In a simple (one main form) application, this can be handled very easily with just a few lines of code.

For loading your settings at startup, use code like that in Figure 3. Then, when saving, use code similar to that in Figure 4.

Protected Overrides Sub OnClosing(ByVal e As _ System.ComponentModel.CancelEventArgs) SaveSettings() End Sub Private Sub SaveSettings() With Me.currentSettings .LastWindowBounds = Me.Bounds .BackgroundColor = ColorTranslator.ToHtml(Me.BackColor) .LastWindowState = Me.WindowState .PersistMe() End With End Sub Dim currentSettings As Settings Public Sub LoadSettings() Try Me.currentSettings = Settings.Load() With Me.currentSettings Me.Bounds = .LastWindowBounds Me.BackColor = ColorTranslator.FromHtml(.BackgroundColor) Me.WindowState = .LastWindowState End With UpdateRecentFileMenu() Catch ex As Exception Me.currentSettings = New Settings End Try End Sub Public Sub New() MyBase.New() 'This call is required by the Windows Form Designer. InitializeComponent() 'Add any initialization after the InitializeComponent() call LoadSettings() End Sub

Alternatively you could call SaveSettings only when your user's options are changed (after showing the user an option dialog, for example), or you could track its modified state using an additional property such as "dirty." If you decide to add an additional property, which you probably don't want serialized to and from disk, it should be marked with the XmlIgnore attribute. This attribute tells the XmlSerialization process to ignore this particular property; it won't be written into the settings file on disk. In my sample, I used XmlIgnore to indicate that the LastWindowBounds property shouldn't be serialized. That property represented a real value, the rectangle indicating the position and size of the main application Form, but the two parts of that value (the location and the size) are also exposed through their own individual properties, and there is no reason to put the same data into the settings file more than once: _ Public Property LastWindowBounds() As Rectangle Get Return m_LastWindowBounds End Get Set(ByVal Value As Rectangle) m_LastWindowBounds = Value End Set End Property

Once you have your application working correctly, including editing, loading, and saving your settings, you may want to add some additional settings-related functionality. Two features that would be useful are the ability to reset your application's settings to a default (which could be loaded from the app.config file) and to save/load the settings to and from an arbitrary file location. I have added both options to my sample application for you to check out.

What About Visual Basic 2005?

This column has discussed where to store user settings, why you shouldn't use the same method as application settings, and how to create a custom settings class; many of these issues are handled for you in the upcoming release of Visual Basic®. In Visual Basic 2005, there is a settings designer (see Figure 5) built into Visual Studio® that allows you to set up your list of app and user-level settings.

Figure 5** Visual Basic 2005 Settings Designer **

Once you've configured this info for your project, you can easily set and retrieve these values through the My.Settings class: Me.Text = My.Settings.CompanyName If My.Settings.StartMaximized Then Me.WindowState = FormWindowState.Maximized End If I won't go into any more detail about this new feature, but if you are interested I suggest you check out the article "Using My.Settings in Visual Basic 2005" by Emad Ibrahim on MSDN® online.

Conclusion

When creating your application, divide any settings or preference information into values at the application level and at the user level. For application settings, .NET provides the app.config file and classes to handle reading those values, but for user settings, you need to take care of it yourself. I've presented one way to handle that using XML Serialization, which should get you started building your own settings class for your applications.

Send your questions and comments for Duncan to  basics@microsoft.com.

Duncan Mackenzie is a developer for MSDN and the author of the Coding 4 Fun column on MSDN online. He can be reached through his personal site at www.duncanmackenzie.net.