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 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
Responsibilities
ResponsibilityOne
ResponsibilityTwo
ResponsibilityThree
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.
Expand|Select|Wrap|Line Numbers
- 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
Employee
JobPriorities
Group
Lets ignore the class decoration for now. (the decoration is all that <xmlRoot("Employees")> 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
Expand|Select|Wrap|Line Numbers
- 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
Expand|Select|Wrap|Line Numbers
- ReDim Preserve EmploymentData.Employees(2)
Expand|Select|Wrap|Line Numbers
- Dim MyPriorities As JobPriorities
Expand|Select|Wrap|Line Numbers
- 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)
Expand|Select|Wrap|Line Numbers
- SaveXML(MyXMLFile, EmploymentData, GetType(Group))
The Generic Serialization Code
Expand|Select|Wrap|Line Numbers
- 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
Once it is written out the XML looks like this:
Expand|Select|Wrap|Line Numbers
- <?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>
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:
Expand|Select|Wrap|Line Numbers
- 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
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.
Expand|Select|Wrap|Line Numbers
- 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
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("Employees")> _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("IndividualEmployees:)> _Since it will be an array the serializer will give each Employee node the tag name of "IndividualEmployee." 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("Jobs")> _
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