[ Inside Visual Basic ]

April 1999

Saving Your State

by Bill Shadish

In this article, we'll show you how to make your users appreciate you a little bit more than they do today. We'll do that by showing you how you can allow them to save and change some of their runtime options. We'll also cover some of the architectural choices that you're faced with when you decide to give your users the power to change the look of your applications.

The problem

Tell me if you find this as annoying as I do: you're editing a document in a word processing application, and you go to open another document. Instead of starting you off in the current directory, the application starts you in the Windows or the application's directory instead. On a large hard disk with many subdirectories, you then find that it takes many bothersome clicks just to get back to where you wanted to start from in the first place. Along those same lines, I find it comforting when I open an application and it remembers the last physical location of the applications' window on the desktop. Or, even better, by selecting a user preference, the application automatically opens up the last document that I was editing. These are some of the usability features that you, as a developer, can build into your programs to make your users happy.

How to fix it

There are three areas that need to be considered when adding stored options within your applications.

  1. Where to let the user change the parameters/options.
  2. Where to save those parameters/options.
  3. How to add awareness to your application so it can use the options.
We'll address each of these areas in the steps below.

Where to let users change the options

There are two basic methods for changing runtime options. The first is manually. This is the standard approach of allowing users to change options on a user preference screen. There would usually be some level of default options in place, in case the user never happened to chance across this option screen. However, if the user did find his way to make the changes, we would store those changes somewhere as well as immediately place them into effect within our program. Notice that this second action differs from many commercial applications. (Did anyone here ever have to restart Windows just to put VB changes, or other application changes, into place? Shudder.) Our goal is to implement changes without forcing an application restart.

The second way that options can be changed isautomatically. This is a little more dangerous, since people usually prefer to personally control the configuration of an application directly. However, there are notable exceptions where an automatic change is the right way to go. One example might be to save the state (minimized, maximized, or normal) of the application window when the user exits your program. You would then start the app in that same window state when it runs the next time.

You can see where you really wouldn't want to have the user go to an options screen, or even to automatically present the options screen, to set a property as simple as saving a window state. However, you should always provide a manual override to any automatically set options within the user options screen. This allows the user to turn off a feature if it's something that she doesn't want. This approach provides the best of both worlds to the majority of your users.

Where to save the options

The user's options might be stored in one of four different places. The selections can be entered into the Registry, into age-old ASCII .INI files, into a local binary file, or into a database table. The reasons for selecting one location over another are based on several tradeoffs. The Registry option is the easiest, but can (occasionally) bring on the specter of Registry problems if someone goes into the Registry to mess with the options directly. The INI file and largely unreadable-by-other-tools binary file options give you a stand-alone file in which you can place your options. This can facilitate centralized LAN administration of the user properties, since you can tell how the application is set up by looking at (or pulling back to study) a single stand-alone file.

However, in a client-server application, it might be best to store the user preferences directly within a database table, keyed by the users' name or user ID. This table can be stored locally, in an Access database, or on a central database table containing all of the user's preferences. This last method is interesting since using a central database table allows users to log in from any PC, while retaining all of their desired preferences.

Figure A shows our user option dialog box, where the user can change from one location to another. Of course, this can be coded so that the user can't change the location herself, but the location can be set centrally by an administrator. The choice is completely up to you.

Figure A: This dialog box allows the user to save her desired preferences.

[ Figure A ]

Notice that once you've added code to support the location options, you'll be able to add further code to the Form_Load of your application to "skip" from one location to another in the event that options aren't found where you'd expect them to be.

For example, let's say that the user has moved to work on a different machine. If she had been storing her options in the local Registry, her selections wouldn't be available on the new PC. You, as the developer, are able to recognize this and then search through the database, INI files, and so on, to see if any previous options for this user exist.

This would have the basic feel of the way that WinNT works, when it tells you that your local Profile Options are older or different than a centrally stored copy, allowing you to pick the one to use. If, and only if, no options at all are found, you can then prompt the user to see if she would like to configure her program options again.

More on where

In the article, "Building smarter applications, part 3," in the October, 1998 issue of Inside Microsoft Visual Basic, we discussed how to save parameters using the Registry calls SaveSetting and GetSetting. In the October code (which is also contained in this month's example) the Registry calls saved the position of the Window upon exiting the application. The application then repositioned itself to those settings whenever it was later restarted. In the October issue, we worked only with the Registry location; the code for this month's article includes the more extensible options SaveParms and GetParms, which are coded to handle each of our location types. You can see from Listing A that we can easily add multiple location logic to the Registry-only SaveWinParms routine that's found in this month's download code (see parms.bas).

The most interesting statement in the listing is:


Select Case CurrentLocation() 
That statement allows you to switch the location of the parameters dynamically at runtime (and live to tell about it!).

There's a related GetParms routine, not included in the listing, but found in the download code, which works in the same way. GetParms stores the values of the retrieved parameters into Public memory variables, so that any piece of code in the rest of the application can easily get to it.

Listing A: Parameter storage routines


' Parameter Options
Public Const LOAD_CAPTION = 100
Public Const SET_WIN_STATE = 200

' Storage Location Choices
Public Const PARMS_IN_DB = 100
Public Const PARMS_IN_INI = 110
Public Const PARMS_IN_BINFILE = 120
Public Const PARMS_IN_REGISTRY = 140

Public Sub SaveParms()
	Select Case CurrentLocation()
		Case PARMS_IN_DB
		...
		Case PARMS_IN_INI
		...
		Case PARMS_IN_BINFILE
		...
		Case PARMS_IN_REGISTRY
			Call SaveSetting(APP_NAME, "settings", _ 
				"SAVESCREENSONEXITMODE", True)
			Call SaveSetting(APP_NAME, "settings", _ 
				"SoundOn", True)
			Call SaveSetting(APP_NAME, "settings", _ 
				"MaximizeChildren", True)
			Call SaveSetting(APP_NAME, "settings", _ 
				"Color0", mColor0)
			Call SaveSetting(APP_NAME, "settings", _ 
				"Color1", mColor1)
			Call SaveSetting(APP_NAME, "settings", _ 
				"Color2", mColor2)
			Call SaveSetting(APP_NAME, "settings", _ 
				"Color3", mColor3)
			Call SaveSetting(APP_NAME, "settings", _ 
				"Color Scheme", mUserColorScheme)

			If frmMain.WindowState = vbMinimized Then
				' don't save the window positions, 
					because 
				` it will produce invalid values on 
					reloading 
				` them. Just save that the app was 
					minimized.

				Call SaveSetting(APP_NAME, 
					"settings", _ 
					"WinState", frmMain.
						WindowState)
			Else
				Call SaveSetting(APP_NAME, 
					"settings", _ 
					"WinState", 
					frmMain.WindowState)
				Call SaveSetting(APP_NAME, 
					"settings", _ 
					"WinLeft", frmMain.Left)
				Call SaveSetting(APP_NAME, 
					"settings", _ 
					"WinTop", frmMain.Top)
				Call SaveSetting(APP_NAME, 
					"settings", _ 
					"WinHeight", frmMain.Height)
				Call SaveSetting(APP_NAME, 
					"settings", _ 
					"WinWidth", frmMain.Width)
			End If
		Case Else
	
		' Add further options here.

	End Select
End Sub

Public Property Get CurrentLocation() As Integer
	If mCurrentLocation = 0 Then
		' it hasn't been set yet
		CurrentLocation = PARMS_IN_REGISTRY
	Else
		CurrentLocation = mCurrentLocation
	End If
End Property

How to use the options

The final piece of the puzzle is to add code to the portions of your program so it can use the options that you've stored. A best-case scenario would have you create one centralized routine, through which any option-using piece of code must pass. This gives you just one major place to worry about whenever new options are added to the system. This also gives you a standardized approach to dealing with the parameters throughout the program. For example, let's say that we want to display--in a caption across the top of each screen--the current client record that our user is working with. What we're trying to do is to use the real estate on the caption bar to hold helpful information within our system such as the client's name and key. Each screen in your application could contain code that retrieves the current clients' info and does the me.caption = things to display the appropriate data. But a better way to deal with this is to have one central routine into which you pass the form instead.

An example of this approach is shown in the following statements:


rc= HandleParms (Me, LOAD_CAPTION, _
	strUserName$)

Function HandleParms (frm as Form, _
	iParm as Integer, _
	Optional vData as Variant) as Integer
The first statement shows how to call our central HandleParmsroutine and the second shows the definition of HandleParms. The HandleParms function returns a value, which is True by default. This value can be used to let the caller know whether or not the handler worked as expected. The Optional vData, shown in HandleParms, allows the developer to pass information into this routine. In our example, the clients' name or user ID can be passed into HandleParms for it to then be displayed in the title bar of our form. Our HandleParms routine doesn't need to worry about the location in which the user stores her parameters, since it operates off of the publicly available memory variables that are loaded by GetParms at program startup.

Listing B demonstrates how this routine can be set up to handle the loading of the title bar. You would call the HandleParms routine with a line like this in your Form_Load event:


rc = HandleParms (Me, LOAD_CAPTION, _ 
	strUserName$)
If rc <> True then
	
	' process this error
	
End If 	
Note that Listing B also includes error-handling code.

Listing B: Handling parameters


Public Function HandleParms (frm As Form, _
	iParm As Integer, Optional vData As Variant)
	HandleParms = False
	On Error GoTo HandleParmsErr

	Select Case iParm
		Case LOAD_CAPTION
			If IsMissing(vData) Then
				vData = "No Record Selected"
			End If
			frm.Caption = "The Current User is " 
				& vData
	
		Case SET_WIN_STATE
			If IsMissing(vData) Then
				vData = vbNormal
			End If
			frm.WindowState = vData

		Case Else
			...
	End Select
	HandleParms = True
	Exit Function
	
	HandleParmsErr:
		HandleParms = False
		If errRoutine(Err, Error$(Err), _
			"HandleParms()") Then
			Resume
		Else
			Resume Next
		 End If
End Function

The entire flow

As a review, the complete flow of handling parameters goes something like this:

  1. The very first time the program runs, check for existing parameters. If none exist, create default parameters in a default location.

  2. In the Form_Load event of the first form to be displayed, or in the Main routine of a module, check to see if a location (Registry, INI, BIN, or Database) has been selected that's different than the default.

  3. Using the current location, retrieve the applications parameters, and store copies of those values into Public memory variables that are accessible throughout the application. Note that you can also create Property functions wrapped around these memory variables so that developers can access them more easily.

  4. Allow the user to change and save the properties using an options screen.

  5. Force any property changes that are made to be in effect immediately by resetting the centralized memory variables.

  6. Call central parm handler routines from anywhere in your program that the parameters will affect. Using a centralized handler and centralized parameter values makes it even easier to have any changes take effect immediately.

Summary

In this article, we've discussed the architecture of an application that allows for dynamically changing and saving runtime options. We also covered the why, where, and how of changing Registry options, and we talked about a centralized handler that allowed you to store these parameters into several different places. In a future article, we'll add code to SaveParms, GetParms, and HandleParms to complete the binary, INI file, and database options. We'll also add a Class file that wraps all of this functionality up into one shareable piece of code.


Copyright © 1999, ZD Inc. All rights reserved. ZD Journals and the ZD Journals logo are trademarks of ZD Inc. Reproduction in whole or in part in any form or medium without express written permission of ZD Inc. is prohibited. All other product names and logos are trademarks or registered trademarks of their respective owners.