|By: Paul S. Cilwa||Viewed: 12/7/2023
|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).
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
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.
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
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
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
|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.