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.
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!