Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Saving the state (serializing) a Windows Form

0.00/5 (No votes)
13 Jun 2010 4  
Describes how to save the state of all controls on a form, and restore them at a later time

Introduction

It's a fairly common requirement to want to save the data when a user closes a form. In many applications nowadays, this is done with a database. Reopening the form involves reading the data back from the database, and populating all the form's controls with the data. Apart from the amount of time it can take to code all of this, maintaining it can be a nightmare, especially if there are a lot of controls on the form. Every time you change the form, you have to change the code to save the data.

I was recently working on an application where a database wasn't really practical, but I wanted to save a form's data for next time. The obvious way to do this would be to serialise the form object to disk. The .NET Framework allows you to mark classes with [Serializable], and then the framework does (almost) all of the hard work for you.

Unfortunately, this doesn't work with Windows Forms, as they are not marked as [Serializable], so the compiler throws an error if you try this with your own form.

Surely Someone Must Have Done This Already?

After spending fruitless hours searching, I came to the conclusion that I wasn't going to find a ready-made solution to this problem, and so I decided to write my own form serialiser. I wanted to make it reusable enough that it could be dropped into any project, and a form saved with the minimum of effort.

I was pleased enough with the results to want to post it here in case it's of use to anyone else. The test project that comes in the download has a main form that has just one button:

FormSerialisor3.gif

Clicking this button opens a child form, that has some (rather stupid) controls that ask you about your state of mind!

FormSerialisor4.gif

If you fill in some values and then click the "OK" button, the form is closed. If the "Serialise?" checkbox was checked, then the data you entered is saved in an XML file. If you now click the "Open" button on the main form, the child form is opened, and repopulated with the same data. If you change the data, uncheck the "Serialise?" checkbox and then click "OK" again, the data will not be serialised, so when you click the "Open" button on the main form, the child form will open again, but with the data from the previous instance will not be shown. If you delete the XML file, then when the child form opens, the controls will be empty.

In line with my desire to make this as simple as possible, saving the form data is done with one call to the serialisation method, and restoring it is done with one more. It doesn't matter how complex your form is, the one call is all you need.

Using the Code

To use this code in your own project, all you need to do is drop the FormSerialisor.cs file into your project, and then add the following line at the top of any class where you wish to use the code:

using FormSerialisation;

The FormSerialisor class is static, so you don't need to create an instance of it. You just call the static methods and pass in the form (or control, see later) you want serialising, and the full path to the XML file you want to use for saving the data.

Serialising and deserialising are now really easy. You can either do this from within the form itself, or from the code that calls the form. For example, if you wanted a form to serialise itself, you just use the following line:

FormSerialisor.Serialise(this, Application.StartupPath + @"\serialise.xml");

This will save the form's data in a file called serialise.xml in the same folder as where the executable lives.

If you then want to restore the data when the form next loads, then you would add the following code to the Form_Load event handler:

FormSerialisor.Deserialise(this, Application.StartupPath + @"\serialise.xml");

This would look for the XML file, and if it exists, populate the form's controls with data from the file. If the file doesn't exist, nothing will happen.

So How Does It Work?

The code for this is fairly straightforward, but there are a few small issues that I had to consider. The serialisation is done by enumerating all the controls on the form, and saving the data for each one in turn.

The Serialise(Control c, string XmlFileName) method creates an XmlTextWriter object that handles writing out the XML file, and then calls the recursive method, AddChildControls, passing the form as a parameter. Notice that the signature for this method specifies a Control as the first parameter. Since a Form is really a type of Control, this works fine if you pass in a form, but means that you don't have to serialise the whole form if you don't want. In the application where I first used this, I passed a TabControl in, as I only wanted to serialise the controls on that, and not the toolbar controls, etc., that were also on the main form.

If a control has child controls (if it were a Panel or GroupBox for example), then the child controls are enumerated and saved. This is all done by calling AddChildControls recursively, and passing the current container control as a parameter.

I decided not to serialise Label controls, as these are not user-changeable, and are not commonly changed in code (in my experience anyway). There's no harm in serialising Label controls, it just makes the XML file a bit bigger. If you change the text of your Label controls in code, you may want to remove the following line (and the corresponding closing bracket) from the AddChildControls method:

if (!(childCtrl is Label)) {

You can prevent other types of controls from being serialised as well by changing this line. For example, if you don't want the state of Button controls to be serialised (which is another fairly common case), you can just change the line to:

if (!(childCtrl is Label) && !(childCtrl is Button)) {

In truth, as disk space is so cheap nowadays, there isn't really any necessity to do this, but I left it in anyway. I used this code on a form with about 250 controls, and even including Label controls, the XML file only grew from about 77KB to about 92KB, so the extra file size is probably not worth worrying about.

Handling Different Types of Controls

All controls have the Visible property saved as this is a property that is common to all of them, and it's one I change often in code. The AddChildControls method handles different types of controls individually, so that control-specific properties can be serialised:

if (childCtrl is TextBox) {
  //...
} else if (childCtrl is ComboBox) {
  //...
} else if (childCtrl is ListBox) {
  //...
} else if (childCtrl is CheckBox) {
  //...
}

As the code stands now, it only handles the most common controls, and only saves the most important properties. It would be pretty easy to modify the code to handle other controls, and/or other properties, but the ones shown here were all I needed.

One point to note here is that the code does not save the list items for ComboBox or ListBox controls. This is because my application didn't populate these dynamically. If you need the list items saving as well, you would just use code very similar to the way the ListBox's SelectedIndex property is saved. As a ListBox can have more than one SelectedIndex, I used a loop to save them all. You could do the same with the list items if you needed this.

The Problem with SplitContainer Controls

After posting the initial version of this code, a comment was left (see below) that it didn't work with SplitContainer controls. As I had never used these, I had not spotted the problem.

It turns out that the SplitContainer control has two child Panels, which do not have a Name property. They are automatically named by the framework when you add the SplitContainer control to your form, but you can't set them yourself. As they don't have explicit names, the serialisation code was giving them an empty name when the XML file was written. This caused an error when the form was deserialised.

It turned out to be pretty easy to fix this, but it did require catching the SplitContainer control as a special case, which is something I don't like doing as a rule. All that was needed was when we serialise the form (at the end of the AddChildControls method), we need to check for the SplitContainer, and instead of serialising all child controls, pass the two Panel controls directly...

if (childCtrl is SplitContainer) {
  // handle this one as a special case
  AddChildControls(xmlSerialisedForm, ((SplitContainer)childCtrl).Panel1);
  AddChildControls(xmlSerialisedForm, ((SplitContainer)childCtrl).Panel2);
} else {
  AddChildControls(xmlSerialisedForm, childCtrl);
}

Then, when deserialising, if we are dealing with a SplitContainer control, the GetImmediateChildControl method needs to check for the parent of the parent, instead of just the parent...

private static Control GetImmediateChildControl(Control[] ctrl, Control currentCtrl) {
  Control c = null;
  for (int i = 0; i < ctrl.Length; i++) {
    if ((ctrl[i].Parent.Name == currentCtrl.Name)
     || (currentCtrl is SplitContainer && 
	ctrl[i].Parent.Parent.Name == currentCtrl.Name)) {
      c = ctrl[i];
      break;
    }
  }
  return c;
}

This fixed the problem.

Visible or Not Visible - That's a Very Interesting Question!

If you play with the test project, you'll see that clicking the "Are you happy?" CheckBox, causes the "happy" GroupBox to be shown or hidden. This is done with the following (pretty simple) event handler for the CheckBox's Click event:

private void chkHappy_CheckedChanged(object sender, EventArgs e) {
  grpHappy.Visible = chkHappy.Checked;
}

When I first wrote the code, I used the following line to serialise the Visible property for the current control:

xmlSerialisedForm.WriteElementString("Visible", childCtrl.Visible.ToString());

When I was playing with the test project included in the download, I noticed something odd. The test project has a child form that looks like this:

FormSerialisor1.gif

If you clear the "Are you happy?" CheckBox, the "happy" GroupBox is hidden. When the form is serialised, then deserialised, and the "Are you happy?" CheckBox checked, the form would look like this:

FormSerialisor2.gif

Notice that the "Why?" Label and the ComboBox next to it have both disappeared. Clicking the "Are you happy?" CheckBox shows and hides the GroupBox, but as the event handler code doesn't alter the visibility of the two child controls, they remain hidden. In a simple case like this, you easily get around the problem by manually setting the visibility of the child controls in the above event handler, but this has two drawbacks, one minor, and one significant.

The minor problem is that you have to write more code. This is little more than annoying, and shouldn't be necessary.

The major problem arises when you have child controls that may not necessarily be in the same state of visibility as the container. For example, in the application I was writing when I developed this serialisation code, I have many containers that contain subcontainers, or just individual controls whose visibility is set dynamically according to the state of other controls. In short, this means that some of the child controls will be visible, and some will not. There is no simple way to tell without running all of the relevant code again when the form is deserialised. This is just not practical in many cases, and would be very difficult to maintain in others.

After a lot of investigation, I discovered that when you set the Visible property of a container to false, the framework sets the Visible property of all contained controls to false as well. When the form is deserialised, all of these controls are hidden. When you click the CheckBox, only the GroupBox is made visible, leaving the other controls still hidden. This made me wonder how the framework knows the true state of the control's visibility, as without the serialisation/deserialisation issue, the child controls had their visibility set correctly when the container's visibility was set.

A friend of mine directed me to a discussion on the Stack Overflow site, where someone had asked a very similar question. An answer had been posted by someone who had followed the code up the control stack to see what the framework was actually doing. It seems that it uses an undocumented method called GetState with a parameter of 2 to get the true visibility.

I copied his code, and it worked fine. The serialisation of the Visible property now looks like this:

bool visible = (bool)typeof(Control).GetMethod("GetState",
                    BindingFlags.Instance | BindingFlags.NonPublic).Invoke(childCtrl,
                                                                 new object[] { 2 });
xmlSerialisedForm.WriteElementString("Visible", visible.ToString());

This is a little more complex, and uses an undocumented call (which makes me a little nervous), but it does work correctly.

The Problem of Multiple Controls with the Same Name

Whilst a Form, or any other container can only contain one control with the name TextBox1, there is nothing to stop a user control containing another control with the same name. As the enumeration of child controls used delves inside user controls (which is good, as it saves having to handle them separately), it does mean that at any point in the control hierarchy, you may have more than one control below you that has a particular name.

When deserialising, the code tries to find the right control to use with the following line:

Control[] ctrl = currentCtrl.Controls.Find(controlName, true);

If the control named controlName is the only one in this part of the control hierarchy with that name, then the ctrl array will only have one entry, and life is simple. However, if your form has a TextBox named TextBox1, and you have two instances of a user control on the form, and you added a TextBox to the user control and left the name as TextBox1, then you will have three controls in the hierarchy with the name TextBox1. The ctrl array will have three entries, and you have to decide which one is the right control.

My first solution to this was to count the length of the path up the hierarchy from ctrl[i] up to currentCtrl, and then choose the shortest one. After I wrote this, I then realised that (assuming the hierarchy has not changed between the form being serialised and deserialised), then the control you want must be an immediate child of the current container control. As only one control in this container can have the name you are looking for, the task of finding the right one just became much easier. All you do is loop through the ctrl array, looking for a control whose parent is the current container control. This was a much more elegant solution, and is encapsulated in the GetImmediateChildControl method.

What If Something Goes Wrong?

I considered this question quite a lot. My first version of the code had MessageBox calls in the cases where something went wrong, such as a control not being found during deserialisation, or the type in the XML file not matching the type of the control on the form. Obviously this is not a practical approach in general, as I wanted an all-purpose class that could be reused, and having MessageBoxes popping up isn't always the right thing to do (some would say it never is, but that's for another day). It was fine for development and debugging, but had to be changed for the final code.

I played around with throwing an exception in case something went wrong, but ended up throwing that idea out as it was getting too complex. In the end, I just ignored errors, meaning that if a control can't be found, or is the wrong type, then nothing is done. For me, this was the best solution. For people working in big teams, where you have little or no control over what other developers do, it may be better to add exceptions to this code, and warn other developers to catch them.

In practice, as long as the XML file can be read and written without problem, I never encountered any exceptions that weren't caused by my own bugs. Once I had those fixed, the code ran without error. The only time I can foresee any problems is if the control hierarchy changes, and you attempt to deserialise the form from an outdated XML file. In this case, some controls may not be repopulated. Next time the form is serialised, the data will be written correctly.

Note

The test project included in the download was written using Visual C# 2010 Express. If you can't open it in your version of Visual Studio, just copy the FormSerialisor.cs file into your project, and follow the instructions above to use it with your own test project. You don't need anything fancy. The code has been tested with the 2.0 and 4.0 versions of the .NET Framework and worked fine with both.

History

  • 8th June '10: Initial article written
  • 13th June '10: Code updated to cope with the odd behaviour of a SplitContainer (see the comment "controls in SplitContainer" below). I also added the FormSerialisor.cs file as a separate download for those who either don't have Visual C# 2010 Express, or who don't want the test project

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here