By: Paul S. Cilwa | Viewed: 12/7/2023 Posted: 1/26/2022 |
Page Views: 621 | |
Topics: #ClassLibrary #Organica #OrganicaLib #VB.NET #VisualBasic | |||
Whether you call then .INI files, PrivateProfiles, or Settings, they're everywhere and you've still gotta use 'em.. |

In the early days of Windows, there was a file in the Windows directory called Win.ini, and another called System.ini. Internally, it was called a "profile", and there were (are) Windows API calls to manage it. Later and current versions of Windows now support .INI files anywhere; these are called private profiles. It was a simple format, in usefulness severely limited compared to today's XML files. But simple isn't always bad, especially when the data being stored is, itself, simple. .INI files can be written in Notepad; the syntax is readily understandable; and, in any case, these files are everywhere.
Here's a sample .INI file, so you can see the syntax for yourself.
[.ShellClassInfo]
LocalizedResourceName=@%SystemRoot%\system32\shell32.dll,-21803
InfoTip=@%SystemRoot%\system32\shell32.dll,-12689
IconResource=%SystemRoot%\system32\imageres.dll,-3
The original Windows API calls were designed for C-language strings (which do not have a stored length; the end of a C string is marked by a NULL character). While these calls can and have been converted into Visual Basic, they required a lot of byte-wise manipulation to be used. There are dozens of examples on the web; but they are equally awkward.
And, besides, with the simple syntax to be parsed, there's no reason to use them at all, when reading (and writing) files is now so easy, thanks to .NET; and the code to parse the file is trivial (a lot simpler than using the original API).
Settings
Open up the OrganicaLib solution and add a Class to it. Name the new class Settings. It will need the following Imports.
Imports OrganicaLib
Imports System.IO
Imports System.Text
Public Class Settings
Public Sections As New SectionList
Private i_FilePath As String
End Class
The Settings class represents the settings (.INI) themselves. It will contain a List of Section objects, each of which will contain (in another List) a collection of Key objects. ALso, I'm going to implement these as nested classes, mostly to avoid any naming conflicts in the real world, but also just to demonstrate the technique.
We'll provide two New subs, one that accepts a simple file name, and the other for Fileinfo objects.
Public Class Settings
Public Sections As New SectionList
Private i_FilePath As String
…
Public Sub New(ByVal FilePath As String)
Initialize(FilePath)
End Sub
Public Sub New(ByVal aFile As FileInfo)
Initialize(aFile.FullName)
End Sub
The Initialize procedure, called by both News, is where the loading magic occurs.
Private Sub Initialize(ByRef FilePath As String)
Dim i_Reader As StreamReader
i_FilePath = FilePath
i_Reader = New StreamReader(i_FilePath)
Dim Buffer As String
Dim S As SectionList.Section = Nothing
Dim K As SectionList.Section.Key
Dim KeyAndValue As String()
While Not i_Reader.EndOfStream
Buffer = i_Reader.ReadLine()
If Buffer.Length > 0 Then
If Buffer.Left(1) = "[" Then '
S = New SectionList.Section(Buffer.Dequote("[", "]"))
Sections.Add(S)
ElseIf Buffer.IndexOf("=") >= 0 Then
KeyAndValue = Strings.Split(Buffer, "=", 2)
K = New Settings.SectionList.Section.Key(KeyAndValue(0), KeyAndValue(1))
S.Add(K)
End If
End If
End While
End Sub
After creating a StreamReader for the target file, we then reach each line of the file. If the line is blank, we ignore it. If it begins with "[" that's the start of a section. After that, there may be one or more Keys, signified by the presence of a "=" in the line.
The Key class, as you'll see, allows updates. Therefore the Settings may have been changed. We'll want to write changes, if any, back to the physical file when the object is released. (For example, when the function, sub or class object in which it was declared is exited; all local objects are released at that time.) When this happens it triggers the Finalize event handler.
Protected Overrides Sub Finalize()
If Changed Then
Dim MyWriter As New StreamWriter(i_FilePath)
For Each S As SectionList.Section In Sections
MyWriter.WriteLine(S.Name.Enquote("[", "]"))
For Each K As SectionList.Section.Key In S
MyWriter.WriteLine(K.ToString)
Next
MyWriter.WriteLine()
Next
End If
MyBase.Finalize()
End Sub
As I keep saying, the format of these files is very simple. That makes the code to create one, simple as well. For each Section in the stored list, its Keys are enumerated. So the output consists of section names, each followed by zero or more keys and their values, followed by a blank line for appearance's sake. It's important to realize that this will overwrite the original file. If the original contained any comments or semantically incorrect lines of text, they will be lost. Likewise, any random blank lines in the original file will be replaced by the single blank line after each section.
Since Finalize invokes the Changed property, let's examine that next.
Private ReadOnly Property Changed() As Boolean
Get
For Each S As Section In Sections
If S.Changed Then Return True
For Each K As SectionList.Section.Key In S
If K.Changed Then
Return True
End If
Next
Next
Return False
End Get
End Property
As with Finalize, we check each Section to see if it's changed (for example, an empty section was added). We only need to find one entity to have been changed for the file to be overwritten; so the first True we get, we can exit the routine.
If the Section itself hasn't been changed, we then query each Key for the same thing. Again, the first changed Key we hit, we can leave. Only after every Section and every Key has been queried can we return False, signifying that the Settings have, ideed, not changed; they do not need to be rewritten to disk.
We'll implement Public, but ReadOnly, access to our internal i_FilePath property. It has to be ReadOnly because we do not want to allow a single Settings object to be re-used on a different file. (Allowing that would make tracking changes and saving them more complex for little benefit. Make another Settings object, for heaven's sake. Make a dozen of them! They are not memory hogs.
Public ReadOnly Property FilePath() As String
Get
Return i_FilePath
End Get
End Property
Most complex classes should implement ToString methods. Here's ours. Note that, if the Settings object's ToString method is invoked, it automatically includes all the sections and their keys in its output.
Public Overrides Function ToString() As String
Dim Result As New StringBuilder
Result.AppendLine("Settings File: " & FilePath)
Result.AppendLine(Sections.ToString)
Return Result.ToString
End Function
So, when we get to the Sections class, look for how its ToString function is implemented.
Now, in order to test this class in our TestBed, we'll want to convert the section and key names into HTML-compliant strings. This is common enough that the underlying Object class has a ToHtml function! So we have to overload that, too.
Public Overloads Function ToHtml() As String
Dim Result As New StringBuilder
Result.AppendLine("<h4>File: " & FilePath & "</h4>")
Result.AppendLine(Sections.ToHtml)
Return Result.ToString
End Function
SectionList
Although it should work if defined as an external class, I like the encapsulation of nested classes, at least in cases like this where doing so reflects the relationship the objects of these classes will have in use. (No one would ever want to create a Key object outside the realm of .INI files.)
I put nested classes after all the containing class' function, procedures and properties. This isn't a requirement but I find it makes the code easier to navigate.
Public Class Settings
…
'*******************************
' Begin Nested Class
'*******************************
Public Class SectionList
Inherits List(Of Section)
End Class
End Class
Now, remember this class is a container of Section objects, which we haven't yet defined. But to understand the below code, you'll want to know that SectionList inherits from a collection class; so it already has a functioning Add method it inherited. However, we will also need an AddNew function, which invokes Add on its own, but also sets that Section's Changed property.
Public Class Settings
…
Public Class SectionList
Inherits List(Of Section)
Private i_SectionList As New List(Of Section)
Friend Function AddNew(SectionName As String) As Section
Dim S As New Section(SectionName)
S.Changed = True
Add(S)
Return S
End Function
Any object that is supposed to act like a container, needs a way to locate individual objects that it contains. A common method name for this is Find, which we'll overload over its base class implementation.
Public Class Settings
…
Public Class SectionList
…
Public Overloads Function Find(SectionName As String) As Section
Dim S As Section
For Each S In Me
If S.Name.LCase = SectionName.LCase Then
Return S
End If
Next
S = New Section(SectionName)
S.Changed = True
i_SectionList.Add(S)
Return S
End Function
The code begins by trying to find the desired Section but if that fails, it creates a new, empty one and adds it to the collection. That way, the programmer calling this library doesn't have to worry about being thrown a Nothing in case of a missing Section. However, this does count as a change and will be written out to the file when the Settings object is released.
Msgbox(MySections.Items("Organica").ToString))
Msgbox(MySections("Organica")).ToString)
And, speaking of ToString, here are the overloaded ToString and ToHtml functions for the SectionList class (most likely to be called from Settings).
Public Class Settings
…
Public Class SectionList
…
Public Overrides Function ToString() As String
Dim Result As New StringBuilder
For Each S As Section In Me
Result.AppendLine("Section: " & S.Name)
For Each K As Section.Key In S
Result.AppendLine(K.ToString)
Next
Next
Return Result.ToString
End Function
Public Overloads Function ToHtml() As String
Dim Result As New StringBuilder
For Each S As Section In Me
Result.AppendLine("<p>Section: " & S.Name & "</p>")
Result.AppendLine("<table>")
For Each K As Section.Key In S
Result.AppendLine("<tr><td>" & K.Name & "</td><td>" & K.Value & "</td></tr>")
Next
Result.AppendLine("</table>")
Next
Return Result.ToString
End Function
Section
The Section class is also a container: of Key objects. And I have further nested it within the SectionList class.
Public Class Settings
…
Public Class SectionList
…
'*******************************
' Begin Nested Class
'*******************************
Public Class Section
Inherits List(Of Key)
Private i_SectionName As String
Friend Changed As Boolean = False
Public Sub New(SectionName As String)
i_SectionName = SectionName
End Sub
Public Sub New(SectionName As String)
i_SectionName = SectionName
End Sub
End Class
End Class
End Class
As with the SectionList, we're okay with the inherited Add function but need to supply an AddNew function, as well as overloading the Find, Item and ToStringfunctions.
Public Class Settings
…
Public Class SectionList
…
Public Class Section
…
Friend Function AddNew(KeyName As String, Optional Value As String = "") As Key
Dim K As New Key(KeyName, Value)
K.Changed = True
Add(K)
Return K
End Function
Public Overloads Function Find(KeyName As String) As Key
Dim K As Key
For Each K In Me
If K.Name.LCase = KeyName.LCase Then
Return K
End If
Next
Return Nothing
End Function
Default Public Overloads ReadOnly Property Item(KeyName As String) As Key
Get
Return Find(KeyName)
End Get
End Property
Public Overrides Function ToString() As String
Dim K As Key
Dim Result As New StringBuilder
Result.AppendLine("Section: " & Name)
For Each K In Me
Result.AppendLine(K.ToString)
Next
Return Result.ToString
End Function
End Class
Key
As the Zen teacher said, "Look farther within." Our final, innermost nested class, is the Key class, nested with Section.
Create the class.
Public Class Settings
…
Public Class SectionList
…
Public Class Section
…
'*******************************
' Begin Nested Class
'*******************************
Public Class Key
Private i_Name As String
Private i_Value As String = ""
Public Changed As Boolean
Public Sub New(KeyName As String, Value As String)
i_Name = KeyName
i_Value = Value
End Sub
End Class
End Class
End Class
End Class
Then we have two properties, Name abd Value. Name can only be read, but Value can be changed; and, if it is, the Key object itself must be marked Changed.
Public Class Settings
…
Public Class SectionList
…
Public Class Section
…
Public Class Key
…
Public ReadOnly Property Name() As String
Get
Return i_Name
End Get
End Property
Public Property Value As String
Get
Return i_Value
End Get
Set(NewValue As String)
i_Value = NewValue
Changed = True
End Set
End Property
End Class
End Class
End Class
End Class
And, last but not least, we have our ToString overload. (We don't need one for ToHtml.)
Public Class Settings
…
Public Class SectionList
…
Public Class Section
…
Public Class Key
…
Public Overrides Function ToString() As String
Return Name + "=" + Value
End Function
End Class
End Class
End Class
End Class
OrganicaLib Test Bed: Settings
By: Paul S. Cilwa | Posted: 1/23/2022 Updated: 1/30/2022 |
Page Views: 669 | |
Topics: #ClassLibrary #Organica #OrganicaLib #Settings #TestBed #VB.NET #VisualBasic | |||
Testing the Settings class. |

Properly testing every single method in the Settings class would basically result in a .INI file editor, which is more work than I want to put into this right now. Besides, just getting it to list all the sections and keys and values, will exercise 96% of the code.
Read more…