Search code examples
.netvb.netclassclass-structure

How to properly structurate the visibility of this Class?


I'm trying to improve an old Class that I've wrote to manage an INI file, the Class contains 3 sub-Classes (File, Key, Section) to separate and organize the procedures (procs for the ini in general, procs for manage the keys/values, and procs for manage the section names).

Well, the problem I have is that in the old class all the members were shared (props/vars/objects/methods) and obviouslly that could result in a disimbiguation, then I would like to try to perfectionate the visibility of the members, and there is where I'm stuck.

The current usage of the Class is like this:

INIFileManager.FilePath = "ini filepath"
dim iniexist as boolean = INIFileManager.File.Exist

And the usage that I would like should be like this:

dim ini as new inifilemanager("ini filepath", textencoding)
dim iniexist as boolean = ini.file.exist

dim another_ini as new inifilemanager("another ini filepath without any kind of conflict with the first instance", textencoding)
dim another_iniexist as boolean = another_ini.file.exist

Below is the relevant code for this example, I'm stuck on the Exist method of the File class 'cause I cannot access to the FilePath variable which is at the top-level class since I don't set that variable and the Exist method both as Shared like I did on my old Class version...

...So how I can improve this?

NOTE: Please keep in mynd that the other 2 sub-Classes should have a method named Exist and other methods with equal names such as "[Get]", not only in the File Class (I don't know if that could be a problem that could need more retouches).

''' <summary>
''' Manages an INI file and it's sections to load/save values.
''' </summary>
Public Class INIFileManager

#Region " Properties "

    ''' <summary>
    ''' Indicates the initialization file location.
    ''' </summary>
    Private Property FilePath As String = String.Empty

    ''' <summary>
    ''' Indicates the initialization file encoding to read/write.
    ''' </summary>
    Private Property TextEncoding As System.Text.Encoding = System.Text.Encoding.Default

#End Region

#Region " Constructors "

    ''' <summary>
    ''' Initializes a new instance of the <see cref="INIFileManager" /> class.
    ''' </summary>
    ''' <param name="IniFile">
    ''' Indicates the initialization file location.
    ''' </param>
    ''' <param name="TextEncoding">Indicates a textencoding to read/write the iniinitialization file.</param>
    Public Sub New(Optional ByVal IniFile As String = Nothing,
                   Optional ByVal TextEncoding As System.Text.Encoding = Nothing)

        If Not String.IsNullOrEmpty(IniFile) Then
            Me.FilePath = IniFile
        Else
            Me.FilePath = IO.Path.Combine(Application.StartupPath,
                                          Process.GetCurrentProcess().ProcessName & ".ini")
        End If

        If Not TextEncoding Is Nothing Then
            Me.TextEncoding = TextEncoding
        End If

    End Sub

#End Region

    ''' <summary>
    ''' Contains a set of procedures to manage the INI file in a general way.
    ''' </summary>
    Private Class [File]

        ''' <summary>
        ''' Checks whether the initialization file exist.
        ''' </summary>
        ''' <returns>True if initialization file exist, otherwise False.</returns>
        Public Function Exist() As Boolean
            Return IO.File.Exists(MyBase.FilePath)
        End Function

        ' More irrelevant methods here that need to access to props and vars of the top-level class...

    End Class

    ' another class here...

    ' and another class here...

End Class

Solution

  • I think part of the problem is that you are revealing rather than handling too much detail of the hierarchy thru the class. The other part is clinging to 1980's 16bit Windows tech when there are much more robust mechanisms around today.

    You have 4 classes (INIManager, File, Section, Key) to manage 2 bits of information (Key and Value). Since INIManager is just to "house" the others, he can be combined with File - especially since there arent many file level operations. You likely do not even need [Sections]. Those only existed so you could store repeated attributes about similar items like:

     [MainDB]
     Path =  
     File =  
     Foo =
    
     [Report DB] 
     Path =  
     File =  
     Foo =
    

    These were intended to provide INItialization values in a way where you could loop thru a collection of strings like FILE1, FILE2... and read multiple sections in a loop. For a simple set of settings, just use a single [Default] section to simplify the class and its use. Then, you are just down to IniManager and Keys. Purposely exposing the underlying hierarchy doesnt lend itself to usability, IMO.

    So to do what you want, you need a SECTIONS property on INIManager which exposes the section related stuff. To support that, you need a INISection class (mainly the methods for Sections) AND a INISectionS collection Class. (Some Comments lead me to surmise that you want to load all the sections all the time and all their keys so they can be deleted etc).

    If you really want something like Sections().Keys().Method, you will have to add a Key class and Keys collection class on IniSection. Which would bring the grand total to 5 classes to manage 2 pieces of information. Of course, it can be done with half the code and 1 class. Most of the extra fluff is there to expose the inner workings the way you mentioned. You'll also have fun with Public vs Friend to keep from revealing some things you dont want to.

    I dont have anything in the code to do the PInvokes; The question deals with class construction not INI management. Most methods are empty and exist just to see how they end up for the poor user.


    Public Class INIManager
    
        ' all the gory PInvokes go here
    
        Friend cfgFile As String
        Public Property INIExists As Boolean
    
        ' this is the bit you seemed to be missing
        ' A Collection property exposed at the IniMgr level
        ' containing a collection of Sections.  Like matryoshka dolls, inside
        ' each is a collection of Keys and Values
        Public Property Sections As IniSections
    
        ' no reason for INI mgr to even exist without a file
        Public Sub New(iniFile As String)
            cfgFile = iniFile
            _INIExists = System.IO.File.Exists(cfgFile)
    
            _Sections = New IniSections(cfgFile)
        End Sub
    
        ' only worthwhile thing I can think of that a "File"
        ' class would ever do.  
        Public Sub RemoveFile()
    
        End Sub
    
        Public Sub Save()
             ' i think you need to delete the file first so any
            ' deleted sections disappear. of course sections the code
            ' does ask for doesnt do any harm either
    
            ' iterate IniSections to call a Save there,
            ' which iterates the keys one by one to save them
            Sections.Save(cfgFile)
        End Sub
    
        ' ****** INISections Class Collection
        Public Class IniSections
            'Inherits Collection(Of IniSection)
            Private Items As Collection(Of IniSection)
    
            Private cfgFile As String
    
            Friend Sub New(file As String)
                cfgFile = file
    
                ' I am assuming from some comments that you are probably
                ' loading the entire file to manage it.  for that:
    
                If System.IO.File.Exists(cfgFile) Then
                    ' load from GetPrivateProfileSectionNames into the collection
                    ' mybase.Items.Add(section_name)...then
    
                    For Each s As IniSection In Items
                        s.LoadKeyValues(cfgFile)
                    Next
    
                End If
    
            End Sub
    
            ' FRIEND!
            Friend Sub Save(cfgfile As String)
                For Each s As IniSection In Items
                    ' instruct each section to write the kvps
                    s.Save(cfgfile)
                Next
            End Sub
    
            ' I dont know why an empty accessor is showing up in Intellisense
            Default Public ReadOnly Property Item(name As String) As IniSection
                Get
                    If IndexOfSection(name) = -1 Then
                        Items.Add(New IniSection(name))
                    End If
                    Return Items(IndexOfSection(name))
                End Get
    
            End Property
    
            ' add a section
            Public Function Add(name As String) As IniSection
                Dim sec As New IniSection(name)
                Items.Add(sec)
                Return sec
            End Function
    
            ' remove a section
            Public Sub Remove(name As String)
    
                Items.RemoveAt(IndexOfSection(name))
    
        ' the only real way to remove a section is to rewrite the file! 
        ' so to support this method we have to load all sections and all keys
        ' all the time even if we dont need them so that we can write the
        ' out the whole file omitting removed keys and sections.
        '
        ' Seriously sir, this kind of junk went to the dustbin with Rubik's Cubes
    
            End Sub
    
            Public Function Exists(secName As String)
                Return IndexOfSection(secName) <> -1
            End Function
    
            Private Function IndexOfSection(name As String) As Integer
                For n As Integer = 0 To Items.Count - 1
                    ' s/b ToLowerInvariant - that makes the screen scroll
                    If Items(n).SectionName.ToLower = name.ToLower Then
                        Return n
                    End If
                Next
                Return -1
            End Function
    
        End Class
    End Class
    
    ' ************** INISection item class
    Public Class IniSection
        ' mostly methods go here for sections,
        ' but is the "host" for the keys collections
    
        Private myKeys As Dictionary(Of String, String)
    
        ' for a .Keys collection (WHY would the calling code WANT to
        ' mess with the whole collection???), change to add a Key Class
        ' and Keys Collection
    
        ' interface for Keys
        Public Property Keys(name As String) As String
            Get
                If myKeys.ContainsKey(name) Then
                    Return myKeys(name)
                Else
                    Return ""
                End If
            End Get
            Set(value As String)
                If myKeys.ContainsKey(value) Then
                    myKeys(value) = value
                Else
                    myKeys.Add(value, value)
                End If
            End Set
        End Property
    
        Public Property SectionName As String
    
        Public Sub New(name As String)
            SectionName = name
            myKeys = New Dictionary(Of String, String)
        End Sub
    
        Public Sub RemoveKey(name As String)
            If myKeys.ContainsKey(name) Then
                myKeys.Remove(name)
            End If
        End Sub
    
        Friend Sub Save(inifile As String)
            ' iterate keys writitng the kvps to the ini
    
        End Sub
    
        ' note FRIEND called by the INISection class not the user
        Friend Function LoadKeyValues(inifile As String) As Integer
            '  presumably call GetPrivateProfileSection  
            '   for this SectionName and parse it to 
            ' get the current key=value pairs into myKeys
            Return myKeys.Count
        End Function
    
    End Class
    

    Sample syntax:

    ini = New INIManager("C:\Temp\Ziggy.INI")
    Dim foo As String = ini.Sections("foo").Keys("bar")
    
    ini.Sections("ziggy").Keys("foo") = "zoey"
    ini.Sections("ziggy").RemoveKey("zacky")
    

    These dont match syntactically because I didnt create a Key class and Keys collection class (5 classes for 2 bits of information is insane). To change it so the setter matches, remove the Keys accessor and add a .ReadKey() and SetKey so it matches syntactically and keep the keys collection internal. You'll end up with:

    ini.Sections("ziggy").RemoveKey("zacky")
    ini.Sections("ziggy").ReadKey("ziggy")
    ini.Sections("ziggy").SetKey(keyName, "zoey")
    

    At least they match syntactically

    ini.Sections.Add("ziggy")
    ini.Sections.Remove("zoey")
    If ini.Sections.Exists("zacky") Then
        Console.Beep()
    End If
    
    ' causes a cascade from INI -> Sections -> keys to save
    ini.Save()