5

In .NET you can select a hashtable as type for a usersetting. However when I save it and retrieve it in this way, it doesnt seem to have saved it at all.

Hashtable t = new Hashtable();
t.Add(1,"Foo");
t.Add(2,"Bar");
Properties.Settings.Default.Setting = t;
Properties.Settings.Default.Save();

if(Properties.Settings.Default.Setting != null)
        foreach (DictionaryEntry entry in Properties.Settings.Default.Setting)
        {
            MessageBox.Show(entry.Key + " " + entry.Value);
        }

Why doesnt it serialize it in the usersettings, when I can clearly select that type in Visual studio? I would understand if this was the case with an unlisted type such as dictionary, but Hashtable is listed. How do I solve this problem?
Simplicity and efficiency in this order have the highest priority for me.

Many Thanks, Kave


update:

@Joao , Many Thanks the Binary solution. I find it quite interesting, its clean. One disadvavtage with serializing it as binary might be the fact that you cant change anything in the usersetting file manually anymore. but I think that will be done very rarely anyway, so its a good solution.

I was thinking of a different approach to create an "XMLSetting" field of type string in the user scope and use this code to store and retrieve the values as an XMl file serialized into a hashtable. But I am sure this is not the best way, does anyone know a better way to serialize a hashtable/dictionary as xml in the usersettings, other than what i am doing below?

if(string.IsNullOrEmpty(Properties.Settings.Default.XMLSetting))
            {
                Console.WriteLine("Usersettings is empty. Initializing XML file...");
                XmlDocument doc = new XmlDocument();
                XmlElement hashtable = doc.CreateElement("HashTable");
                doc.AppendChild(hashtable);

                GenerateValues(doc, hashtable, "1", "Foo");
                GenerateValues(doc, hashtable, "2", "Bar");

                Properties.Settings.Default.XMLSetting = doc.OuterXml;
                Properties.Settings.Default.Save();
            }
            else
            {
                Console.WriteLine("Retrieving existing user settings...");
                XmlDocument doc = new XmlDocument();
                doc.LoadXml(Properties.Settings.Default.XMLSetting);

                Hashtable hashtable = new Hashtable();

                foreach (XmlNode entry in doc.DocumentElement.ChildNodes)
                {
                    hashtable.Add(int.Parse(entry.FirstChild.InnerText), entry.FirstChild.NextSibling.InnerText);
                }

                foreach (DictionaryEntry entry in hashtable)
                {
                    Console.WriteLine(entry.Key + " " + entry.Value);
                }
            }

private static void GenerateValues(XmlDocument doc, XmlElement hashtable, string skey, string svalue)
        {
            XmlElement entry = doc.CreateElement("entry");
            XmlElement key = doc.CreateElement("Key");
            XmlElement value = doc.CreateElement("Value");
            entry.AppendChild(key);
            entry.AppendChild(value);

            key.AppendChild(doc.CreateTextNode(skey));
            value.AppendChild(doc.CreateTextNode(svalue));

            hashtable.AppendChild(entry);
        }
Houman
  • 64,245
  • 87
  • 278
  • 460
  • I have just tested persisting an Hashtable as user setting and detected no problems. If you haven't done it already, try to do it from scratch in a new test project. – João Angelo Nov 21 '09 at 12:12
  • Hi, I just have tried it again. It keep ssaying the Properties.Settings.Default.Setting stays as null, even after i saved and restarted the application. It works with other types such string though. How did you get it work? Can you post your solution somehow? – Houman Nov 21 '09 at 16:50
  • I was to hasty in my tests. The Hashtable is not persisted between application runs with the default settings class. However if it's vital for you to have a Hashtable and don't mind some manual work, you can check my answer. – João Angelo Nov 21 '09 at 21:58

1 Answers1

12

The Hashtable does not support serialization to XML nor I believe to a simple string. These are the two serialization options available when you use a Settings.settings file and the associated auto-generated class.

However if you create your settings class by yourself and also manage the App.Config section you can persist an Hastable by using Binary serialization.

See the following example. It's a console application with following files:

App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup 
      name="userSettings" 
      type="System.Configuration.UserSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
      <section 
        name="ConsoleApplication1.MyCustomSettings" 
        type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" 
        allowExeDefinition="MachineToLocalUser" 
        requirePermission="false" />
    </sectionGroup>
  </configSections>
  <userSettings>
    <ConsoleApplication1.MyCustomSettings>
      <setting name="MyHashtable" serializeAs="Binary">
        <value></value>
      </setting>
      <setting name="MyBackColor" serializeAs="String">
        <value>Silver</value>
      </setting>
    </ConsoleApplication1.MyCustomSettings>
  </userSettings>
</configuration>

Custom Settings Class created manually:

public class MyCustomSettings : ApplicationSettingsBase
{
    private static MyCustomSettings defaultInstance = (
        (MyCustomSettings)
        (ApplicationSettingsBase.Synchronized(new MyCustomSettings())));

    public static MyCustomSettings Default
    {
        get { return defaultInstance; }
    }

    [UserScopedSettingAttribute()]
    [DebuggerNonUserCodeAttribute()]
    [DefaultSettingValueAttribute("Silver")]
    public Color MyBackColor
    {
        get { return ((Color)(this["MyBackColor"])); }
        set { this["MyBackColor"] = value; }
    }

    [UserScopedSettingAttribute()]
    [DebuggerNonUserCodeAttribute()]
    [SettingsSerializeAs(SettingsSerializeAs.Binary)]
    public Hashtable MyHashtable
    {
        get { return ((Hashtable)(this["MyHashtable"])); }
        set { this["MyHashtable"] = value; }
    }
}

Program.cs

class Program
{
    static void Main(string[] args)
    {
        // For the first time no Hastable will exist.
        // Create one with the default values
        if (MyCustomSettings.Default.MyHashtable == null)
        {
            Console.WriteLine("Initializing Hashtable...");

            MyCustomSettings.Default.MyHashtable = new Hashtable();

            MyCustomSettings.Default.MyHashtable.Add(1, "foo");
            MyCustomSettings.Default.MyHashtable.Add(2, "bar");

            MyCustomSettings.Default.Save();
        }

        foreach (DictionaryEntry entry in MyCustomSettings.Default.MyHashtable)
        {
            Console.WriteLine(entry.Key + ": " + entry.Value);
        }

        Console.ReadKey();
    }
}

Update: If you want a human readable representation of the data, the approach you're using seems reasonable. Nonetheless you can also try a different approach that better encapsulates the logic of converting to string (XML) and from string (XML).

This approach allows you to use the IDE support for Settings.settings file removing the need to generate a custom setting class or messing with App.config.

You just need to implement a custom class that will hold your data, in my example I will inherit this class from a StringDictionary and also implement a TypeConverter that the settings system will use to persist the data in string format.

[TypeConverter(typeof(StringDictionaryTypeConverter))]
public class MyStringDictionary : StringDictionary
{
}

public class StringDictionaryTypeConverter : TypeConverter
{
    public override bool CanConvertFrom(
        ITypeDescriptorContext context, 
        Type sourceType)
    {
        if (sourceType.Equals(typeof(string)))
        {
            return true;
        }

        return base.CanConvertFrom(context, sourceType);
    }

    public override bool CanConvertTo(
        ITypeDescriptorContext context, 
        Type destinationType)
    {
        if (destinationType.Equals(typeof(string)))
        {
            return true;
        }

        return base.CanConvertTo(context, destinationType);
    }

    public override object ConvertFrom(
        ITypeDescriptorContext context, 
        CultureInfo culture, 
        object value)
    {
        if (value is string)
        {
            MyStringDictionary sd = new MyStringDictionary();

            XDocument xs = XDocument.Load(new StringReader(value as string));

            foreach (var item in xs.Descendants("entry"))
            {
                sd.Add(item.Element("key").Value, item.Element("value").Value);
            }

            return sd;
        }

        return base.ConvertFrom(context, culture, value);
    }

    public override object ConvertTo(
        ITypeDescriptorContext context, 
        CultureInfo culture, 
        object value, 
        Type destinationType)
    {
        if (destinationType.Equals(typeof(string)))
        {
            MyStringDictionary sd = value as MyStringDictionary;

            StringBuilder sb = new StringBuilder();

            sb.Append("<entries>");
            foreach (DictionaryEntry item in sd)
            {
                sb.AppendFormat(
                    "<entry><key>{0}</key><value>{1}</value></entry>", 
                    item.Key, 
                    item.Value);
            }
            sb.Append("</entries>");

            return sb.ToString();
        }

        return base.ConvertTo(context, culture, value, destinationType);
    }
}

Now you just need to use the MyStringDictionary class as the data type of your settings. Due to Visual Studio not displaying user classes in the available data types for a user setting you need to do a one time workaround that consists of opening the Settings.settings file with the XML editor (Right-click and Open Width) and manually specify the type of the user setting as the full name of MyStringDictionary.

Hope this helps.

João Angelo
  • 56,552
  • 12
  • 145
  • 147
  • This works now many thanks. I have added also a new solution to this problem. What do you think about that? Any optimization suggestions? – Houman Nov 22 '09 at 13:12
  • Thanks alot. This worked as well. Now we have three solutions to the problem. :o) – Houman Nov 23 '09 at 13:03
  • 1
    here is an implementation that's actually a bit nicer and uses 'real' XML instead of manually entering the etc stuff: http://bytes.com/topic/c-sharp/answers/492245-complex-data-types-user-scoped-applicationsettings – stijn Nov 01 '11 at 08:56
  • For the last bit, to get the new type as the data type for you settings, first build the app, then after you've clicked Browse... in the Type column in the Settings.settings screen, you get the 'Select a Type' window. Try typing the fully-qualified type name into the 'Selected type' field and clicking OK. It worked for me. – ssg31415926 Aug 20 '12 at 14:38