Persisting Form Settings

(Originally published: 15 September 2007)

This article was originally written when .NET v2 was released.  Some of the information in here may be outdated with newer .NET versions but there is still some good information on persisting form settings and avoiding some pitfalls in the process.  Therefore this article is being posed unchanged from its initial form.

When working with Windows applications users expect that the application will remember its state when it is closed and restore that state when it is opened.  This allows the user to customize the layout of the application to fit their needs.  If Bill only works with a single application at a time he might like to have the application take up the whole screen so he can maximize his view.  Julie, on the other hand, runs several applications at once.  She wants to keep windows laid out in a certain order so she can efficiently copy and paste data between applications.  It doesn't make sense to require Julie and Bill to share the same window layouts when their individual settings can be stored easily.

In the early betas of .NET v2 this functionality was available.  It seems to have been removed before the final release.  This article will discuss how to add in form state persistence to your WinForms applications.  This article will only deal with a single form and the basic form attributes but it can be extended to multiple forms with many different properties including tool windows and docking state.

Where to Store the Data

For persisting data on a per-user basis you basically have three common choices: registry, isolated storage and settings file.  The registry is commonly used for old-school programming.  The HKEY_CURRENT_USER key is designed for storing per-user settings.  It works a lot like a file system.  The registry is generally not recommended anymore.  It is designed for small pieces of data and has limited support for data formats.  It is, however, secure and requires more than a passing knowledge of Windows to use properly.  Therefore it is a good choice for settings that should generally be protected but not to big in size.  A big limitation of the registry is that it can't be used in applications that don't have registry access (like network applications or smart clients).

Isolated storage is a step up the file system hierarchy.  It looks like a file system (and actually resides in the system somewhere) but its actual location is hidden from applications.  Isolated storage allows any application to store data per-user.  The downside to isolated storage is that it can be a little confusing to use.  Additionally, since the actual path and file information is hidden, it can be hard to clean up corrupt data if something were to go wrong. 

Finally there is the settings file.  We are talking about the user settings file here, not the application settings file.  Each user can have their own settings file.  This file works similar to the application property settings file that you can use for application-wise settings.  The difference is that each user has their own copy and it is store in the user's profile directory.

Before moving on it is important to consider versioning of the settings.  If you want a user running v1 of your application to be able to upgrade to v2 and not lose any of their settings then you must be sure to chose a persistence location that is independent of the version of the application.  The registry is a good choice here as isolated storage and user settings are generally done by application version.  Still it doesn't make sense in all cases to be backward compatible with the settings file.  You will have to decide on a case by case basis.

FormSettings

Let's start with the basic class we'll use.  Ultimately, since we might have quite a few forms to persist we want to create a base class (FormSettings) that will take care of the details.  We can derive from this class for custom form settings as needed.  In this article we will use the user's setting file so we derive from ApplicationSettingsBase.  If you want to use a different storage mechanism then you'll need to make the appropriate changes. 

Since we want our class to work with multiple forms we need to make each form unique.  We will use the SettingsKey to make each form unique.  Each form must specify the key it will use.  Here is the start of our class.

public class FormSettings : ApplicationSettingsBase 

   public FormSettings ( string prefix ) 
   { 
      SettingsKey = prefix; 
   } 

   public void Load ( Form target )  />   {
      //Load
   }

   public void Save ( Form target ) />   {
      //Save
   }
}

When the form is loaded it will call Load to load its settings.  When the form is closed it will call Save.  Here is sample code for our form.

protected override void OnLoad(EventArgs e) 

    base.OnLoad(e); 

    m_Settings.Load(this); 


protected override void OnFormClosing ( FormClosingEventArgs e ) 

    base.OnFormClosing(e); 

    if (!e.Cancel) 
        m_Settings.Save(this);
}

private FormSettings m_Settings = new FormSettings("MainForm");

 

Persisting Properties

At a minimum a user would expect to be able to move the window around and resize it to fit their needs.  Therefore we need to load and save the following properties: DesktopLocation, Size and WindowStateDesktopLocation specifies the position, relative to the top-left corner of the desktop, of the top-left corner of the form.  The Size property indicates the width and height of the form.  Finally the WindowState is used to track when a window is minimized or maximized.  We will discuss this property shortly.

To load and save properties using the settings file we need to define a property for each item we want to save.  We need to mark the property as user-scoped and we need to get the value from and set the value to the settings file.  This is pretty straightforward so we will not dwell on the details.  Here is the code for getting and setting the property values.  One point of interest is that we use special values when we can not find the property values.  This will come back later when we talk about loading the settings.

public class FormSettings : ApplicationSettingsBase  
{
   ... 

   [UserScopedSetting] 
   public Point Position  BR />   { 
      get 
      { 
         object value = this["Position"]; 
         return (value != null) ? (Point)value : Point.Empty; 
      } 
      set { this["Position"] = value; } 
   } 

   [UserScopedSetting] 
   public Size Size  R />   { 
      get 
      { 
         object value = this["Size"]; 
         return (value != null) ? (Size)value : Size.Empty; 
      } 
      set { this["Size"] = value; } 
   } 

   [UserScopedSetting] 
   public FormWindowState State  R />   { 
      get 
      { 
         object value = this["State"]; 
         return (value != null) ? (FormWindowState)value : FormWindowState.Normal; 
      } 
      set { this["State"] = value; } 
   } 
}

 

Saving the Settings

Saving the settings is really easy.  All we have to do is set each of the user-scoped property values and then call Save on the base settings class.  This will flush the property values to the user's settings file.

public void Save ( Form target ) 

   //Save the values 
   Position = target.DesktopLocation; 
   Size = target.Size; 
   State = target.WindowState; 

   //Save the settings
   Save(); 
}

 

Loading the Settings

Loading the settings requires a little more work.  On the surface it is similar to the save process: get each property value and assign it to the form.  The only issue that comes up is what to do when no settings have been persisted (or they are corrupt).  In this case I believe the best option is to not modify the form's properties at all and, therefore, let it use whatever settings were defined in the designer.  Let's do that now and see what happens.

public void Load ( Form target ) 

   //If the saved position isn't empty we will use it 
   if (!Position.IsEmpty) 
        target.DesktopLocation = Position; 
   //If the saved size isn't empty we will use it 
   if (!Size.IsEmpty) 
     target.Size = Size; 

   target.WindowState = State; 
}

It seems to work.  This is too easy, right?  Now try minimizing the form and then closing it.  Open the form again.  Can't restore it can you? 

Minimizing and Maximizing Forms

The problem is that for a minimized/maximized form the DesktopLocation and Size properties are not reliable.  Instead we need to use the RestoreBounds property which tracks the position and size of the form in its normal state.  If we persist this property when saving then when we load we can restore the normal position and size and then set the state to cause the form to minimize or maximize properly.  But there is another problem.  RestoreBounds isn't valid unless the form is minimized or maximized.  Therefore our save code has to look at the state of the form and use DesktopLocation when normal and RestoreBounds when minimized/maximized.  Note that RestoreBounds.Size is valid in both cases although whether this is by design or not is unknown.  The load code remains unchanged as we will set the values based upon the form's normal state and then tell the form to minimize or maximize.  Here is the updated code.

public void Save ( Form target ) 

   //Save the values 
   if (target.WindowState == FormWindowState.Normal) 
      Position = target.DesktopLocation; 
   else 
      Position = target.RestoreBounds.Location; 

   Size = target.RestoreBounds.Size; 
   State = target.WindowState; 

   //Save the settings 
   Save(); 
}

 

Disappearing Windows

The final problem with our code is the problem of disappearing windows.  We've all seen it happen.  You start an application and the window shows up in the Task Bar but not on the screen.  Windows doesn't realize that the application's window is off the screen.  This often occurs when using multiple monitors and we switch the monitors around or when changing the screen resolution.  Fortunately we can work around it.

During loading we need to verify that the window is going to be in the workspace of the screen (which includes all monitors).  If it isn't then we need to adjust the window to appear (at least slightly) on the screen.  We can do this by doing a quick check to make sure the restore position is valid and update it if not. 

What makes this quite a bit harder is the fact that screen coordinates are based off the primary monitor.  Therefore it is possible to have negative screen coordinates.  We also don't want the Task Bar or other system windows to overlap so we will use the working area of the screen which is possibly smaller than the screen size itself.  .NET has a method to get the working area given a control or point but it returns the closest match.  In this case we don't want a match.  .NET also has SystemInformation.VirtualScreen which gives us the upper and lower bounds of the entire screen but it doesn't take the working area into account.

For this article we'll take the approach of calculating the working area manually by enumerating the monitors on the system and finding the smallest and largest working areas.  Once we have the work area we need to determine if the caption of the form fits inside this area.  The caption height is fixed by Windows but the width will match whatever size the form is.  We do a little math and viola.  If the caption is visible then we will set the position otherwise, in this case, we simply let the form reset to its initial position.  Here is the load code.

public void Load ( Form target ) 

   //If the saved position isn't empty we will use it 
   if (!Position.IsEmpty) 
   { 
       //Verify the position is visible (at least partially) 
       Rectangle rcArea = GetWorkingArea(); 

       //We want to confirm that any portion of the caption is visible 
       //The caption is the same width as the window but the height is fixed 
       //from the top-left of the window 
       Size sz = (Size.IsEmpty) ? target.Size : this.Size; 
       Rectangle rcForm = new Rectangle(Position, new Size(sz.Width, SystemInformation.CaptionHeight)); 
       if (rcArea.IntersectsWith(rcForm)) 
          target.DesktopLocation = Position; 
   }; 

   //If the saved size isn't empty we will use it 
   if (!Size.IsEmpty) 
       target.Size = Size; 

   target.WindowState = State; 


private Rectangle GetWorkingArea () 

   int minX, maxX, minY, maxY; 
   minX = minY = Int32.MaxValue; 
   maxX = maxY = Int32.MinValue; 

   foreach (Screen scr in Screen.AllScreens) 
   { 
      Rectangle area = scr.WorkingArea; 

      if (area.Bottom < minY) minY = area.Bottom; 
      if (area.Bottom > maxY) maxY = area.Bottom; 

      if (area.Top < minY) minY = area.Top; 
      if (area.Top > maxY) maxY = area.Top; 

      if (area.Left < minX) minX = area.Left; 
      if (area.Left > maxX) maxX = area.Left; 

      if (area.Right < minX) minX = area.Right; 
      if (area.Right > maxX) maxX = area.Right; 
   }; 

   return new Rectangle(minX, minY, (maxX - minX), (maxY - minY)); 
}
Published Sun, Jul 18 2010 16:38 by Michael Taylor
Filed under: ,