2

I'm trying to implement a custom configuration section so that I can load a list of user defined items and not having much luck. I've read through the following posts, but I still can't figure out what it is that I'm doing wrong. They seem to be good guides, but I'm missing some important fact here. I'm hoping someone can point out exactly what.

This is my test. When I step through it, config remains null. It's like the call to GetSection does nothing at all.

[TestClass]
public class ToDoConfigTests
{
    [TestMethod]
    public void TestGetTodoAttribute()
    {
        var config = ConfigurationManager.GetSection("ToDoListAttributesSection") as ToDoItemsConfigurationSection;

        Assert.Fail();

    }
}

My Configuration Classes:

using System.Configuration;
using Rubberduck.ToDoItems;

namespace Rubberduck.Config
{

    public class ToDoItemsConfigurationCollection : ConfigurationElementCollection
    {
        protected override ConfigurationElement CreateNewElement()
        {
            return new ToDoListAttributeElement();
        }

        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((ToDoListAttributeElement)element).Comment;
        }
    }

    public class ToDoItemsConfigurationSection : ConfigurationSection
    {
        [ConfigurationProperty("ToDoListAttributes", IsRequired = true, IsDefaultCollection=true)]
        public ToDoItemsConfigurationCollection ToDoListAttributes
        {
            get { return (ToDoItemsConfigurationCollection)this["ToDoListAttributes"]; }
            set { this["ToDoListAttributes"] = value; }
        }
    }

    public class ToDoListAttributeElement : ConfigurationElement
    {
        [ConfigurationProperty("TaskPriority", DefaultValue = TaskPriority.Low, IsRequired = true)]
        public TaskPriority Priority
        {
            get { return (TaskPriority)this["TaskPriority"]; }
            set { this["TaskPriority"] = value; }
        }

        [ConfigurationProperty("Comment",IsKey=true, IsRequired = true)]
        public string Comment
        {
            get { return (string)this["Comment"]; }
            set { this["Comment"] = value; }
        }
    }
}

Finally, the app.config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
        <sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
            <section name="Rubberduck.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
        </sectionGroup>
        <section name="ToDoListAttributesSection" type="Rubberduck.Config.ToDoItemsConfigurationSection, Rubberduck.Config"/>
    </configSections>
    <ToDoListAttributesSection>
      <ToDoListAttributes>
        <add Comment="note" TaskPriority="0" />
        <add Comment="todo" TaskPriority="1" />
        <add Comment="bug" TaskPriority="2"/>
      </ToDoListAttributes>
    </ToDoListAttributesSection>
</configuration>
Community
  • 1
  • 1
RubberDuck
  • 11,933
  • 4
  • 50
  • 95

2 Answers2

1

The secret to why this wasn't working has to do with a detail about my software that I did not share back when I wrote this question.

My application is not a stand alone application. It is a COM add-in for the VBA Editor. As such, it is a *.dll file, not an *.exe. App.config files only work with the executing assembly (the *.exe), so that is why my code was not working. There are a few good solutions here, but I ended up rolling my own configuration using XML Serialization.

Below is the code I ended up using. It can also be found in the Rubberduck repository hosted on GitHub if you prefer looking at it there.

Central to the solution is the IConfigurationService interface and the ConfigurationLoader implementation that allows me to read from and write to the xml file where the configuration is store. (The version here has been simplified to deal with the original code only.)

IConfigurationService:

using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;

namespace Rubberduck.Config
{
    [ComVisible(false)]
    public interface IConfigurationService
    {
        Configuration GetDefaultConfiguration();
        ToDoMarker[] GetDefaultTodoMarkers();
        Configuration LoadConfiguration();
        void SaveConfiguration<T>(T toSerialize);
    }
}

ConfigurationLoader:

public class ConfigurationLoader : IConfigurationService
{
    private static string configFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Rubberduck", "rubberduck.config");

    /// <summary>   Saves a Configuration to Rubberduck.config XML file via Serialization.</summary>
    public void SaveConfiguration<T>(T toSerialize)
    {
        XmlSerializer xmlSerializer = new XmlSerializer(toSerialize.GetType());
        using (TextWriter textWriter = new StreamWriter(configFile))
        {
            xmlSerializer.Serialize(textWriter, toSerialize);
        }
    }

    /// <summary>   Loads the configuration from Rubberduck.config xml file. </summary>
    /// <remarks> If an IOException occurs, returns a default configuration.</remarks>
    public Configuration LoadConfiguration()
    {
        try
        {
            using (StreamReader reader = new StreamReader(configFile))
            {
                var deserializer = new XmlSerializer(typeof(Configuration));
                var config = (Configuration)deserializer.Deserialize(reader);

                //deserialization can silently fail for just parts of the config, 
                //  so we null check and return defaults if necessary.
                if (config.UserSettings.ToDoListSettings == null)
                {
                    config.UserSettings.ToDoListSettings = new ToDoListSettings(GetDefaultTodoMarkers());
                }

                return config;
            }
        }
        catch (IOException)
        {
            return GetDefaultConfiguration();
        }
        catch (InvalidOperationException ex)
        {
            var message = ex.Message + System.Environment.NewLine + ex.InnerException.Message + System.Environment.NewLine + System.Environment.NewLine +
                    configFile + System.Environment.NewLine + System.Environment.NewLine +
                    "Would you like to restore default configuration?" + System.Environment.NewLine + 
                    "Warning: All customized settings will be lost.";

            DialogResult result = MessageBox.Show(message, "Error Loading Rubberduck Configuration", MessageBoxButtons.YesNo,MessageBoxIcon.Exclamation);

            if (result == DialogResult.Yes)
            {
                var config = GetDefaultConfiguration();
                SaveConfiguration<Configuration>(config);
                return config;
            }
            else
            {
                throw ex;
            }
        }
    }

    public Configuration GetDefaultConfiguration()
    {
        var userSettings = new UserSettings(new ToDoListSettings(GetDefaultTodoMarkers()));

        return new Configuration(userSettings);
    }

    public ToDoMarker[] GetDefaultTodoMarkers()
    {
        var note = new ToDoMarker("NOTE:", TodoPriority.Low);
        var todo = new ToDoMarker("TODO:", TodoPriority.Normal);
        var bug = new ToDoMarker("BUG:", TodoPriority.High);

        return new ToDoMarker[] { note, todo, bug };
    }
}

Configuration:

using System;
using System.IO;
using System.Xml.Serialization;
using System.Runtime.InteropServices;

namespace Rubberduck.Config
{
    [ComVisible(false)]
    [XmlTypeAttribute(AnonymousType = true)]
    [XmlRootAttribute(Namespace = "", IsNullable = false)]
    public class Configuration
    {
        public UserSettings UserSettings { get; set; }

        public Configuration()
        {
            //default constructor required for serialization
        }

        public Configuration(UserSettings userSettings)
        {
            this.UserSettings = userSettings;
        }
    }
}

User Settings:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
using System.Runtime.InteropServices;

namespace Rubberduck.Config
{
    [ComVisible(false)]
    [XmlTypeAttribute(AnonymousType = true)]
    public class UserSettings
    {
        public ToDoListSettings ToDoListSettings { get; set; }

        public UserSettings()
        {
            //default constructor required for serialization
        }

        public UserSettings(ToDoListSettings todoSettings)
        {
            this.ToDoListSettings = todoSettings;
        }
    }
}

TodoListSettings:

using System.Xml.Serialization;
using System.Runtime.InteropServices;

namespace Rubberduck.Config
{
    interface IToDoListSettings
    {
        ToDoMarker[] ToDoMarkers { get; set; }
    }

    [ComVisible(false)]
    [XmlTypeAttribute(AnonymousType = true)]
    public class ToDoListSettings : IToDoListSettings
    {
        [XmlArrayItemAttribute("ToDoMarker", IsNullable = false)]
        public ToDoMarker[] ToDoMarkers { get; set; }

        public ToDoListSettings()
        {
            //empty constructor needed for serialization
        }

        public ToDoListSettings(ToDoMarker[] markers)
        {
            this.ToDoMarkers = markers;
        }
    }
}

TodoMarkers:

using System.Xml.Serialization;
using System.Runtime.InteropServices;
using Rubberduck.VBA;

namespace Rubberduck.Config
{
    [ComVisible(false)]
    public enum TodoPriority
    {
        Low, 
        Normal,
        High
    }

    [ComVisible(false)]
    public interface IToDoMarker
    {
        TodoPriority Priority { get; set; }
        string Text { get; set; }
    }

    [ComVisible(false)]
    [XmlTypeAttribute(AnonymousType = true)]
    public class ToDoMarker : IToDoMarker
    {
        //either the code can be properly case, or the XML can be, but the xml attributes must here *exactly* match the xml
        [XmlAttribute]
        public string Text { get; set; }

        [XmlAttribute]
        public TodoPriority Priority { get; set; }

        /// <summary>   Default constructor is required for serialization. DO NOT USE. </summary>
        public ToDoMarker()
        {
            // default constructor required for serialization
        }

        public ToDoMarker(string text, TodoPriority priority)
        {
            Text = text;
            Priority = priority;
        }

        /// <summary>   Convert this object into a string representation. Over-riden for easy databinding.</summary>
        /// <returns>   The Text property. </returns>
        public override string ToString()
        {
            return this.Text;
        }
    }
}

And a sample xml file:

<?xml version="1.0" encoding="utf-8"?>
<Configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <UserSettings>
    <ToDoListSettings>
      <ToDoMarkers>
        <ToDoMarker Text="NOTE:" Priority="Low" />
        <ToDoMarker Text="TODO:" Priority="Normal" />
        <ToDoMarker Text="BUG:" Priority="High" />
      </ToDoMarkers>
    </ToDoListSettings>
  </UserSettings>
</Configuration>
Community
  • 1
  • 1
RubberDuck
  • 11,933
  • 4
  • 50
  • 95
0

Your code looks good. It bet something in your collection constructor is off a bit. I implemented nealy the same as you a year ago but I used ConfigurationGroup and my ConfigurationSection was an object with properties not a collection.

Just for fun could you run this.

[TestMethod]
    public void TestGetTodoAttribute()
    {
        ToDoItemsConfigurationSection config= (ToDoItemsConfigurationSection)ConfigurationManager.GetSection("ToDoListAttributesSection");


        Assert.Fail();

    }
Ross Bush
  • 14,648
  • 2
  • 32
  • 55