views:

656

answers:

1

Hello,

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);
        }
+2  A: 

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
This works now many thanks. I have added also a new solution to this problem. What do you think about that? Any optimization suggestions?
Kave
Check my update...
João Angelo
Thanks alot. This worked as well. Now we have three solutions to the problem. :o)
Kave