Simplified VB.Net Configuration

Filed under .NET, Code Garage

image I really liked the idea of a built-in configuration management framework with .NET. That is, until I actually tried to use it.

I wrote about the configuration functions in .NET here. My specific words were:

If your sideline utility needs to save a few settings, use the .NET configuration…

Ugh, I’m sorry I said that…

What’s Wrong

The .NET configuration namespace is powerful, no doubt about that. But, that comes at a huge cost. The thing is enormous. And the available documentation and examples just aren’t that good.

Honestly, though. The docs don’t bother me that much. What bothers me is that even though the existing framework will probably do everything I want it to, discovering how is just too dang hard and non-intuitive.

For instance:

  • Why the hell should I have to create a new ExeConfigurationFileMap object just to change the path of where the system reads its config file? Sorry, but that’s about as intuitive as a car you accelerate by yodeling.
  • What on earth were they smoking when they came up with those ludicrous guid/hash/hex based folder names where your user level config files are stored by default? Facilitate upgrades? Yeah, maybe, but pretty much nothing else. That’s one that I guarantee will go down with the registry and DCOM as a bad idea.
  • Registering ConfigurationSection handlers? Huh? Why should I have to write the full class names of classes internal to my application into my configuration file so that .NET can read them?

What Would Be Nice

What I was looking for was a simple way to create a class like so:

Public Sub MySettingClass
   Public Setting1 as string = ""
   Public Setting2 as Integer = 0
End Class

No need to explicitly use properties unless you need them. No need for attributes. Heck, you shouldn’t even have to declare the class <Serializable>, though that’s a minor point.

In lieu of coexisting within the My.Settings space, it should be able to persist itself to a config file, and then de-persist itself back when asked, like this:

Settings = New MySettingsClass
Settings.Load
...
Settings.Save

Accessing your settings should be completely early bound, with all that sweet Intellisense goodness baked right in:

x = Settings.Setting1
y = Setting.Setting2

Further, I should be able to easily persist sub-objects or collections made accessible off this root settings object. For example, to save a form’s current position and size, and then restore it, should take code similar to:

Settings.FormPositions.Save(MyForm)
Settings.FormPositions.Restore(MyForm)
Debug.print "Form position is " & Settings.FormPositions(0).Position

And a few additional requirements.

  • First, having no initial configuration file shouldn’t be a problem. The entire collection of settings should easily default to some “built in” default values when no config file exists.
  • Second, I should be able to go from 0-60 in no time. In other words, I should be able to take the .VB file for a settings base class, drop it in my application, add a settings class with the properties I need to persist, as well as a .LOAD and a .SAVE at the appropriate points in my project, and be off. No “presetting” my config file, no tweaks to anything, no registering this or that, mucking with the GAC, etc, etc.

Research

The available Microsoft documentation on the configuration system was so confusing, I believe I knew less about it after I finished reading the docs than I did when I started.

I did turn up a very good article on CodeProject by Jon Rista called Cracking the Mysteries of .NET 2.0 Configuration. Definitely worth a read if you’re diving into this stuff.

While Jon gives several samples of code, nothing really illustrated exactly what I was looking for. However, there was more than enough info in the article to kick-start things for me.

Long story short, it turns out that the Configuration system in .NET is, in typical MS fashion, more than capable but ultra-overkill for many small-app type scenarios.

My Solution

While this is definitely still a work in progress, it’s proved quite useful so far, so I thought others might find it handy too.

The system consists of one file, SettingsBase.vb. It defines two classes, SettingsBase and SettingsBaseDictionary.

SettingsBaseDictionary is just a simple extension to the normal generic Dictionary class that allows it to be serialized. This is something I found on the web and is so handy with respect to settings that I just include it directly in the file.

SettingsBase is a MustInherit class, meaning it’s abstract. To use it, you must create your own settings class (call it whatever you like) that inherits from SettingsBase:

Public Class Settings
   Inherits SettingsBase

   Public Name As String = ""
   Public Phone As String = ""
End Class

When you want to load your settings, just instantiate your Settings object and invoke Settings.Load. To change settings, set the object’s properties as you normally would.

To save your settings, invoke Settings.Save.

Finally, you’ll need to add a reference to System.Configuration. Directly accessing the ConfigurationManager and EXEConfigurationFileMap objects requires it.

The sample project I’ve zipped up shows several examples of this, from ridiculously simple to moderately sophisticated. I even threw in a really simple example of DataBinding to a setting property (in this case, a Dictionary of contacts).

The Code

If you don’t want to download the sample, I’ve included the source to the SettingBase.VB file here. Note that this also includes the source to the SettingBaseDictionary, but if you don’t want it, you can simply delete it.

Imports System.Configuration
Imports System.IO
Imports System.Text
Imports System.Xml
Imports System.Xml.Serialization


''' <summary>
''' Base Class that will allow you to easily persist
''' a "settings" object via the .net configuration management framework
''' 
''' By Darin Higgins
''' Sept 2008
''' You are free to use this class in your own projects.
''' But please, keep the attributions as to the source.
''' </summary>
''' <remarks>
''' Be sure to add a reference to System.Configuration
''' </remarks>
''' <editHistory></editHistory>
Public MustInherit Class SettingsBase
#Region " Constants"
   '---- just some names of constants used in the class
   Private Const DEFAULTSETTINGFILENAME = "Settings.config"
   Private Const ROOTSECTION = "general"
   Private Const ROOTITEM = "settings"
#End Region


#Region " Properties"
   Private rFilename As String = DEFAULTSETTINGFILENAME
   ''' <summary>
   ''' The Settings filename (name only, no path)
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Public Property FileName() As String
      Get
         Return rFilename
      End Get
      Set(ByVal value As String)
         rFilename = value
      End Set
   End Property


   Private rCompanyName As String = My.Application.Info.CompanyName
   ''' <summary>
   ''' The CompanyName used when creating a path to the settings store
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Public Property CompanyName() As String
      Get
         Return rCompanyName
      End Get
      Set(ByVal value As String)
         rCompanyName = value
      End Set
   End Property


   Private rAppName As String = My.Application.Info.ProductName
   ''' <summary>
   ''' The AppName used when creating a path to the settings store
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Public Property AppName() As String
      Get
         Return rAppName
      End Get
      Set(ByVal value As String)
         rAppName = value
      End Set
   End Property


   ''' <summary>
   ''' Retrieves the full path and filename to the settings store
   ''' Normally \Docs and Settings\All Users\Application Data\CompanyName\AppName\Settings.config
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Private ReadOnly Property pAppConfigFilename() As String
      Get
         '---- Don't use My.Computer.FileSystem.SpecialDirectories 
         '     because the commonappdata folder returned will always have
         '     the version in it and it will automatically be created
         '     but I don't want that here
         Dim path = System.Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)

         '---- as is pretty standard practice, our app settings
         '     go in a CompanyName\Appname folder in the CommonAppData folder
         If Me.CompanyName.Length > 0 Then
            path = System.IO.Path.Combine(path, Me.CompanyName)
            If Not My.Computer.FileSystem.DirectoryExists(path) Then
               My.Computer.FileSystem.CreateDirectory(path)
            End If
         End If
         If Me.AppName.Length > 0 Then
            path = System.IO.Path.Combine(path, Me.AppName)
            If Not My.Computer.FileSystem.DirectoryExists(path) Then
               My.Computer.FileSystem.CreateDirectory(path)
            End If
         End If

         Dim Filename = System.IO.Path.Combine(path, Me.FileName)
         Return Filename
      End Get
   End Property


   ''' <summary>
   ''' Creates an ExeConfigurationFileMap object to properly
   ''' locate the config files we'll use
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Private ReadOnly Property pAppConfigMap() As ExeConfigurationFileMap
      Get
         Dim filemap = New ExeConfigurationFileMap
         filemap.ExeConfigFilename = Me.pAppConfigFilename
         Return filemap
      End Get
   End Property


   ''' <summary>
   ''' Creates a Configuration object mapped to 
   ''' the proper settings files
   ''' Sets up several internal setting elements and sections
   ''' so we can persist the host object
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Private ReadOnly Property pConfig() As Configuration
      Get
         Static cfg As Configuration

         '---- cache the config object so we can reuse it
         If cfg Is Nothing Then
            cfg = ConfigurationManager.OpenMappedExeConfiguration(Me.pAppConfigMap, ConfigurationUserLevel.None)
         End If
         If Not cfg.HasFile Then
            '---- force a file to be created
            '     This settings is just general purpose placeholder
            '     not really intended to be used
            cfg.AppSettings.Settings.Add("version", My.Application.Info.Version.ToString)
            cfg.Save()
         End If

         Dim bDirty As Boolean = False
         If cfg.HasFile Then
            '---- no need for groups in this case
            '     but this sample code illustrates how you'd create a ConfigGroup if necessary
            'If cfg.SectionGroups(ROOTSECTION) Is Nothing Then
            '   cfg.SectionGroups.Add(ROOTSECTION, New ConfigurationSectionGroup)
            '   bDirty = True
            'End If
            'If cfg.SectionGroups(ROOTSECTION).Sections("options") Is Nothing Then
            '   cfg.SectionGroups(ROOTSECTION).Sections.Add("options", New ClientSettingsSection)
            '   bDirty = True
            'End If
            Dim sect As ClientSettingsSection = cfg.Sections(ROOTSECTION)
            If cfg.Sections(ROOTSECTION) Is Nothing Then
               sect = cfg.Sections(ROOTSECTION)
               cfg.Sections.Add(ROOTSECTION, sect)
               bDirty = True
            End If
            Dim element = sect.Settings.Get(ROOTITEM)
            If element Is Nothing Then
               element = New SettingElement(ROOTITEM, SettingsSerializeAs.Xml)
               sect.Settings.Add(element)
               bDirty = True
            End If
            If element.Value.ValueXml Is Nothing Then
               '---- make sure element contains something
               element.Value.ValueXml = New Xml.XmlDocument().CreateElement("value")
               bDirty = True
            End If

            If bDirty Then cfg.Save()
         End If
         Return cfg
      End Get
   End Property


   ''' <summary>
   ''' If you would like to use the built in ConfigurationStringsSection
   ''' just add a readonly property that exposes this property
   ''' Doing it this way, you can expose this property anywhere you want
   ''' in your Setting object heirarchy.
   ''' 
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Protected ReadOnly Property ConnectionStrings() As ConnectionStringsSection
      Get
         Return pConfig.ConnectionStrings
      End Get
   End Property


   ''' <summary>
   ''' Since this is essentially a key/value pair collection
   ''' there's not much benefit to exposing it, but I've included it
   ''' for completeness.
   ''' 
   ''' These kinds of sections are particularly useful when config merging
   ''' is used heavily, but that's not the point of this base class
   ''' </summary>
   ''' <value></value>
   ''' <remarks></remarks>
   Protected ReadOnly Property AppSettings() As AppSettingsSection
      Get
         Return pConfig.AppSettings
      End Get
   End Property
#End Region


#Region " Methods"
   ''' <summary>
   ''' Persist the host object to the configuration file
   ''' </summary>
   ''' <remarks></remarks>
   Public Sub Save()
      With Me.pConfig
         '---- not using configgroups right now, but keeping this for reference
         'Dim sect = TryCast(.SectionGroups(ROOTSECTION).Sections("options"), ClientSettingsSection)
         'If sect.Settings.Get(ROOTITEM) Is Nothing Then
         '   element = New SettingElement(ROOTITEM, SettingsSerializeAs.Xml)
         '   element.Value.ValueXml = New Xml.XmlDocument().CreateElement("value")
         '   sect.Settings.Add(element)
         'Else
         '   element = sect.Settings.Get(ROOTITEM)
         'End If
         Dim sect = TryCast(.Sections(ROOTSECTION), ClientSettingsSection)
         Dim element = sect.Settings.Get(ROOTITEM)

         '---- Create a serializer to serial our superclass
         Dim s = New XmlSerializer(Me.GetType)
         Using ms = New MemoryStream
            '---- serialize it
            s.Serialize(ms, Me)
            '---- rewind and convert the stream to a string
            ms.Seek(0, SeekOrigin.Begin)
            Dim myutf As UTF8Encoding = New UTF8Encoding()
            '---- load it up into an xml doc
            Dim xml = New XmlDocument
            xml.LoadXml(myutf.GetString(ms.GetBuffer()))
            '---- and push into the settings "value"
            '     stripping out the XML header stuff
            element.Value.ValueXml.InnerXml = xml.DocumentElement.OuterXml
         End Using

         '---- Force the save, because we won't otherwise trigger
         '     the dirty condition
         sect.SectionInformation.ForceSave = True
         .Save()
      End With
   End Sub


   ''' <summary>
   ''' Reload the superclass's properties from configuration
   ''' </summary>
   ''' <remarks></remarks>
   Public Sub Load()
      With Me.pConfig
         Dim sect = TryCast(.Sections(ROOTSECTION), ClientSettingsSection)
         Dim element As SettingElement = sect.Settings.Get(ROOTITEM)

         '---- if we've got the required element...
         If Len(element.Value.ValueXml.InnerXml) Then
            '---- deserialize the xml
            Dim s = New XmlSerializer(Me.GetType)
            Dim myutf As UTF8Encoding = New UTF8Encoding()
            Using ms = New MemoryStream(myutf.GetBytes(element.Value.ValueXml.InnerXml))
               '---- just get a generic object
               '     and we'll use reflection to 
               '     update the properties and fields of THIS object
               Dim o As Object = Nothing
               Try
                  o = s.Deserialize(ms)
               Catch ex As Exception
                  Debug.Print("Problem")
               End Try

               If o IsNot Nothing Then
                  '---- now need to refresh our values from 
                  '     this deserialized object
                  For Each Field In Me.GetType().GetFields
                     If Field.IsPublic Then
                        '---- BaseSettings has no fields, so 
                        '     we don't need to check if the field
                        '     is defined the the base object
                        Try
                           Field.SetValue(Me, Field.GetValue(o))
                        Catch
                        End Try
                     End If
                  Next

                  '---- now copy over any properties
                  For Each Prop In Me.GetType().GetProperties
                     '---- first, check that the property
                     '     isn't one of the BaseSettings properties
                     '     we don't want to depersist those!
                     Dim n = Prop.Name
                     Dim query As IEnumerable(Of System.Reflection.PropertyInfo) = Me.GetType.BaseType.GetProperties.Where(Function(Prop2) Prop2.Name = n)
                     If query.Count = 0 Then
                        '---- this is not a property on BaseSettings
                        If Prop.CanWrite Then
                           If Prop.GetIndexParameters.Count = 0 Then
                              '---- handle non-indexed properties
                              Try
                                 Prop.SetValue(Me, Prop.GetValue(o, Nothing), Nothing)
                              Catch
                              End Try
                           Else
                              '---- not handling indexed properties yet
                           End If
                        End If
                     End If
                  Next
               End If
            End Using
         End If
      End With
   End Sub
#End Region


#Region " SettingsBaseDictionary"
   ''' <summary>
   ''' A simple serializable dictionary class I pulled off the web.
   ''' 
   ''' Originally at http://www.playswithcomputers.com/SGDCollection.aspx
   ''' 
   ''' I've included it here because very often settings collections
   ''' need to be keyed for access, and lists/bindinglists (which will
   ''' persist just fine) don't make that especially straightforward
   ''' like a dictionary.
   ''' 
   ''' However, you can also use a generic List or BindingList for properties
   ''' and they seem to be persisted just fine, they just aren't quite 
   ''' as easy to perform keyed lookups on.
   ''' 
   ''' </summary>
   ''' <remarks></remarks>
   ''' <editHistory>
   ''' </editHistory>
   <XmlRoot("dictionary", IsNullable:=True)> _
   Public Class SettingsBaseDictionary(Of TKey, TValue)
      Inherits Generic.Dictionary(Of TKey, TValue)
      Implements IXmlSerializable

      Private Const ITEMNAME = "item"
      Private Const KEYNAME = "key"
      Private Const VALUENAME = "value"

      Public Function GetSchema() As System.Xml.Schema.XmlSchema Implements IXmlSerializable.GetSchema
         Return Nothing
      End Function


      Public Sub New()
         MyBase.New()
      End Sub
      Public Sub New(ByVal capacity As Integer)
         MyBase.New(capacity)
      End Sub
      Public Sub New(ByVal comparer As System.Collections.Generic.IEqualityComparer(Of TKey))
         MyBase.New(comparer)
      End Sub
      Public Sub New(ByVal capacity As Integer, ByVal comparer As System.Collections.Generic.IEqualityComparer(Of TKey))
         MyBase.New(capacity, comparer)
      End Sub
      Public Sub New(ByVal dictionary As Generic.IDictionary(Of TKey, TValue))
         MyBase.New(dictionary)
      End Sub
      Public Sub New(ByVal dictionary As Generic.IDictionary(Of TKey, TValue), ByVal comparer As System.Collections.Generic.IEqualityComparer(Of TKey))
         MyBase.New(dictionary, comparer)
      End Sub
      Public Sub New(ByVal info As System.Runtime.Serialization.SerializationInfo, ByVal context As System.Runtime.Serialization.StreamingContext)
         MyBase.New(info, context)
      End Sub


      ''' <summary>
      ''' Read a Serialized XML Dictionary of generic objects
      ''' </summary>
      ''' <param name="reader"></param>
      ''' <remarks></remarks>
      Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) Implements IXmlSerializable.ReadXml
         Dim keySerializer As XmlSerializer = New XmlSerializer(GetType(TKey))
         Dim valueSerializer As XmlSerializer = New XmlSerializer(GetType(TValue))
         Dim wasEmpty As Boolean = reader.IsEmptyElement
         reader.Read()
         If wasEmpty Then Return

         Do While (reader.NodeType <> System.Xml.XmlNodeType.EndElement)
            reader.ReadStartElement(ITEMNAME)
            reader.ReadStartElement(KEYNAME)

            Dim key As TKey = DirectCast(keySerializer.Deserialize(reader), TKey)
            reader.ReadEndElement()

            reader.ReadStartElement(VALUENAME)
            Dim value As TValue = DirectCast(valueSerializer.Deserialize(reader), TValue)
            reader.ReadEndElement()

            Me.Add(key, value)

            '---- finish reading this element and move to the next
            reader.ReadEndElement()
            reader.MoveToContent()
         Loop
         reader.ReadEndElement()
      End Sub


      ''' <summary>
      ''' Write the XML Serialization of a dictionary of generic objects
      ''' </summary>
      ''' <param name="writer"></param>
      ''' <remarks></remarks>
      Public Sub WriteXml(ByVal writer As System.Xml.XmlWriter) Implements IXmlSerializable.WriteXml
         Dim keySerializer As XmlSerializer = New XmlSerializer(GetType(TKey))
         Dim valueSerializer As XmlSerializer = New XmlSerializer(GetType(TValue))

         For Each key As TKey In Me.Keys
            writer.WriteStartElement(ITEMNAME)

            writer.WriteStartElement(KEYNAME)
            keySerializer.Serialize(writer, key)
            writer.WriteEndElement()

            writer.WriteStartElement(VALUENAME)
            valueSerializer.Serialize(writer, DirectCast(Me(key), TValue))
            writer.WriteEndElement()
            writer.WriteEndElement()
         Next
      End Sub
   End Class
#End Region

End Class

Points of Interest

  • Your settings file will, by default, be written to the CommonApplicationData folder, and in there, in a folder named the same as the CompanyName in your assembly information screen, and in there, in a folder named the same as the Application Name in the assembly information screen.

image 

So, for the above app, under Windows XP, you’ll find the default setting file in

c:\Documents and Settings\All Users\Application Data\One Nifty Company\SettingsTest\Settings.config

and under Vista

c:\Program Data\One Nifty Company\SettingsTest\Settings.config

This is pretty standard practice for config files, but if you want to change it, you’ve got the source<g>.

  • You can change the setting filename, the Company Name and the Product Name used by simply changing the associated  properties of your setting object (these properties are all inherited from the SettingsBase object).
  • Although you can use fields in your base Settings object, you won’t be able to use any DataBinding support with them. This is a limitation of the .NET Databinding support, and not with the Settings class. Quite frankly, it sucks. Sometimes full blown properties make sense, but in this case, properties are just a lot more work for the same net effect.
    If you want to use Databinding with your settings class, you’re probably best off declaring all persistent elements of your settings class as properties to begin with, and just avoid using fields at all.
  • The SettingsBase object doesn’t even try to provide access to configuration “sections” or “section groups”. You can easily break settings down into groups or sections by using “sub objects” off your main settings object (the one that inherits from SettingsBase). I illustrate this is the sample app.
  • There’s no merging (at least not intentionally anyway). I’m still not completely sure of how that works, and I haven’t needed it yet. 
  • As I indicated earlier, much of this could be accomplished by creating a custom ConfigurationSection class, and registering it in your config file, but from what I can tell so far, that mean you have to manually add gunk to your config file. To me, that’s got a code smell akin to that guy in the next cube that burns patchouli all day. It doesn’t stink, per se, but it sure makes your eyes water if you’re around it long enough.

<configSections>
    <section name="sampleSection"
    type="System.Configuration.MySectionHandler" />
</configSections>

 

What’s Next

  • Really, this should be wrappable in a custom ConfigurationSection handler. My only gripes with that approach is that it doesn’t resolve the folder naming problem, and it requires the goofy registration of the section handler. I’m betting there’s ways around both those issues, though. I just haven’t found them yet.
  • In the same vein, I’d really love to figure a way to accommodate what I’m looking for, AND still use the My.Settings namespace as it’s currently automatically defined by Visual Studio. Again, I’m working on that, but this simple approach works for the short and sweet utility apps I’ve needed to turn out recently.

Check it out and let me know what you think!

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*