0

I have a C# application with a settings field that is a collection of custom objects.

When I start the application I create some class instances that take instances from the settings entry collection and keep a reference to them internally and modify them. While debugging I saw that changes done through these external references are not reflected when I callSettings.Default.Save() but changes done by directly accessing the properties likeSettings.Default.<property> work fine.

I looked up the code responsible for Save() and saw that the implementation actually checks a SettingsPropertyValue.IsDirty field to decide whether to or not to serialize it. Of course when I access the external references to the objects in the settings field that value is not set.

Is there any lightweight solution to this?

I don't think I'm the first person to encounter this. One way I can think of is implementing the IsDirty property in the collections I serialize and add INotifyPropertyChanged interface event PropertyChanged for all the contained instances so that the container is being notified of changes and can reflect them to the actual settings property. But that means wrapping each of the settings classes around with this logic. So what I am asking for is if there is a lightweight solution to this found by anyone else who has encountered this issue.

Example

Consider this class:

namespace SettingsTest
{
    [DataContract(Name="SettingsObject")]
    public class SettingsObject
    {
        [DataMember]
        public string Property { get; set; }
    }
}

And the following program:

namespace SettingsTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var settingsObjects = new List<SettingsObject>();
            var settingsObject = new SettingsObject{Property = "foo"};
            settingsObjects.Add(settingsObject);
            Settings.Default.SettingsObjects = settingsObjects;
            Settings.Default.Save();
            settingsObject.Property = "bar";
            Settings.Default.Save();
        }
    }
}

After the second Save() call the final output in user.config file is:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <userSettings>
        <SettingsTest.Settings>
            <setting name="SettingsObjects" serializeAs="Xml">
                <value>
                    <ArrayOfSettingsObject xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                        xmlns:xsd="http://www.w3.org/2001/XMLSchema">
                        <SettingsObject>
                            <Property>foo</Property>
                        </SettingsObject>
                    </ArrayOfSettingsObject>
                </value>
            </setting>
        </SettingsTest.Settings>
    </userSettings>
</configuration>

As you can see the modification to the property via the external reference to the SettingsObject instance was not persisted.

abatishchev
  • 98,240
  • 88
  • 296
  • 433
Rudolfs Bundulis
  • 11,636
  • 6
  • 33
  • 71
  • _"Is there any general solution to this?"_ -- it's not clear what you mean by that question. Obviously, the settings property can't know it's been changed without some mechanism to do so; at the very least, you would need to add a mechanism in your custom settings objects, since C# objects don't come with this by default. You also seem to be aware of the basic constraints surrounding the problem. Please note that it's not clear from your question how you set up your own settings properties. Providing [a good, _minimal_, _complete_ code example](http://stackoverflow.com/help/mcve) would help. – Peter Duniho May 11 '15 at 21:21
  • @PeterDuniho Ok I'll reformulate my question then - what I want is to get feedback from others who have encountered this on what is the most lightweight example for solving this (in contrary to hooking up a `INotifyPropertyChanged` interface in each of my settings objects). Isn't the provided code sample clear enough? It demonstrates exactly what I am doing in my application. – Rudolfs Bundulis May 11 '15 at 22:46
  • @PeterDuniho the main issue is that I don't have a strong background in C# and I am actually not aware of all the possibilities of the language. I mean, maybe there is a way to solve this using attributes, maybe this can be solved by inheriting from `SettingsPropertyValue` (but then I am not sure how to make the settings aware of all the children) and so on. What I wanted was feedback from more experienced developers, maybe there are no better options to tackle this than doing the `INotifyPropertyChanged` thing in which case you comment is the answer I needed. – Rudolfs Bundulis May 11 '15 at 23:00
  • Quick question, why the DataContract/DataMember attributes? "Since a lot of programmers were overwhelmed with the [DataContract] and [DataMember] attributes, with .NET 3.5 SP1, Microsoft made the data contract serializer handle all classes - even without any of those attributes - much like the old XML serializer." (from: http://stackoverflow.com/questions/4836683/when-to-use-datacontract-and-datamember-attributes) – Ryan Buddicom May 12 '15 at 00:39
  • _"Isn't the provided code sample clear enough"_ -- I guess it depends on how many people you want to look seriously at the question. Me, it's been many years since I've had to support a custom object, never mind a collection, in a `Settings` object, and your post doesn't contain enough context to reproduce that from scratch (not even a link to a reference explaining the technique you've used here). For now, you're limiting your audience to those who already know such a technique without looking it up, along with those with the patience to do _that_ research before considering your question. – Peter Duniho May 12 '15 at 07:00
  • @PeterDuniho well here you are totally right and I couldn't agree more, but my intention wasn't to make anyone spend time to look all this up if they are not familiar with all this:) As I said, I have an ugly solution that works and my main and only interest is to understand if anyone has solved it in a nicer way. I mean I do not wan't to make anyone waste their time. But thanks for the point, totally on spot. – Rudolfs Bundulis May 12 '15 at 07:21
  • _"my intention wasn't to make anyone spend time to look all this up"_ -- no doubt. And for that matter, it wouldn't matter even if it were your intention. You can't make anyone here do anything they don't want to do. :) But you can enhance your chances of getting a useful answer by providing as much useful detail in your question as you can. There are experienced C# programmers around who may have forgotten important specifics, but who could still provide useful input given half a chance and a bit of a jog of their memory. – Peter Duniho May 12 '15 at 07:39
  • @PeterDuniho I basically copied and pasted that sample directly into VS and spend 10 minutes mucking around with the answer I gave (most of which was the commenting haha) I personally didn't think the code sample was 100% clear but it was certainly better than a number of other questions ;) I had to solve this problem within the last year but I never personally put a collection into settings and have only had to do this once for a client. – Ryan Buddicom May 12 '15 at 23:06

1 Answers1

1

You seem to be covering a bunch of issues with this question. As you correctly said "changes done by directly accessing the properties likeSettings.Default. work fine." Modifying in memory references will not cause the settings to be updated automatically as you wanted. INotifyProperty changed is one solution.

If you want a quick and dirty method of serializing these objects into your settings file you can use the following code.

Objects involved:

/// <summary>Generic class to support serializing lists/collections to a settings file.</summary>
[Serializable()]
public class SettingsList<T> : System.Collections.ObjectModel.Collection<T> 
{
    public string ToBase64()
    {
        // If you don't want a crash& burn at runtime should probaby add 
        // this guard clause in: 'if (typeof(T).IsDefined(typeof(SerializableAttribute), false))'
            using (var stream = new MemoryStream())
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(stream, this);
                stream.Position = 0;
                byte[] buffer = new byte[(int)stream.Length];
                stream.Read(buffer, 0, buffer.Length);
                return Convert.ToBase64String(buffer);
            }
    }
    public static SettingsList<T> FromBase64(string settingsList)
    {
        using (var stream = new MemoryStream(Convert.FromBase64String(settingsList)))
        {
            var deserialized = new BinaryFormatter().Deserialize(stream);

            return (SettingsList<T>)deserialized;
        }
    }  
}
[Serializable()]
public class SettingsObject
{
    public string Property { get; set; }

    public SettingsObject()
    {
    }
}

The main method demonstrates the issue you are facing, with a solution.

class Program
{
    static void Main(string[] args)
    {
        // Make sure we don't overwrite the previous run's settings.
        if (String.IsNullOrEmpty(Settings.Default.SettingsObjects))
        {
            // Create the initial settings.
            var list = new SettingsList<SettingsObject> { 
                new SettingsObject { Property = "alpha" }, 
                new SettingsObject { Property = "beta" }
            };
            Console.WriteLine("settingsObject.Property[0] is {0}", list[0].Property);

            //Save initial values to Settings
            Settings.Default.SettingsObjects = list.ToBase64();
            Settings.Default.Save();

            // Change a property
            list[0].Property = "theta";

            // This is where you went wrong, settings will not be persisted at this point
            // because you have only modified the in memory list.

            // You need to set the property on settings again to persist the value.
            Settings.Default.SettingsObjects = list.ToBase64();
            Settings.Default.Save();
        }
        // pull that property back out & make sure it saved.
        var deserialized = SettingsList<SettingsObject>.FromBase64(Settings.Default.SettingsObjects);
        Console.WriteLine("settingsObject.Property[0] is {0}", deserialized[0].Property);            

        Console.WriteLine("Finished! Press any key to continue.");
        Console.ReadKey();
    }

}

So all I'm doing is storing your whole list of objects as a base64 encoded string. Then we deserialize on the next run.

From what I understand of the question you want to only hold in memory references without hitting the settings object. You could simply run some code at the end of main to persist this list and any other setting you need to. Any changes will still be in memory as long as you hold a reference the object & will remain around until you terminate the application.

If you need the settings to be saved while the application is running just create a Save() method of your own & call it from the end of Main() as well when the user performs an action that requires saving the settings. e.g.

public static void SaveSettings(SettingsList list)
{
    Settings.Default.SettingsObjects = list.ToBase64();
    Settings.Default.Save();
}

Edit: One caveat as mentioned in the comments below.

From my benchmarks this method is very slow, meaning it's not a good idea to persist large object lists in settings this way. Once you have more than a handful of properties you might want to look at an embedded database like SQLite. A CSV, INI or XML file could also be an option here for large numbers of trivial settings. One benefit of simpler storage formats is easy modification by non-developers eg csv in excel. Of course this may not be what you want ;)

Ryan Buddicom
  • 1,131
  • 15
  • 30
  • 1
    Yeah, reassigning the properties to themselves was also something that came to my mind. If nothing better comes along I guess I may as well accept this. Only about the thing you mentioned on using `DataContract`, I actually use the additional attributes to define the node names and other stuff at which point it comes in handy. – Rudolfs Bundulis May 12 '15 at 07:25
  • @RudolfsBundulis: note that you can always change the accepted answer later anyway, if one comes along that you think is better. So if you feel that the above answer directly and usefully answers your question as stated, there's no harm at all in accepting it. Don't think you're under any obligation to accept _any_ answer, but at the same time if you find at least one answer does in fact address your question usefully, accepting the best one is the right thing to do (and if there's only one such answer, well...there's the "best one" right there :) ). – Peter Duniho May 12 '15 at 07:43
  • Either way you should be able to get the functionality you desire with this method but as Peter said if a better answer comes along feel free to jump ship :) One thing I didn't try was assigning the settings property to a variable and then manipulating x followed by saving changes. e.g. doing this: `var x = Settings.Default.SettingsObjects;` – Ryan Buddicom May 12 '15 at 22:58
  • And just FYI this is REALLY slow from my benchmarks so you probably don't wanna persist large objects. Depending on how many settings you have exactly you might want to look at an embedded db like SQLite or even simple CSV files (imitating tables in a RDBMS) – Ryan Buddicom May 12 '15 at 23:13
  • @Hellfire, yeah, I guess we'll move away and most probably use SQLite as a persistence backend. That would solve other issues as well. – Rudolfs Bundulis May 13 '15 at 08:18
  • I ended up doing that for the project where I wrote this code initially. Its a pretty sweet little package, SQLite. Highly recommended ;) – Ryan Buddicom May 13 '15 at 09:16