VBNET:Class XMLINI

From GPWiki

Files:GUITutorial_warn.gif The Game Programming Wiki has moved! Files:GUITutorial_warn.gif

The wiki is now hosted by GameDev.NET at wiki.gamedev.net. All gpwiki.org content has been moved to the new server.

However, the GPWiki forums are still active! Come say hello.

In VB6, one way to store data has long been the elderly INI file. Since Windows 95, Microsoft has pushed hard to get rid of the INI file, and move that data into the registry. Many developers are wary of that approach, fearful of contributing to the "clutter" of the registry. So the INI file has survived despite Microsoft's best efforts.

And there are other advantages to an INI file. In Rogue (a game we're working on over at Atomic Monks), it holds video settings, key mappings, and more. It's much quicker to open Notepad and change an INI file than it is to modify registry entries. But INI support in .NET falls outside of managed code...so if you want a "real" .NET way of doing this, you need to go to XML.

Let's look at the video section of Rogue's INI file:

[Video]
 Height=768
 Width=1024
 Windowed=1
 TripleBuffer=0 

And the XML file for the same entries:

<?xml version="1.0" encoding="utf-8"?>
 <sections>
   <section name="video">
     <item key="Width" value="800" />
     <item key="Height" value="600" />
     <item key="Windowed" value="1" />
     <item key="TripleBuffer" value="0" />
   </section>
 </sections>

The structure is easy enough to follow, and is modelled after the INI file structure (different sections, with numerous key/value sets). The hard part of dealing with the XML itself will be handled by the xmlDocument object.

Let's first take a look at the class declarations and the New procedure(s):

Public Class CXMLINI
    Private doc As New Xml.XmlDocument
    Private xmlfile As String<br/>
    Public Sub New(ByVal Filename As String)
        ' open xml file for use
        Try
            doc.Load(Filename)
            xmlfile = Filename
        Catch ex As Exception<br/>
        End Try
    End Sub<br/>
    Public Sub New()
        ' open default xml file for use
        Dim s As String<br/>
        s = System.AppDomain.CurrentDomain.BaseDirectory
        s &= Application.ProductName
        s &= ".xml"<br/>
        Try
            doc.Load(s)
            xmlfile = s
        Catch ex As Exception<br/>
        End Try
    End Sub

doc is the xmlDocument object for the class, which we'll maintain until the class is destroyed. This object stores the contents of the xml file we load in memory, where we can almost treat it like a dataset.

Notice that I've overloaded the New procedure. When given a filename, it opens that xml file. Otherwise, it defaults to Application Name.xml. In either case, if the file doesn't exist, it catches the error and goes on, but remembers the file name that it was supposed to use (for later use).

In the class destructor, Finalize, we see why we saved that name:

Protected Overrides Sub Finalize()
     ' save changes to settings here, if we haven't already
     Try
         ' save changes to data
         doc.Save(xmlfile)
     Catch ex As Exception<br/>
     End Try
     MyBase.Finalize()
 End Sub

Any changes made will be saved automatically here, so if the file didn't exist before, it will now.

Now for GetSetting, which should be a familiar command for this sort of thing:

Public Function GetSetting(ByVal Section As String, ByVal Key As String, _
                            ByVal DefaultValue As String) As String
     Dim node As Xml.XmlNode<br/>
     Try
         node = doc.SelectSingleNode("/sections/section[@name='" & _
                                     Section & "']/item[@key='" & Key & "']")<br/>
         If node Is Nothing Then
             GetSetting = DefaultValue
         Else
             GetSetting = node.Attributes("value").Value
         End If
     Catch ex As Exception<br/>
     End Try
 End Function

Here we see our first instance of the xmlNode object, which represents a single line of our XML file. With the SelectSingleNode method we can attach to a particular item in a particular section. The format for the search is:

/sections/section[@name='sectionname']/item[@key='keyname']

The brackets are how you specify an element parameter to match ("name" for section and "key" for items, as seen in the sample XML file at the beginning of this article). It will return nothing if there is no match...otherwise it returns a node, from which we can read the "value" attribute to return from the function.

The full version of the class I created includes several overloads for GetSetting to more cleanly handle different data types, such as Boolean and Integer. They convert the passed parameters to strings, call the "root" GetSetting function, and convert the returned string as Boolean or Integer, as appropriate.

The other important piece of this is SaveSetting, which is only slightly more involved than GetSetting:

Public Sub SaveSetting(ByVal Section As String, ByVal Key As String, _
                        ByVal NewValue As String)
     Dim node As Xml.XmlNode, keynode As Xml.XmlNode<br/>
     Try
         ' check for/create section
         node = doc.SelectSingleNode("/sections/section[@name='" & Section & "']")
         If node Is Nothing Then
             ' section doesn't exist, create
             node = doc.SelectSingleNode("/sections")<br/>
             Dim newnode As Xml.XmlNode = doc.CreateElement("section")<br/>
             Dim att As Xml.XmlAttribute = doc.CreateAttribute("name")
             att.Value = Section
             newnode.Attributes.Append(att)<br/>
             node.AppendChild(newnode)
             node = doc.SelectSingleNode("/sections/section[@name='" & Section & "']")
         End If<br/>
         ' get key
         keynode = doc.SelectSingleNode("/sections/section[@name='" & _
                                        Section & "']/item[@key='" & Key & "']")
         If keynode Is Nothing Then
             ' create key
             Dim newnode As Xml.XmlNode = doc.CreateElement("item")<br/>
             Dim att As Xml.XmlAttribute = doc.CreateAttribute("key")<br/>
             att.Value = Key
             newnode.Attributes.Append(att)<br/>
             att = doc.CreateAttribute("value")
             att.Value = NewValue
             newnode.Attributes.Append(att)<br/>
             node.AppendChild(newnode)
         Else
             ' just update key value
             keynode.Attributes("value").Value = NewValue
         End If<br/>
     Catch ex As Exception<br/>
     End Try
 End Sub

With a first call of SelectSingleNode, SaveSetting checks to see if the required section exists. If not, then it creates it with the CreateElement method of xmlDocument. It then sets the name of the section by creating a new xmlAttribute object, which it appends to the new section node.

The same thing is done for the actual key next, with one more attribute (since the items have "key" and "value"). In either case, if the section or item already exists, it simply updates the attributes accordingly.

Again, my class has some overloads for SaveSetting to handle Boolean and Integer values.

Improvements might include doing a SaveSetting when a GetSetting is going to return a default value. This would create and populate the wanted XML file, data and all, if none existed.




Last Modified By Brother Erryn 10:11, 16 Aug 2004 (EDT)