Showing posts with label app.config. Show all posts
Showing posts with label app.config. Show all posts

November 17, 2008

Custom configuration section

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.

There was an error in this gadget