Question

I'm trying to implement a Load / Save function for a Windows Forms application.

I've got following components:

  • A tree view
  • A couple of list views
  • A couple of text boxes
  • A couple of objects (which holds a big dictionarylist)

I want to implement a way to save all of this into a file, and resume/load it later on.

What's the best way to do this?

I think XML serialization is the way to go, but I'm not quite sure how, or where to start. Or will it require a really complex solution to be able to do this?

Was it helpful?

Solution

Here's an example that binds an object and some ancestors to the UI; the use of C# 3.0 here is purely for brevity - everything would work with C# 2.0 too.

Most of the code here is setting up the form, and/or dealing with property-change notifications - importantly, there isn't any code devoted to updating the UI from the object model, or the object model from the UI.

Note also the IDE can do a lot of the data-binding code for you, simply by dropping a BindingSource onto the form and setting the DataSource to a type via the dialog in the property grid.

Note that it isn't essential to provide property change notifications (the PropertyChanged stuff) - however, most 2-way UI binding will work considerably better if you do implement this. Not that PostSharp has some interesting ways of doing this with minimal code.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Windows.Forms;
using System.Xml.Serialization;
static class Program { // formatted for vertical space
    [STAThread]
    static void Main() {
        Application.EnableVisualStyles();

        Button load, save, newCust;
        BindingSource source = new BindingSource { DataSource = typeof(Customer) };
        XmlSerializer serializer = new XmlSerializer(typeof(Customer));
        using (Form form = new Form {
            DataBindings = {{"Text", source, "Name"}}, // show customer name as form title
            Controls = {
                new DataGridView { Dock = DockStyle.Fill, // grid of orders
                    DataSource = source, DataMember = "Orders"},
                new TextBox { Dock = DockStyle.Top, ReadOnly = true, // readonly order ref
                    DataBindings = {{"Text", source, "Orders.OrderRef"}}},
                new TextBox { Dock = DockStyle.Top, // editable customer name
                    DataBindings = {{"Text", source, "Name"}}},
                (save = new Button { Dock = DockStyle.Bottom, Text = "save" }),
                (load = new Button{ Dock = DockStyle.Bottom, Text = "load"}),
                (newCust = new Button{ Dock = DockStyle.Bottom, Text = "new"}),   
            }
        })
        {
            const string PATH = "customer.xml";
            form.Load += delegate {
                newCust.PerformClick(); // create new cust when loading form
                load.Enabled = File.Exists(PATH);
            };
            save.Click += delegate {
                using (var stream = File.Create(PATH)) {
                    serializer.Serialize(stream, source.DataSource);
                }
                load.Enabled = true;
            };
            load.Click += delegate {
                using (var stream = File.OpenRead(PATH)) {
                    source.DataSource = serializer.Deserialize(stream);
                }
            };
            newCust.Click += delegate {
                source.DataSource = new Customer();
            };
            Application.Run(form);
        } 
    }
}

[Serializable]
public sealed class Customer : NotifyBase {
    private int customerId;
    [DisplayName("Customer Number")]
    public int CustomerId {
        get { return customerId; }
        set { SetField(ref customerId, value, "CustomerId"); }
    }

    private string name;
    public string Name {
        get { return name; }
        set { SetField(ref name, value, "Name"); }
    }

    public List<Order> Orders { get; set; } // XmlSerializer demands setter

    public Customer() {
        Orders = new List<Order>();
    }
}

[Serializable]
public sealed class Order : NotifyBase {
    private int orderId;
    [DisplayName("Order Number")]
    public int OrderId  {
        get { return orderId; }
        set { SetField(ref orderId, value, "OrderId"); }
    }

    private string orderRef;
    [DisplayName("Reference")]
    public string OrderRef {
        get { return orderRef; }
        set { SetField(ref orderRef, value, "OrderRef"); }
    }

    private decimal orderValue, carriageValue;

    [DisplayName("Order Value")]
    public decimal OrderValue {
        get { return orderValue; }
        set {
            if (SetField(ref orderValue, value, "OrderValue")) {
                OnPropertyChanged("TotalValue");
            }
        }
    }

    [DisplayName("Carriage Value")]
    public decimal CarriageValue {
        get { return carriageValue; }
        set {
            if (SetField(ref carriageValue, value, "CarriageValue")) {
                OnPropertyChanged("TotalValue");
            }
        }
    }

    [DisplayName("Total Value")]
    public decimal TotalValue { get { return OrderValue + CarriageValue; } }
}

[Serializable]
public class NotifyBase { // purely for convenience
    [field: NonSerialized]
    public event PropertyChangedEventHandler PropertyChanged;

    protected bool SetField<T>(ref T field, T value, string propertyName) {
        if (!EqualityComparer<T>.Default.Equals(field, value)) {
            field = value;
            OnPropertyChanged(propertyName);
            return true;
        }
        return false;
    }
    protected virtual void OnPropertyChanged(string propertyName) {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

OTHER TIPS

Ideally, you shouldn't be persisting the UI state; you should be persisting the state of some object model representing your data. With the exception of TreeView, it is fairly trivial to use data-binding to tie an object model to the UI. This could be either a DataTable-based approach, or a custom class hierarchy (my preference).

Once you have separated data from UI, saving data is simple. There are plenty of examples for XmlSerializer etc.

Yes, you should definitely use XML serialization for this. But as Marc Gravell noted, you must have objects that hold the data displayed by your GUI components first. Then you can practically make (de)serialization automatic, with minimum lines of code.

there is a issue with the sample above. consider that eventually your application is updated. your object model could change drastically and therefore it could not get deserialized. there are some things you could do to ensure that a deserialization from xml version 1 can be deserialized to your object model in version 2 but if there is the possibility that you can have big structural changes xml deserialization is not the way to go.

if this is the case and your application is deployed to customers i would strongly suggest to take a further look at your save/load logic.

Versionized Serialization/Deserialization
serialize your object state in the form of:

<ObjectState version="1">
    <Field1>value</Field1>
    ... etc ...
</ObjectState>

so now you have the version of the object model that generated the saved state. in your deserialization you can take special measurements to accomodate for this fact. for example write the Field1-Value in a list in some other object.

another approach would be:

Versionized Serialization and Conversion before Deserialization
serialize your object state as mentioned above (with an version attribute).
when deserializing look at the version attribute if this is not the version you expect convert the serialized object state with xsl-scripts or c# code to your current version. you can save a list of xsl conversions in your current project

- conversions
    - v1-v2
    - v2-v3

if you are currently in version 3 and want to load your xml file look at the version attribute and run all xsl scripts to get your current version (version 3). so you would run xsl-script v1-v2 and afterwards v2-v3.

in this case you can have normal serialization and deserialization classes that must not care about backwards capability.

it is fairly trivial to use data-binding to tie an object model to the UI.

How can I tie an object with a GUI control without a persistent storage? If I do it manually that means I have to write ridiculous amount of code for every single object in memory. I already have some sort of class storage for this data but it's not bind sort scenario, it's like read this write here.

Am I supposed to write a loader which loads serialized XML and get the object and then read the object and fill up the whole GUI? Obviously this more like manual loading not binding. Am I missing something ?

Here is a great article on how to make your a class or struct serializable. I would create a class that will allow you to store all the data you wish. Make the class serialable. This way in just a few lines of code you can save all your data to a file. Then with just a few more lines of code you can retrieve the data from the file.

http://www.codeproject.com/KB/cs/objserial.aspx

An alternative to serializing your classes is to use the an ADO.NET Dataset for data storage which has built in facilities for persisting to an XML file. The code will be minimal, and you can store only the relevant data you need by designing tables that fit the model of the operation you are performing. Additionally, you will be able to use the same code if you decide to later persist the UI state to a database instead of a local file. You would only need to have an alternate function to save the Dataset.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top