views:

282

answers:

2

I would like to have an xml config file as follows:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
    <section name="plugins" type="MyApp.PluginsConfiguration, MyApp"/>
</configSections>

<plugins>
    <use assembly="MyApp.Plugin1.dll" />
    <use assembly="MyApp.Plugin2.dll" />
</plugins>
</configuration>

How exactly do I arrange the interplay between the PluginsConfigurations, UsePluginCollection, and UsePlugin classes in my code?

I found this tutorial online but it would introduce an element inside of plugins that surrounds the use collection and I do not need this.

This is what I have so far but its not quite right

+2  A: 

Based on the tutorial, you could easily create an XML Configuration as follows:

<MyApp>
  <Plugins>
    <add assembly="MyApp.Plugin1.dll" />
    <add assembly="MyApp.Plugin2.dll" />
  </Plugins>
</MyApp>

Which I find perfectly acceptable. Is there a reason this would be unacceptable?

Edit:

I'm not sure how exactly to do that with the tutorial. You would need some sort of intermediate element (like the tutorial has "actions") – George Mauer

Ok, try this on for size. I may have some typos since I'm copy/pasting/editing some code I used that does what you want, but it should work with the XML I defined above.

public sealed class MyAppConfiguration : ConfigurationSection
{
    public const string MyAppConfigurationTagName = "MyApp";
    [ConfigurationProperty(MyAppConfigurationTagName, IsRequired = true)]
    public PluginConfigurationCollection Plugins
    {
        get
        {
            return this[MyAppConfigurationTagName] as PluginConfigurationCollection;
        }
        set
        {
            this[MyAppConfigurationTagName] = value;
        }
    }
}

public sealed class PluginConfigurationItem : ConfigurationElement
{
    // repeat this pattern for each additional attribute you want in the <add /> tag. 
    // Only the assembly="foo.dll" portion is defined in this class, and is accessed 
    // via the AssemblyName property. 
    public const string AssemblyPropertyName = "assembly";
    [ConfigurationProperty(AssemblyPropertyName, IsRequired = true, IsKey = true)]
    public string AssemblyName
    {
        get
        {
            return this[AssemblyPropertyName] as string;
        }
        set
        {
            this[AssemblyPropertyName] = value;
        }
    }
}

public class PluginConfigurationCollection : ConfigurationElementCollection, IEnumerable<PluginConfigurationItem>
{
    public const string = "Plugins";

    protected override ConfigurationElement CreateNewElement()
    {
        return new PluginConfigurationItem();
    }

    protected override object GetElementKey(ConfigurationElement element)
    {
        return ((PluginConfigurationItem)element).AssemblyName;
    }

    protected override string ElementName
    {
        get
        {
            return PluginsElementName;
        }
    }

    // this is extraneous, but I find it very useful for enumerating over a configuration collection in a type-safe manner.
    #region IEnumerable<PluginConfigurationItem> Members

    public new IEnumerator<PluginConfigurationItem> GetEnumerator()
    {
        foreach(PluginConfigurationItemitem in (this as IEnumerable))
        {
            yield return item;
        }
    }

    #endregion
}
Randolpho
I'm not sure how exactly to do that with the tutorial. You would need some sort of intermediate element (like the tutorial has "actions")
George Mauer
In this approach, you should be able to use the internal NameValueSectionHandler: http://msdn.microsoft.com/en-us/library/system.configuration.namevaluesectionhandler.aspx - Sorry, don't know exactly how (i would roll my own to by honest), but Google would help here :)
Michael Stum
+2  A: 

If MyApp.PluginsConfiguration is a ConfigurationSection, then you can define a new Class that inherits from ConfigurationElementCollection and make the new class a ConfigurationProperty of MyApp.PluginsConfiguration

Check this article for some in-depth info about those types. I also blogged about having nested properties, but not specifically for collections.

Edit: Here with some code. Given is this bit in the web.config:

<configSections>
    <section name="plugins"
             type="WebApplication1.PluginsConfiguration, WebApplication1"/>
</configSections>
<plugins>
    <use assembly="MyApp.Plugin1.dll"/>
    <use assembly="MyApp.Plugin2.dll"/>
</plugins>

Here is are the classes to make that happen. Note that there maybe NullReferenceExceptions that would need to be handled.

namespace WebApplication1
{
    public class PluginsConfiguration : ConfigurationSection
    {
        private static ConfigurationPropertyCollection properties;
        private static ConfigurationProperty propPlugins;

        static PluginsConfiguration()
        {
            propPlugins = new ConfigurationProperty(null, typeof(PluginsElementCollection),
                                                          null,
                                                          ConfigurationPropertyOptions.IsDefaultCollection);
            properties = new ConfigurationPropertyCollection { propPlugins };
        }

        protected override ConfigurationPropertyCollection Properties
        {
            get
            {
                return properties;
            }
        }

        public PluginsElementCollection Plugins
        {
            get
            {
                return this[propPlugins] as PluginsElementCollection;
            }
        }
    }

    public class PluginsElementCollection : ConfigurationElementCollection
    {
        public PluginsElementCollection()
        {
            properties = new ConfigurationPropertyCollection();
        }

        private static ConfigurationPropertyCollection properties;

        protected override ConfigurationPropertyCollection Properties
        {
            get
            {
                return properties;
            }
        }

        public override ConfigurationElementCollectionType CollectionType
        {
            get
            {
                return ConfigurationElementCollectionType.BasicMap;
            }
        }

        protected override string ElementName
        {
            get
            {
                return "use";
            }
        }

        protected override ConfigurationElement CreateNewElement()
        {
            return new PluginsElement();
        }

        protected override object GetElementKey(ConfigurationElement element)
        {
            var elm = element as PluginsElement;
            if (elm == null) throw new ArgumentNullException();
            return elm.AssemblyName;
        }
    }

    public class PluginsElement : ConfigurationElement
    {
        private static ConfigurationPropertyCollection properties;
        private static ConfigurationProperty propAssembly;

        protected override ConfigurationPropertyCollection Properties
        {
            get
            {
                return properties;
            }
        }

        public PluginsElement()
        {
            propAssembly = new ConfigurationProperty("assembly", typeof(string),
                                                          null,
                                                          ConfigurationPropertyOptions.IsKey);
            properties = new ConfigurationPropertyCollection { propAssembly };
        }

        public PluginsElement(string assemblyName)
            : this()
        {
            AssemblyName = assemblyName;
        }

        public string AssemblyName
        {
            get
            {
                return this[propAssembly] as string;
            }
            set
            {
                this[propAssembly] = value;
            }
        }
    }
}

And to access it, this code snippet should help:

        var cfg = WebConfigurationManager.GetWebApplicationSection("plugins") as PluginsConfiguration;
        var sb = new StringBuilder();
        foreach(PluginsElement elem in cfg.Plugins)
        {
            sb.AppendFormat("{0}<br/>", elem.AssemblyName);
        }
        testLabel.Text = sb.ToString();

Basically we have a ConfigurationSection that handles the plugins-element. In this, we specify a ConfigurationElementCollection property and declare it as the Default Collection (you could in theory have multiple different collections under one root node).

PluginsElementCollection implements the ConfigurationElementCollection. ElementName has to be the name of the tag, in our case "use". Also, GetElementKey needs to be overridden and should return an attribute that is unique among the entries.

PluginsElement then implements a single use tag. We only define one property: AssemblyName, which is mapped to the assembly-attribute.

I don't claim to fully understand all of this (Especially ConfigurationElementCollection and it's various BaseAdd, BaseGet etc. properties are not really explored here), but I can claim that this works :)

Also, It doesn't use any attributes. I hate Attributes - luckily, all these attributes can be converted to proper code. You could use one or the other (or both).

Michael Stum
Also if you want, look at System.Web.Configuration.CustomErrorsSection and others using either Reflector or the Debugging Server. Looking at how Microsoft implements these (IMHO rather complicated) classes gives good insight.
Michael Stum