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!