Code references for this post are taken from the following demonstration projects:
C#:
href="http://www.4shared.com/file/78081512/11c27b29/Demo_-_Custom_Configuration_-_C.html
VB:
http://www.4shared.com/file/78081513/66c54bbf/Demo_-_Custom_Configuration_-_VB.html
After having come back to custom configuration files from a previous application where I'd kind of
haphazzardly thrown in a custom configuration in the "standard" format, I decided that in my
current application I was going to spend some time to really design my custom configuration exactly as
I wanted it, rather than how "it was supposed to be" according to the documentation found on
the MSDN site - which for those interested can be found at
http://msdn.microsoft.com/en-us/library/2tw134k3.aspx.
So I want to condense from my current extremely verbose format:
<MyCustomSection title="My custom configuration section">
<SectionsCollection>
<Section name="Section 1">
<SubSectionCollection>
<SubSection name="SubSection 1">
<ItemCollection>
<Item name="Item1" />
<Item name="Item2" />
</ItemCollection>
</SubSection>
<SubSection name="SubSection 2">...
</SubSectionCollection>
</Section>
<Section name="Section 2">...
</SectionsCollection>
</MyCustomSection>
down to:
<MyCustomSection title="This is my custom section">
<Section name="Section1">
<SubSection name="SubSection1">
<Item name="Item1" />
<Item name="Item2" />
</SubSection>
<SubSection name="SubSection2">
<Item name="Item1" />
<Item name="Item2" />
</SubSection>
</Section>
<Section name="Section2">
<SubSection name="SubSection1">
<Item name="Item1" />
<Item name="Item2" />
</SubSection>
</Section>
</MyCustomSection>
Okay, so I've defined what my custom section needs to look like... now I need to rewrite my custom section handler.
Well, as with the standard documentation, we must still have an entry in the configSection to
allow the ConfigurationManager to determine what handler should be used to read our configuration
section. This will look somewhat like the following:
<configSections>
<section name="MyCustomSection"
type="HandlerAssembly.HandlerNamespace.HandlerClass, HandlerAssembly" />
<section name=....
HandlerAssembly = The name of your application assembly
HandlerNamespace = The namespace that contains your custom configuration handler
HandlerClass = The name you gave your custom configuration handler
Be aware that in VB, a Windows application and a Web application can differ slightly
as does C#. In a Windows application, you can reference the name of your configuration
handler directly without specifying the assembly name; however, in an asp.net web application
you are required to provide the assembly name. For the sake of consistency, it is a good
rule of thumb to always specify the name of the assembly in the configuration file. This
way you never have to remember which one requires it and which one doesn't. Both types
of application will work if you reference it, only one type will work if you don't. In C#
however, you must specify the reader as just HandlerNamespace.HandlerClass before the comma, and
the assembly name after the comma:
<section name="CustomSection"
type="ReaderNamespace.MySectionReader, MyTestApplication">
So cutting the configuration file back to the bare bones to demonstrate - we should have
something (ignoring all the default settings that are provided by .NET) that looks like the
following:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="CustomSection"
type="MyTestApplication.MySectionReader, MyTestApplication" />
</configSections>
<CustomSection name="Custom Section Header">
<Section name="Section 1">
<SubSection name="Subsection 1">
<Item name="Item 1" />
<Item name="Item 2" />
</SubSection>
<SubSection name="Subsection 2">
<Item name="Item 1" />
<Item name="Item 2" />
</SubSection>
</Section>
<Section name="Section 2">
<SubSection name="Subsection 1">
<Item name="Item 1" />
<Item name="Item 2" />
</SubSection>
</Section>
</CustomSection>
</configuration>
So, on to writing our custom section handler. We're going to need a few classes
built - one that handles the collection of sections within our custom section, one that
handles each section in the collection, one that handles each of the individual subsections
and one that handles each item within our subsection. Normally you see this demonstrated
from the top down detailing the whole process, but I personally think it's easier to understand
if you work from the bottom up. Partly because our bottom level item doesn't contain any
collections to deal with, so it's the easiest to write.
Imaginitively I called the bottom
level item "item" - I know, hold the applause. It's a relatively simple class:
VB:
Public class Item
Inherits ConfigurationElement
<ConfigurationProperty("name")> _
Public ReadOnly Property Name() As String
Get
Return MyBase.Item("name")
End Get
End Property
End Class
C#:
class Item : ConfigurationElement
{
[ConfigurationProperty("name")]
public string Name
{
get { return (string)this["name"]; }
}
}
Glancing at this class, you can see that it's inherited from the ConfigurationElement class,
which requires you to add the reference to the System.Configuration namespace and add the
necessary using/imports statement to import the namespace.
The ConfigurationProperty
attribute tells the compiler that this property is actually a reference to a property within
the configuration file and values should be retrieved from that rather than the class structure
itself.
So now we've got our base element set up, we need to create our SubSection structure.
Our Subsection, unlike our Item, actually inherits from the ConfigurationElementCollection.
VB:
Public Class SubSection
Inherits ConfigurationElementCollection
Public Sub New()
AddElementName = "Item"
End Sub
Protected Overloads Overrides Function CreateNewElement() As ConfigurationElement
Return New Item
End Function
Protected Overrides Function GetElementKey(ByVal element As ConfigurationElement) As Object
Return DirectCast(element, Item).Name
End Function
_
Public ReadOnly Property Name() As String
Get
Return MyBase.Item("name")
End Get
End Property
End Class
C#:
class SubSection : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement()
{
return new Item();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((Item)element).Name;
}
//Constructor
public SubSection()
{
AddElementName = "Item";
}
[ConfigurationProperty("name")]
public string Name
{
get { return (string)this["name"]; }
}
}
Because we're inheriting from the ConfigurationElementCollection, we don't have to
actually implement much of our collection at all. We do have to define how items
are added to our collection and any attributes that our collection may have. In
this demonstration, I've only given my collection a single attribute called name, but
you can add as many as you wish giving them each names relevant to your application.
In order to use a custom node name rather than the default "add" we must
override the AddElementName. This can be done in a number of ways, but there are
limitations that aren't documented depending on which you choose. I prefer to
override this in my constructor as this can be consistently achieved across the
configuration element collections. Mostly you will find the documentation handles
this using the compiler attribute:
VB:
<ConfigurationCollection(GetType(SubSection),
AddItemName:="Item")> _
Public Class SubSection...
C#:
[ConfigurationCollection(GetType(SubSection),
AddItemName:="Item")]
public class SubSection...
While using this format in the standard documented way will work, if you stray from
the original structure, and start nesting collections directly within other collections,
you will notice very quickly that your application crashes and it will complain with the
following message: Unrecognized element 'Item'. When you define the
node name right in the constructor by setting the AddElementName, it will work in all
cases. It appears that when nesting collections directly within collections, .NET
will only recognize the standard "Add", "Remove" and "Clear"
elements unless you override the AddElementName, RemoveElementName and ClearElementName
properties directly in the constructor of your collection class.
You will also
notice that each of our collection class methods returns information or works with
information regarding the items held within the class, rather than itself as a collection.
The exception being the property that returns the value held by the name attribute
of the collection class.
Onwards and upwards. On to the Section class.
This class holds a collection of SubSections. Much like the SubSection you will
notice that we define the AddElementName property defining that we want to use the
node name "SubSection" to define each of our sub sections.
VB:
Public Class Section
Inherits ConfigurationElementCollection
Public Sub New()
AddElementName = "Section"
End Sub
<ConfigurationProperty("name")> _
Public ReadOnly Property Name() As String
Get
Return MyBase.Item("name")
End Get
End Property
Protected Overloads Overrides Function CreateNewElement() As System.Configuration.ConfigurationElement
Return New SubSectionCollection
End Function
Protected Overrides Function GetElementKey(ByVal element As System.Configuration.ConfigurationElement) As Object
Return DirectCast(element, SubSectionCollection).Name
End Function
End Class
C#
class Section : ConfigurationElementCollection
{
public Section()
{
AddElementName = "Section";
}
[ConfigurationProperty("name")]
public string Name
{
get { return (string)this["name"]; }
}
protected override ConfigurationElement CreateNewElement()
{
return new SubSectionCollection();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((SubSectionCollection)element).Name;
}
}
So we have our Section, SubSection and Item classes, but you may have noticed
that our Section class references a SubSectionCollection rather than SubSection
directly. We need to create a simple helper class which is basically a
container class for a collection of sub-sections, imaginitively, I'll call it
SubSectionCollection. I know, I'm a genius :oP so lets create our container class:
VB:
Public Class SubSectionCollection
Inherits ConfigurationElementCollection
Public Sub New()
AddElementName = "SubSection"
End Sub
<ConfigurationProperty("name")> _
Public ReadOnly Property Name() As String
Get
Return DirectCast(MyBase.Item("name"), String)
End Get
End Property
Protected Overloads Overrides Function CreateNewElement() As System.Configuration.ConfigurationElement
Return New SubSection
End Function
Protected Overrides Function GetElementKey(ByVal element As System.Configuration.ConfigurationElement) As Object
Return DirectCast(element, SubSection).Name
End Function
End Class
C#
class SubSectionCollection : ConfigurationElementCollection
{
//Constructor
public SubSectionCollection()
{
AddElementName = "SubSection";
}
[ConfigurationProperty("name")]
public string Name
{
get { return (string)this["name"]; }
}
protected override ConfigurationElement CreateNewElement()
{
return new SubSection();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((SubSection)element).Name;
}
}
Now, we can nest as many collections inside each other as we wish without
having to add extra property nodes to contain our collections, which simplifies
the design of our configuration file...as well as simplifying our class structure.
The last step is to reference the collection held directly by our custom section.
This is referenced exactly the same way as we normally would with one very
minor difference. Where normally we would specify our configuration property
by name, we instead pass in an empty string.
Replace:
VB:
Public Class MySectionReader
Inherits ConfigurationSection
<ConfigurationProperty("name")> _
Public ReadOnly Property Name() As String
Get
Return DirectCast(MyBase.Item("name"), String)
End Get
End Property
<ConfigurationProperty("OldNeedlessNodeName",
IsDefaultCollection:=True,
IsRequired:=True)> _
Public ReadOnly Property Items() As DefaultCollection
Get
Return DirectCast( _
MyBase.Item("OldNeedlessNodeName"), DefaultCollection)
End Get
End Property
End Class
With:
VB:
Public Class MySectionReader
Inherits ConfigurationSection
<ConfigurationProperty("name")> _
Public ReadOnly Property Name() As String
Get
Return DirectCast(MyBase.Item("name"), String)
End Get
End Property
<ConfigurationProperty("",
IsDefaultCollection:=True,
IsRequired:=True)> _
Public ReadOnly Property Items() As DefaultCollection
Get
Return DirectCast(MyBase.Item(""), DefaultCollection)
End Get
End Property
End Class
So now we've finished building our custom configuration section handler. All we need to do is provide a
reference to it in our main application and we can treat it as we would any normal class:
VB:
Dim oSR As MySectionReader = _
ConfigurationManager.GetSection("CustomSection")
Dim Title As String = oSR.Name
With Console
.WriteLine(Title)
.WriteLine("".PadRight(Title.Length, "-"))
For Each section As Section In oSR.Items
Dim OuterTitle As String = section.Name
.WriteLine(Space(2) & OuterTitle)
.WriteLine(Space(2) & "".PadRight(OuterTitle.Length, "-"))
For Each SubSection As SubSection In section
Dim InnerTitle As String = SubSection.Name
.WriteLine(Space(4) & InnerTitle)
.WriteLine(Space(4) & "".PadRight(InnerTitle.Length, "-"))
For Each Item As Item In SubSection
.WriteLine(Space(6) & Item.Name)
Next
.WriteLine()
Next
.WriteLine()
Next
End With
C#
MySectionReader oSR = (MySectioMySectionReader oSR = _
(MySectionReader)ConfigurationManager.GetSection("CustomSection");
string Title = oSR.Name;
Console.WriteLine(Title);'-', Title.Length));
foreach(SubSectionCollection section in oSR.Items)
{
string OuterTitle = section.Name;
string startChars = new String(' ', 2);
Console.WriteLine(startChars + OuterTitle);
Console.WriteLine(startChars + new String('-', OuterTitle.Length));
foreach (SubSection subSection in section)
{
string InnerTitle = subSection.Name;
startChars = new String(' ', 4);
Console.WriteLine(startChars + InnerTitle);
Console.WriteLine(startChars + new String('-', InnerTitle.Length));
foreach (Item item in subSection)
{
Console.WriteLine(new String(' ', 6) + item.Name);
}
Console.WriteLine();
}
Console.WriteLine();
}
So there we have it - our custom condensed configuration section, our reader and the method by
which to read the custom node names. As well as how we access the default collection without
requiring it inside a property element.