简体   繁体   中英

ApplicationSettingsBase writes empty tag for custom collection

I have been fighting with this off and on for several days now. I need to store a collection of custom objects as part of the user's settings. Based on lots of google work it seems that building a preferences class off of ApplicationSettingsBase is an appropriate way to do so. The problem I run into is as soon as I try to store a collection of a custom type no data gets saved for that property. If I keep to collections of base types, such as string, things work. I have a separate proof of concept project I have been working with for the past day to isolate down to just this issue.

This project consists of a WPF window with a listbox and two buttons, “+” and “-“. I have a Prefs class with three properties defining different types of collections. In my window code I bind the listbox to one of these lists and the buttons either add or remove an item to/from the list. Closing the window should save the contents of the list to the users.config file for the current user. Reopening it should display the saved contents of the list.

If I use List1 ( ObservableCollection<string> ) and click the + button a few times then close the window the data is correctly saved to user.config. However if I change and use List2( ObservableCollection<Foo> ) or List3( FooCollection ) I just end up with an empty value tag. I have tried to implement ISerializable and IXmlSerializable in attempts to get this working with no change in behavior. In fact running in debug mode with break points shows that the collection contains data when the save method is called but the serialization interface methods are never being called.

What am I missing?

UPDATE: I have changed my approach and written the data off to another file alongside of user.config for the time being so that I can make some progress on other portions of the app. But I would still like to know why application settings base was failing to record my collection data.

Here is all of the relevent code, if someone thinks an ommitted section is important I will add it back into the post.

user.config after adding a couple of elements to each list property

<setting name="List1" serializeAs="Xml">
    <value>
        <ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema">
            <string>0</string>
            <string>1</string>
            <string>2</string>
        </ArrayOfString>
    </value>
</setting>
<setting name="List2" serializeAs="Xml">
    <value />
</setting>
<setting name="List3" serializeAs="Xml">
    <value />
</setting>

Window XAML

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition Width="Auto"></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <ListBox Name="l1" Grid.Column="0" Grid.Row="0" ItemsSource="{Binding}"></ListBox>
    <StackPanel Grid.Column="1" Grid.Row="0">
        <Button Name="bP" Margin="5" Padding="5" Click="bP_Click">+</Button>
        <Button Name="bM" Margin="5" Padding="5" Click="bM_Click">-</Button>
    </StackPanel>

</Grid>

Code Behind for Window

public partial class Window1 : Window
{
    Prefs Prefs = new Prefs();

    public Window1()
    {
        InitializeComponent();

        //l1.DataContext = Prefs.List1;
        //l1.DataContext = Prefs.List2;
        l1.DataContext = Prefs.List3;
    }

    private void Window_Closed(object sender, EventArgs e)
    {
        Prefs.Save();
    }

    private void bP_Click(object sender, RoutedEventArgs e)
    {
        //Prefs.List1.Add(Prefs.List1.Count.ToString());
        //Prefs.List2.Add(new Foo(Prefs.List2.Count.ToString()));
        Prefs.List3.Add(new Foo(Prefs.List3.Count.ToString()));
    }

    private void bM_Click(object sender, RoutedEventArgs e)
    {
        //Prefs.List1.RemoveAt(Prefs.List1.Count - 1);
        //Prefs.List2.RemoveAt(Prefs.List2.Count - 1);
        Prefs.List3.RemoveAt(Prefs.List3.Count - 1);
    }
}

Prefs class

class Prefs : ApplicationSettingsBase
{
    [UserScopedSettingAttribute()]
    [DefaultSettingValueAttribute(null)]
    public System.Collections.ObjectModel.ObservableCollection<string> List1
    {
        get
        {
            System.Collections.ObjectModel.ObservableCollection<string> Value = this["List1"] as System.Collections.ObjectModel.ObservableCollection<string>;
            if (Value == null)
            {
                Value = new System.Collections.ObjectModel.ObservableCollection<string>();
                this["List1"] = Value;
            }
            return Value;
        }
    }

    [UserScopedSettingAttribute()]
    [DefaultSettingValueAttribute(null)]
    public System.Collections.ObjectModel.ObservableCollection<Foo> List2
    {
        get
        {
            System.Collections.ObjectModel.ObservableCollection<Foo> Value = this["List2"] as System.Collections.ObjectModel.ObservableCollection<Foo>;
            if (Value == null)
            {
                Value = new System.Collections.ObjectModel.ObservableCollection<Foo>();
                this["List2"] = Value;
            }
            return Value;
        }
    }

    [UserScopedSettingAttribute()]
    [DefaultSettingValueAttribute(null)]
    public FooCollection List3
    {
        get
        {
            FooCollection Value = this["List3"] as FooCollection;
            if (Value == null)
            {
                Value = new FooCollection();
                this["List3"] = Value;
            }
            return Value;
        }
    }
}

Foo Class

[Serializable()]
class Foo : System.ComponentModel.INotifyPropertyChanged, ISerializable, IXmlSerializable
{
    private string _Name;
    private const string PropName_Name = "Name";
    public string Name
    {
        get { return this._Name; }
        set
        {
            if (value != this._Name)
            {
                this._Name = value;
                RaisePropertyChanged(Foo.PropName_Name);
            }
        }
    }
    public override string ToString()
    {
        return Name;
    }

    public Foo() { }
    public Foo(string name)
    {
        this._Name = name;
    }

    #region INotifyPropertyChanged Members
/***Omitted for space***/
    #endregion

    #region ISerializable Members
    public Foo(SerializationInfo info, StreamingContext context)
    {
        this._Name = (string)info.GetValue(Foo.PropName_Name, typeof(string));
    }
    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue(Foo.PropName_Name, this._Name);
    }
    #endregion

    #region IXmlSerializable Members
    public System.Xml.Schema.XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(System.Xml.XmlReader reader)
    {
        reader.MoveToContent();
        _Name = reader.GetAttribute(Foo.PropName_Name);
        bool Empty = reader.IsEmptyElement;
        reader.ReadStartElement();
        if (!Empty)
        {
            reader.ReadEndElement();
        }
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
        writer.WriteAttributeString(Foo.PropName_Name, _Name);
    }
    #endregion
}

and FooCollection class

[Serializable()]
class FooCollection : ICollection<Foo>, System.ComponentModel.INotifyPropertyChanged, INotifyCollectionChanged, ISerializable, IXmlSerializable
{
    List<Foo> Items;
    private const string PropName_Items = "Items";

    public FooCollection()
    {
        Items = new List<Foo>();
    }

    public Foo this[int index]
    {
/***Omitted for space***/
    }

    #region ICollection<Foo> Members
/***Omitted for space***/
    #endregion

    public void RemoveAt(int index)
    {
/***Omitted for space***/
    }

    #region IEnumerable Members
/***Omitted for space***/
    #endregion

    #region INotifyCollectionChanged Members
/***Omitted for space***/
    #endregion

    #region INotifyPropertyChanged Members
/***Omitted for space***/
    #endregion

    #region ISerializable Members
    public FooCollection(SerializationInfo info, StreamingContext context)
    {
        this.Items = (List<Foo>)info.GetValue(FooCollection.PropName_Items, typeof(List<Foo>));
    }
    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue(FooCollection.PropName_Items, this.Items);
    }
    #endregion

    #region IXmlSerializable Members
    public System.Xml.Schema.XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(System.Xml.XmlReader reader)
    {
        XmlSerializer FooSerializer = new XmlSerializer(typeof(Foo));

        reader.MoveToContent();
        bool Empty = reader.IsEmptyElement;
        reader.ReadStartElement();
        if (!Empty)
        {
            if (reader.IsStartElement(FooCollection.PropName_Items))
            {
                reader.ReadStartElement();

                while (reader.IsStartElement("Foo"))
                {
                    this.Items.Add((Foo)FooSerializer.Deserialize(reader));
                }

                reader.ReadEndElement();
            }

            reader.ReadEndElement();
        }
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
        XmlSerializer FooSerializer = new XmlSerializer(typeof(Foo));

        writer.WriteStartElement(FooCollection.PropName_Items);

        foreach (Foo Item in Items)
        {
            writer.WriteStartElement("Foo");
            FooSerializer.Serialize(writer, Item);
            writer.WriteEndElement();//"Foo"
        }

        writer.WriteEndElement(); //FooCollection.PropName_Items
    }
    #endregion
}

I also had a similar problem, which I managed to fix, but I was not using an ObservableCollection , but a normal List<Column> , Column being a class of mine, that contained as public members: string, int and bool, which are all xmlserializable. The List<> , since it implement the IEnumerable is also XMLSerializable .

The only things you have to take care of in the custom Foo Class are: you must have the members as public, a parameterless constructor and the class itself has to be public. You do not need to add the [Serializable] tag for xml serialization.

I did not need to implement a FooCollection class, since I am using List which has no issue with xml serialization.

Another thing:

  • the class that derives from ApplicationSettingsBase can be internal sealed - no need for it to be public.
  • above the list prop add the fact that it is xml serializable:

    [global::System.Configuration.UserScopedSettingAttribute()] [SettingsSerializeAs(SettingsSerializeAs.Xml)] [global::System.Configuration.DefaultSettingValueAttribute("")] public List Columns { get { return ((List)this["Columns"]); } set { this["Columns"] = (List)value; } }

If this does not work you can also try to implement a TypeConverter .

I struggled with a very similar issue for two days as well, but now I found a missing link which might help here. If your and my case are actually similar, then the classes 'Foo' and 'FooCollection' need to be explicitly public!

I assume, that in case of List2( ObservableCollection<Foo> ) and List3(FooCollection) the .NET runs into IXmlSerializable which requires somehow the explicit public access (crosses the border of the own assembly).

Option List1 ( ObservableCollection<string> ) in the opposite seems to run on a flat string-Typeconversion which is happy with the (internal) class 'Foo'...

(From ApplicationsettingsBase Docu: There are two primary mechanisms that ApplicationSettingsBase uses to serialize settings: 1) If a TypeConverter exists that can convert to and from string, we use it. 2) If not, we fallback to the XmlSerializer )

Kind regards, Rudy

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM