Basic XML Serialization in VB.NET

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • !NoItAll
    Contributor
    • May 2006
    • 297

    Basic XML Serialization in VB.NET

    Reading XML in VB.NET is pretty straightforward . Create an XML Document Object, then you can load the XML and iterate through all of the nodes as required. You can search for nodes, use x-path, whatever you like.
    Creating XML, on the other hand, is not so straight forward. There are essentially two methods.
    • Create an XML Document Object and set each of the Element and/or attribute nodes to the values you want
    • Create the structure you want with serialization in mind and serialize the data to XML


    Option 1 is, in my opinion, a non-option. While it may be easier to create your structures without having to think of serialization, the process of creating the XML Document Object (that's not hard) and then creating and setting each of the Element and Attribute nodes is suicidal! It also will create a code segment that will collapse under its own weight, even with less-than-complex XML! I have a project I did prior to learning serialization and the process of saving the XML takes about 3 pages of code, and I can't understand it any more!

    Enter XML Serialization
    I am not going to go into excrutiating detail on all of this, instead I have created a VS 2008 project you can download and play with. In it you will see the following things:
    • A few classes that are designed to create the data structure I wish to create as XML and read in as XML.
    • A way to name the XML nodes anything you want
    • A way to keep the XML clean (by default .NET will load all kinds of namespace garbage into your XML that doesn't need to be there)
    • How to save the Data Object as XML using a generic serialization function
    • How to read the XML into the Data Object using a generic serialization function

    I think that covers it...

    I am going to create a simple structure for Employees. This structure will consist of a four basic data points - one of which will be a nested structure with three data points of its own.
    Employee
    FirstName
    LastName
    Birthday
    Responsibilitie s
    ResponsibilityO ne
    ResponsibilityT wo
    ResponsibilityT hree


    We will need to be able to create an array of this structure because we will have more than one employee.

    We are going to start by building the structure in classes. I suppose you could do it with a simple structure - I may try that later, but today we are going to create classes. I like classes. The classes will all be inside a vb module I call Serialize. Its a dumb name - but it's only a demo folks.

    Code:
    Public Module Serialize
    
        Public EmploymentData As New Group
    
        <XmlRoot("Employees")> _
        Public Class Group
            <XmlElement("IndividualEmployee")> _
            Public Employees() As Employee
        End Class
    
        Public Class Employee
            Private vFirstName As String = ""
            Private vLastName As String = ""
            Private vBirthday As New Date
    
            Private vPriorities As JobPriorities
    
            Public Sub New()
            End Sub
    
            Public Sub New(ByVal FirstName As String, ByVal LastName As String, ByVal Birthday As Date, ByVal Responsibilities As JobPriorities)
                Me.FirstName = FirstName
                Me.LastName = LastName
                Me.Birthday = Birthday
                Me.Priorities = Responsibilities
            End Sub
    
            <XmlAttribute("firstname")> _
            Public Property FirstName() As String
                Get
                    Return vFirstName
                End Get
                Set(ByVal value As String)
                    vFirstName = value
                End Set
            End Property
    
            <XmlAttribute("lastname")> _
            Public Property LastName() As String
                Get
                    Return vLastName
                End Get
                Set(ByVal value As String)
                    vLastName = value
                End Set
            End Property
    
            <XmlAttribute("birthday")> _
            Public Property Birthday() As String
                Get
                    Return vBirthday
                End Get
                Set(ByVal value As String)
                    vBirthday = value
                End Set
            End Property
    
            <XmlElement("Jobs")> _
            Public Property Priorities() As JobPriorities
                Get
                    Return vPriorities
                End Get
                Set(ByVal value As JobPriorities)
                    vPriorities = value
                End Set
            End Property
    
        End Class
    
        Partial Public Class JobPriorities
    
            Private vResponsibilityOne As String = ""
            Private vResponsibilityTwo As String = ""
            Private vResponsibilityThree As String = ""
    
            Public Sub New()
            End Sub
    
            Public Sub New(ByVal ResponsibilityOne As String, Optional ByVal ResponsibilityTwo As String = "", Optional ByVal ResponsibilityThree As String = "")
                Me.ResponsibilityOne = ResponsibilityOne
                Me.ResponsibilityTwo = ResponsibilityTwo
                Me.ResponsibilityThree = ResponsibilityThree
            End Sub
    
            <XmlElement("first_priority")> _
            Public Property ResponsibilityOne() As String
                Get
                    Return vResponsibilityOne
                End Get
                Set(ByVal value As String)
                    vResponsibilityOne = value
                End Set
            End Property
    
            <XmlElement("Second_priority")> _
            Public Property ResponsibilityTwo() As String
                Get
                    Return vResponsibilityTwo
                End Get
                Set(ByVal value As String)
                    vResponsibilityTwo = value
                End Set
            End Property
    
            <XmlElement("Third_priority")> _
            Public Property ResponsibilityThree() As String
                Get
                    Return vResponsibilityThree
                End Get
                Set(ByVal value As String)
                    vResponsibilityThree = value
                End Set
            End Property
    
        End Class
    I am creating three classes.

    Employee
    JobPriorities
    Group

    Lets ignore the class decoration for now. (the decoration is all that <xmlRoot("Emplo yees")> kind of stuff in front of each class. I'll explain that a little later.

    The Group class allows me to create an array of employees, and is in fact a very small, but necessary class.

    The Employee class contains the three primitive elements and one element that is of the JobPriorities class.

    There's really nothing special in these classes. You can see that I did overload the New constructor - mostly just for convenience - so users would have a way to construct a new object and set the properties easily. These are very simple and straightforward classes with appropriate setters and getters. It has properly constructed private variables and public properties.

    Creating the Object, Setting the Properties, and Serializing to XML
    Notice at the beginning of the Serialize module I create a global object called EmploymentData. This is the object I will add data to before I serialize the output to XML.

    In a button_click event I put the following code

    Code:
        Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
    
            Dim MyPriorities As JobPriorities
    
            ReDim Preserve EmploymentData.Employees(2)
    
            MyPriorities = New JobPriorities("Secretary", "Eye Candy", "Mistress")
            EmploymentData.Employees(0) = New Employee("Melissa", "Ryan", "1/1/1970", MyPriorities)
    
            MyPriorities = New JobPriorities("Janitor", "Cook", "Bottlewasher")
            EmploymentData.Employees(1) = New Employee("Amanda", "Terranova", "1/1/1980", MyPriorities)
    
            MyPriorities = New JobPriorities("Boss", "Idiot", "Syncophant")
            EmploymentData.Employees(2) = New Employee("Harlan", "Butts", "1/1/1980", MyPriorities)
    
            SaveXML(MyXMLFile, EmploymentData, GetType(Group))
    
        End Sub
    Here I am Creating a MyPriorities object from the JobPriorities class and setting the properties of the EmploymentData object with a bunch of hard coded values. I felt it was important to show some level of nesting in my example - because that can be confusing and difficult to understand for someone just starting out (like me). The EmploymentData object (which was declared Globally in the Serialize module) will have three employees - so I need to Redim it to 2 (0, 1, 2).
    Code:
    ReDim Preserve EmploymentData.Employees(2)
    I now have an array of EmploymentData that I can fill out. One of the properties of the EmploymentData is going to be a JobPriorities type and I have created MyPriorities for that.

    Code:
    Dim MyPriorities As JobPriorities
    Now I can fill in the structure array

    Code:
            MyPriorities = New JobPriorities("Secretary", "Eye Candy", "Mistress")
            EmploymentData.Employees(0) = New Employee("Melissa", "Ryan", "1/1/1970", MyPriorities)
    
            MyPriorities = New JobPriorities("Janitor", "Cook", "Bottlewasher")
            EmploymentData.Employees(1) = New Employee("Amanda", "Terranova", "1/1/1980", MyPriorities)
    
            MyPriorities = New JobPriorities("Boss", "Idiot", "Syncophant")
            EmploymentData.Employees(2) = New Employee("Harlan", "Butts", "1/1/1980", MyPriorities)
    And finally I can send it to my generic Serialization function

    Code:
            SaveXML(MyXMLFile, EmploymentData, GetType(Group))
    Here is where the heavy lifting takes place. The function to serialize the entire structure is really just a few lines of code. I kept it basic and generic so that virtually any object can be passed. I did this by having it accept anything derived from the Object base class (just about everything in VB) and returning anything derived from the Object class. When you SaveXML you pas it the filename, the Object with the data, and you have to tell it exactly what object you are sending it.

    The Generic Serialization Code

    Code:
        Public Function SaveXML(ByVal FileName As String, ByVal DataToSerialize As Object, ByVal objType As Type) As Boolean
    
            'set up a blank namespace to eliminate unnecessary junk from the xml
            Dim nsBlank As New XmlSerializerNamespaces
            nsBlank.Add("", "")
    
            'create an object for the xml settings to control how the xml is written and appears
            Dim xSettings As New System.Xml.XmlWriterSettings
            With xSettings
                .Encoding = Encoding.UTF8
                .Indent = True
                .NewLineChars = Environment.NewLine
                .NewLineOnAttributes = False
                .ConformanceLevel = Xml.ConformanceLevel.Document
            End With
    
            Try
                'create the xmlwriter object that will write the file out
                Dim xw As System.Xml.XmlWriter = Xml.XmlWriter.Create(FileName, xSettings)
    
                'create the xmlserializer that will serialize the object to XML
                Dim writer As New XmlSerializer(objType)
    
                'now write it out
                writer.Serialize(xw, DataToSerialize, nsBlank)
    
                'be sure to close it or it will remain open
                xw.Close()
    
                Return True
    
            Catch ex As Exception
    
                MsgBox(ex.Message)
                Return False
    
            End Try
    
    
        End Function
    This code is pretty self-explanatory. The one, non-obvious, element is my creation of a new XmlSerializerNa mespaces object. I did this because .NET, by default, will put some namespace declarations into your XML. It's not necessary, in only creates noise in my opinion. So by creating a blank Namespace and including that in my serializer, it will remove it.

    Once it is written out the XML looks like this:

    Code:
    <?xml version="1.0" encoding="utf-8"?>
    <Employees>
      <IndividualEmployee firstname="Melissa" lastname="Ryan" birthday="1/1/1970">
        <Jobs>
          <first_priority>Secretary</first_priority>
          <Second_priority>Eye Candy</Second_priority>
          <Third_priority>Mistress</Third_priority>
        </Jobs>
      </IndividualEmployee>
      <IndividualEmployee firstname="Amanda" lastname="Terranova" birthday="1/1/1980">
        <Jobs>
          <first_priority>Janitor</first_priority>
          <Second_priority>Cook</Second_priority>
          <Third_priority>Bottlewasher</Third_priority>
        </Jobs>
      </IndividualEmployee>
      <IndividualEmployee firstname="Harlan" lastname="Butts" birthday="1/1/1980">
        <Jobs>
          <first_priority>Boss</first_priority>
          <Second_priority>Idiot</Second_priority>
          <Third_priority>Syncophant</Third_priority>
        </Jobs>
      </IndividualEmployee>
    </Employees>
    This is very pretty because I set the properties of the XMLWriterSettin gs object I created (xSettings) to define the look of the XML.

    Reading the XML into the EmploymentData object

    Getting the XML back into an EmploymentData object is equally simple.

    In another Button_Click event I place the following code:

    Code:
        Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    
            Dim EmployeeData As New Group
    
            EmployeeData = GetXML(MyXMLFile, GetType(Group))
    
            'the following is just so you can see the output from the EmploymentData object
    
            Dim sData As Employee
            With TextBox1
                For Each sData In EmployeeData.Employees
                    .SelectedText = "Employee: " & sData.FirstName & " " & sData.LastName & Environment.NewLine
                    .SelectedText = "Birthday: " & sData.Birthday & Environment.NewLine
                    .SelectedText = "Job Responsabilities: " & Environment.NewLine
                    .SelectedText = "     " & sData.Priorities.ResponsibilityOne & Environment.NewLine
                    .SelectedText = "     " & sData.Priorities.ResponsibilityTwo & Environment.NewLine
                    .SelectedText = "     " & sData.Priorities.ResponsibilityThree & Environment.NewLine
                    .SelectedText = "---------------" & Environment.NewLine
                Next
                .SelectedText = ""
            End With
    
        End Sub
    I create a new EmployeeData object based on Group. Here I do not need to create the Array as the deserialization will create the array for me.
    There's also a bunch of code that places the results into a text box just so you can see that it actually deserialized the XML and created the array.

    The deserializer, like the serializer is generic. I followed the same rules and return the data in the base Object class. To use the generic deserializer you only need to tell it where the XML file is, and tell it what type of object you are expecting to deserialize.

    Code:
        Public Function GetXML(ByVal sFileName As String, ByVal objType As Type) As Object
    
            If My.Computer.FileSystem.FileExists(sFileName) Then
                Dim fs As FileStream = New FileStream(sFileName, FileMode.Open)
                Dim xs As XmlSerializer = New XmlSerializer(objType)
                Dim obj As Object = CType(xs.Deserialize(fs), Object)
                fs.Close()
                Return obj
            Else
                Return Nothing
            End If
    
        End Function
    This works a treat!

    XML Decoration

    By default the XML will use your class names for node names. This may be perfectly fine with you. I wanted to override that naming in another project I am doing so I needed to figure that out. Here is the process.

    When creating the classes for the Group Class you can tell the Serializer to use different names for the Elements and Attributes. By preceding each class with the appropriate decoration you will override the default behavior of the serialization code (which is to use the class names).

    Since Group is my root class I decorate it with
    <XmlRoot("Emplo yees")> _

    Notice in the Group Class I create one member variable array called Employees which I want to appear as XML Elements so I use the decoration:
    <XmlElement("In dividualEmploye es:)> _

    Since it will be an array the serializer will give each Employee node the tag name of "IndividualEmpl oyee." You can call the node whatever you like.

    For each of the properties in the Employee class I decorated appropriate names, but also declared them to be Attributes. I did this with the following decoration:
    <XmlAttribute(" firstname")> _

    I did this with three of the four properties in the Employee class so that when Employee was serialized they would become Attributes in XML rather than new tags. You could place them in as Elements if you like, but I like the way Attributes read for this data. The Priorities Property I decided to call "Jobs" and make it an Element (because it would have sub elements). I used the following decoration there:

    <XmlElement("Jo bs")> _


    In the JobPriorities class I used the decorations to create elements and name them appropriately.

    All in all some of my renaming is probably making the code a bit more confusing, but I felt it was important to show how decorations can be used to define how properties appear as either Attributes or Elements, and that you can have them appear in the XML with any name you want. So if it's confusing - I know about it, but it was done that way as a learning tool.


    I've included a full VS2008 VB.NET project you can play with. It's self contained and includes all the source referenced in this article.

    Hopefully I haven't done anything stupid - but don't be shy about letting me know.

    Des
    Attached Files
  • !NoItAll
    Contributor
    • May 2006
    • 297

    #2
    Classes are nice, but perhaps you want to do it a little more simply. Instead of the class structure I did originally - you can do it entirely with simple structures in VB.NET as well. Structures are really very similar to classes - and they appear to also be derived from the Object base class - so the generic serializer and deserializer work just fine. You can pass my generic SaveXML and ReadXML functions objects that are structure or classes.

    Here are my structures
    Code:
        <Serializable()> _
        <XmlRoot("employees")> _
        Public Structure T_Group
            <XmlElement("employee")> Public Employees() As T_Employee
        End Structure
    
        Public Structure T_Employee
            <XmlAttribute("firstname")> Public FirstName As String
            <XmlAttribute("lastname")> Public LastName As String
            <XmlAttribute("birthday")> Public Birthday As Date
            <XmlElement("jobs")> Public Jobs As T_Jobs
        End Structure
    
        Public Structure T_Jobs
            <XmlElement("first_priority")> Public ResponsibilityOne As String
            <XmlElement("second_priority")> Public ResponsibilityTwo As String
            <XmlElement("third_priority")> Public ResponsibilityThree As String
        End Structure
    As you can see there's a lot less code than in the classes, but it's also not as powerful. Sometimes you just don't need the power though.
    Same decoration rules apply.
    I've uploaded the original project with the structure approach added to it. You can look at both.
    Again - if you see stupidity I'm not to proud to admit it - so let me know if you see better ways of doing things.

    Des
    Attached Files

    Comment

    • sashi
      Recognized Expert Top Contributor
      • Jun 2006
      • 1749

      #3
      Hi there,

      Nice piece of work! Keep up the good job :)

      Comment

      Working...