(Print this page)

Persisting screen settings in a .Net application
Published date: Sunday, August 14, 2016
On: Moer and Éric Moreau's web site

This is a very recurrent question in forums: how can I persist my forms settings (location and size) to restore them at the same place when the application restart?

I have seen this question so many. I decided to write something that will answer some of the issues and fill some caveats of answers I found.

Downloadable demo

This month downloadable demo is available in both VB and C#. it has been created using Visual Studio 2015 but the same code can be used in earlier version as well.

Figure 1: The demo application in action

What are the issues and caveats?

Very often, answers provided in forums will only persist the location and the size of forms. This is only part of the solution.

We also need to consider the window state (maximized, minimized, normal). If a form is maximized or minimized, it is best to persist the restore bounds (location and size of the form before being maximized or minimized).

Another factor to consider is whether the location is still valid when restoring the bounds. If you have a configuration with 2 screens (your laptop and an external screen for example) and a form’s bounds persisted from the external screen, you need to ensure that when you restore the bounds, you are not trying to set it to the external screen if it has been detached!

Inheriting from a base form

Because I don’t want to repeat the same boring code on every form, I will create a base form and add some code to automatically persist and restore the state and size of forms. All the forms of my project will inherit from that base form.

I have decided to persist these values into a text file (named UserSettings.txt) in the same folder as the application. Feel free to use any other mechanism you are already using to persist these values.

The values are only loaded once when the application starts and saved once when the application is closing. They are maintained in memory in a simple dictionary.

The Settings class

So if you start by looking at the cSettings class, you will first notice that all members are Shared (of static in C#). This is because only one instance of the settings is required in memory.

You will also find a LoadUserSettings method that reads the content of the settings file to fill the dictionary.

The next important method is SaveUserSettings. This method just recreates the settings file with all the values currently contained in the dictionary.

Then comes 2 overloads of the GetProperty method. The first one returns a string that we will use for the window state persistence while the second overload returns a rectangle which we will use to set the Bounds property of the forms.

The last method in this class is little helper called ClearProperties which is a nice feature to offer to your user to reset all the various settings to their default values. In my case, a simply new-up the dictionary (deleting all the current values).

Here is the complete code of this class:

Option Strict On
 
Imports System.IO
 
Public Class cSettings
 
    Public Shared Properties As New Dictionary(Of String, String)
 
    Private Const SettingsFile As String = "UserSettings.txt"
 
    Public Shared Sub LoadUserSettings()
        Try
            Using reader As New StreamReader(SettingsFile)
                While reader.Peek() >= 0
                    Dim strReadLine As String = reader.ReadLine()
                    If Not String.IsNullOrWhiteSpace(strReadLine) Then
                        Dim keyValue As String() = strReadLine.Split(","c)
                        Properties(keyValue(0)) = keyValue(1)
                    End If
                End While
            End Using
        Catch ex As FileNotFoundException
            ' Handle when file is not found 
            ' * When the first application session 
            ' * When file has been deleted
        Catch ex As Exception
            If Debugger.IsAttached Then Debugger.Break()
        End Try
    End Sub
 
    Public Shared Sub SaveUserSettings()
        Try
            Using fs As StreamWriter = File.CreateText(SettingsFile)
                ' Persist each application-scope property individually 
                For Each key As String In Properties.Keys
                    fs.WriteLine("{0},{1}", key, Properties(key))
                Next
            End Using
        Catch ex As Exception
            If Debugger.IsAttached Then Debugger.Break()
        End Try
    End Sub
 
    Public Shared Function GetProperty(propertyName As String) As String
        Return If(Properties.ContainsKey(propertyName), Properties(propertyName), String.Empty)
    End Function
 
    Public Shared Function GetProperty(propertyName As String, defaultValue As Rectangle) As Rectangle
        Try
            Dim value As String = GetProperty(propertyName)
            If String.IsNullOrWhiteSpace(value) Then Return defaultValue
            
                Dim rect As Rectangle = defaultValue
                value = value.Replace("{", "").Replace("}", "")
                For Each s1 As string In value.Split(Convert.ToChar(";"))
                    Dim v1 As String = s1.Split(Convert.ToChar("="))(0)
                    Dim v2 As String = s1.Split(Convert.ToChar("="))(1)
 
                    Select Case v1.Trim().ToUpper()
                        Case "X"
                            rect.X = Convert.ToInt32(v2)
                        Case "Y"
                            rect.Y = Convert.ToInt32(v2)
                        Case "WIDTH"
                            rect.Width = Convert.ToInt32(v2)
                        Case "HEIGHT"
                            rect.Height = Convert.ToInt32(v2)
                    End Select
                Next
                Return rect
        Catch ex As Exception
            Return defaultValue
        End Try
    End Function
 
    Public Shared Sub ClearProperties()
        Properties = New Dictionary(Of String, String)
    End Sub
 
End Class

The base form

Now that we have a class to load and save the settings, lets create a base form. This form contains extra behavior that you want all your forms to inherit from. In this case, only the state, size and location persistence will be added as new behavior.

The OnLoad override sets the bounds of the form (which contains the location and the size) to what was persisted. If the form was persisted with a Maximized state, this state is also recovered and applied. In my experience, restoring a minimized form doesn’t make any sense because the users are looking for it! So if the form was persisted with a minimized state, it is restored with a normal state. You might notice a call to EnsureFormIsVisible. We will talk about this later.

The OnClosing override happens whenever a form is closing. It saves the state of the form. Then depending on the state of the form, it saves either the Bounds or the RestoreBounds. The reason is a form that is maximized or minimized had bounds before their WindowState was changed. The RestoreBounds contains those prior values.

Here is the full code for this form:

Option Strict On
 
Imports System.ComponentModel
 
Public Class fBaseForm
 
    Private Property FormSpecialName As String
 
    Public Sub New()
        InitializeComponent()
 
        FormSpecialName = [GetType]().Name
    End Sub
 
#Region "-- Events"
 
    Protected Overrides Sub OnClosing(e As CancelEventArgs)
        MyBase.OnClosing(e)
 
        If DesignMode Then Return
 
        'Persist form's state 
        cSettings.Properties(FormSpecialName & Convert.ToString(".WindowState")) = WindowState.ToString()
 
        'Persist form's position and size
        If WindowState = FormWindowState.Maximized OrElse WindowState = FormWindowState.Minimized Then
            cSettings.Properties(FormSpecialName & Convert.ToString(".Bounds")) = RestoreBounds.ToString().Replace(",", ";")
        Else
            cSettings.Properties(FormSpecialName & Convert.ToString(".Bounds")) = Bounds.ToString().Replace(",", ";")
        End If
    End Sub
 
    Protected Overrides Sub OnLoad(e As EventArgs)
        MyBase.OnLoad(e)
 
        If DesignMode Then Return
 
        'Retreive form's position and size
        Bounds = cSettings.GetProperty(FormSpecialName & Convert.ToString(".Bounds"), New Rectangle(0, 0, Width, Height))
 
        'Retreive form's state - ignore the minimize state
        If cSettings.GetProperty(FormSpecialName & Convert.ToString(".WindowState")).Trim().ToUpper() = "MAXIMIZED" Then
            WindowState = FormWindowState.Maximized
        End If
 
        EnsureFormIsVisible()
    End Sub
 
    Private Sub EnsureFormIsVisible()
        Dim windowRect = New Rectangle(Left, Top, Width, Height)
        If Screen.AllScreens.Any(Function(s) s.WorkingArea.IntersectsWith(windowRect)) Then
            Return
        End If
 
        Top = 0
        Left = 0
    End Sub
 
#End Region
 
End Class

The main form

This is the main form of the application. In my demo, this class is in charge of starting (initializing) the application and also the one to shut it down. It then becomes its duty to load the existing settings and to save them when the application exits.

When the main form first load, you need to load the existing settings (if any). I have chosen to had the call to the LoadUserSettings to the constructor of the form.

When the main form is closing, it will also close all the children forms automatically but we don’t have the warranty that all those children will get there closing events properly called and on time for the requirements of persisting their bounds. This is why the OnClosing event of the main form has been overwritten to ensure that all the children forms are properly closed before the settings are saved. Of course, you need to ignore the current form (If f IsNot Me Then) because the current form is already in a closing state. You will also see the I use the OpenForms collection to loop through the children and I convert it to an array. The reason is that if you loop through the collection itself without converting it to an array, as soon as a child form has been closed, you will get an error saying that the “Collection was modified; enumeration operation may not execute.”. By transforming the collection into an array, you circumvent this exception.

As shown in figure 1, the main form has 3 buttons. The first 2 buttons on the left whole purpose are to show 2 children forms (Form2 and Form3). The button of the right calls the ClearProperties method of the cSettings class. It is a nice feature to offer your user to reset all the settings to return to the default values.

The full code for this main form is like this:

Public Class fMain
 
    Public Sub New()
 
        ' This call is required by the designer.
        InitializeComponent()
 
        ' Add any initialization after the InitializeComponent() call.
        cSettings.LoadUserSettings()
    End Sub
 
    Protected Overrides Sub OnClosing(e As CancelEventArgs)
        MyBase.OnClosing(e)
 
        'close all children forms to ensure we have their latest bounds and states
        For Each f As Form In Application.OpenForms.Cast(Of Form)().ToArray()
            If f IsNot Me Then
                'except the current form which is already in closing mode!!!
                f.Close()
            End If
        Next
 
        cSettings.SaveUserSettings()
    End Sub
 
    Private Sub btnShowForm2_Click(sender As Object, e As EventArgs) Handles btnShowForm2.Click
        Form2.Show()
    End Sub
 
    Private Sub btnShowForm3_Click(sender As Object, e As EventArgs) Handles btnShowForm3.Click
        Form3.Show()
    End Sub
 
    Private Sub btnClearProperties_Click(sender As Object, e As EventArgs) Handles btnClearProperties.Click
        cSettings.ClearProperties()
    End Sub
 
End Class

Inheriting from the base form

In my demo application, Form2 and Form3 don’t have any code at all. The required code to persist the window state and the bounds are in the base form.

So we need the forms (including the main form) to inherits from the base form.

In C#, the inheritance is right in front of your eyes when you open the form’s code editor. If you look near the top of your file where the form is declared, you will find this line:

public partial class Form2 : Form

As you already know, the colon followed by Form indicates that we are inheriting from the Form class. To inherit from our own base form, just replace this Form class with our own:

public partial class Form2 : fBaseForm

In VB, the inheritance is hidden in the .Designer.vb file. Probably the easiest to change the inheritance is to open your form’s code editor and find the “Public Class FormX” near the top of your file. Click your form’s name and use the “Go To Definition” shortcut (by hitting F12 if you have the default shortcut settings). Once the designer file has been opened, you will find this line near the top of the file:

Inherits System.Windows.Forms.Form

This line needs to be modified to read like this:

Inherits fBaseForm

Testing the application

You can now run the application. You can size and set the location of your forms (including the children forms) anywhere you want and close the application. When you restart the application, the main form will reappear where it was. By reopening the children forms, the same behavior will apply.

If your forms were maximized, they will be reshown as maximized. If your forms were minimized, they will show up with the normal state (because from my experience, it is better to show them as normal then minimized).

It should work just as well if you have multiple monitors. If a form was on the second screen when it was closed, it will be shown back to the same spot.

About EnsureVisible

In the base form, there is a method called EnsureFormIsVisible which I said we were to revisit. Well it is the perfect time now!

The whole purpose of this method is to ensure it is visible. You might wonder why? I had the issue once where I had an external monitor plugged to my laptop. I was running an application and had a form with location persisted on the second screen. I unplugged the external monitor and went home. At night, on my laptop without an external monitor attached, I reran the application and never been able to see the child form because the bounds where restored to a position that was not existing on my now single screen setup.

So the EnsureFormIsVisible just makes sure that the bounds of the form to be shown will be visible with the current setup. If not, the form will be shown top-left of the main screen.

If you don’t have a multiple screens setup but still want to test the behavior, run the demo application at least once and close it to create the settings file. No open the generated UserSettings.txt file in your favorite text editor and modify the X and/or Y values of one of the form to values that are not existing on your setup and rerun the application to see it being reposition at 0,0.

Figure 2: The UserSettings file

Conclusion

A very simple method of persisting the state and the bounds of your forms with a very few lines of code.

If you never took advantage of inheritance for your forms, it might be time to revisit it. You can easily add common behaviors and features to your form without a lot of efforts!


(Print this page)