How to Serialize a Hashtable (and a Date)
In a previous blog entry, i mentioned that i figured out how to get the one XMLSerializer to handle multiple types and hashtables all in one hit.
That post is currently third in number of hits based on the referrer info i get. (First and second go to the posts on CSS Expressions and TBODY scrolling, and HTML Table Column Resizing). All three are this high (much higher than almost any other post that i've done) due to google searches. People seem to want answers to these things, so I figured I should give a legitimate example on how to do it - i don't want to disappoint people afterall, do I? :)
So first we'll set the scene on what I want to cover. The scenario is this.
I have a base class, from which two other classes inherit. I want them all to be XMLSerializable, but i want to handle all three in one hit.
Also, I want to store instances of these in a hashtable, I want the hash to contain all three class types at once, and i want to be able to serialize that too.
Sounds hard? It did to me when i first started, but now, to put it in Australian terms, it's a Peice of Piss, Mate!
Now, let's get the base class and the two derived ones sorted.
Public Class BaseClass
Private mlID As Int32
Private msText As String
Public Sub New()
mlID = -1
msText = vbNullString
End Sub
'by setting the default value attributes, we don't bloat the xml if the
'current value is the same as the default value. The XMLSerializer skips
'it, and expects it to be restored to the default at the other end.
<System.ComponentModel.DefaultValue(-1)> _
Public Property ID() As Int32
Get
Return mlID
End Get
Set(ByVal Value As Int32)
mlID = Value
End Set
End Property
<System.ComponentModel.DefaultValue(vbNullString)> _
Public Property Text() As String
Get
Return msText
End Get
Set(ByVal Value As String)
msText = Value
End Set
End Property
End Class
What we've got here is a simple ordinary class with a couple of properties. Taking advantage of the fact that we'll be storing it in a hashtable later, i've added an ID property to use as the index.
The most important part of this class is the default values. Specify a default value attribute on the properties, and make sure you match that in your constructor.
Next here's two more small classes, both extending the base class. I'm only giving the source to prove that they really did :)
Public Class Extended1
Inherits BaseClass
Private msExtendedText As String
Public Sub New()
MyBase.New()
msExtendedText = vbNullString
End Sub
<System.ComponentModel.DefaultValue(vbNullString)> _
Public Property ExtendedText() As String
Get
Return msExtendedText
End Get
Set(ByVal Value As String)
msExtendedText = Value
End Set
End Property
End Class
Public Class Extended2
Inherits BaseClass
Private msExtendedText2 As String
Public Sub New()
MyBase.New()
msExtendedText2 = vbNullString
End Sub
<System.ComponentModel.DefaultValue(vbNullString)> _
Public Property ExtendedText2() As String
Get
Return msExtendedText2
End Get
Set(ByVal Value As String)
msExtendedText2 = Value
End Set
End Property
End Class
Let's go all out now and jump straight to the hashtable part. The way i wrote this class is to appear like the base functionality of the hashtable, while not really being one - it just uses one internally as a member variable.
Public Class MyHash
Private mlSomeProperty As Int32
Private moMyItems As Hashtable
Public Sub New()
mlSomeProperty = -1
moMyItems = New Hashtable
End Sub
'extended constructor just for fun.
Public Sub New(ByVal plSomeProperty As Int32)
Me.New()
mlSomeProperty = plSomeProperty
End Sub
<System.ComponentModel.DefaultValue(-1)> _
Public Property SomeProperty() As Int32
Get
Return mlSomeProperty
End Get
Set(ByVal Value As Int32)
mlSomeProperty = Value
End Set
End Property
'the browsable attribute doens't stop you using this property. But it does
'hide it from the PropertyGrid, and it's often a good sign that you shoulnd't
'touch it unless you know what you are doing. In this case, it's here
'purely for the XMLSerializer and nothing else - your documentation should
'of course dictate this to your other devs.
<System.ComponentModel.Browsable(False)> _
Public Property MyItems() As BaseClass()
Get
Dim oMyItemArray() As BaseClass
ReDim Preserve oMyItemArray(moMyItems.Count - 1)
moMyItems.Values.CopyTo(oMyItemArray, 0)
Return oMyItemArray
End Get
Set(ByVal Value() As BaseClass)
moMyItems.Clear()
For Each oBaseClass As BaseClass In Value
moMyItems.Add(oBaseClass.ID, oBaseClass)
Next
End Set
End Property
'this attribute instructs the XMLSerializer to ignore this property.
<System.Xml.Serialization.xmlIgnore()> _
Public ReadOnly Property Keys() As ICollection
Get
Return moMyItems.Keys
End Get
End Property
<System.Xml.Serialization.XmlIgnore()> _
Public ReadOnly Property Values() As ICollection
Get
Return moMyItems.Values
End Get
End Property
'add an item to the hash table
Public Sub Add(ByVal poBaseClass As BaseClass)
moMyItems.Add(poBaseClass.ID, poBaseClass)
End Sub
'locate an item in the hash table
Public Function Item(ByVal plKey As Int32) As BaseClass
If moMyItems.ContainsKey(plKey) Then
Return DirectCast(moMyItems.Item(plKey), BaseClass)
End If
Return Nothing
End Function
End Class
This is emulating a strongly-typed hashtable. The other way you could do it is inherit from DictionaryBase, but this way is much easier (in my lazy opinion). I haven't shown here an implementation of things like Clear() and Remove(), because, well, it's obvious, innit? And i added an extra property to this hash class, just to prove that it gets serialized too :)
There's three things going on here of interest. The first is the Browsable(False) attribute. This is not necessary, but an easy way of marking the property as non-touchable to other developers who might use this code.
The second is the XMLIgnore attribute. The XMLSerializer skips happily over it.
The third is the MyItems property. To get the contents of the hashtable serialized, it needs to be somehow 'accessible' by the XMLSerializer. Which means it needs to be a in a property somewhere. The important bit is that you don't just return the hashtable, or even just the values collection. The XMLSerializer barfs on this. You need to return an Array. By doing this, everything is good. Of course, the contents of the array must be serializable too...
The a little bit of specifics in this that you should take note of too. Because it's a strongly-typed hash, you know the classes you're dealing with. So internally, we know we can use the ID() property of the base class as the key into the hashtable. Also, it's only strongly typed to BaseClass. This also means that we can store Extended1 and Extended2 objects in it too - so long as we cast them down to the base.
So how do we get it serialized? Well, here we go. This is an all encompassing serializer that deals with all 4 classes I've discussed so far.
Imports System.Xml.Serialization
Public Class MySerializer
Private moBaseHandler As XmlSerializer
Private moMyHashHandler As XmlSerializer
Private oArr(1) As System.Type
Public Sub New()
oArr(0) = GetType(Extended1)
oArr(1) = GetType(Extended2)
moBaseHandler = New XmlSerializer(GetType(BaseClass), oArr)
moMyHashHandler = New XmlSerializer(GetType(MyHash), oArr)
End Sub
Public Function SerializeMyHash(ByVal poMyHash As MyHash) As String
Dim oWriter As New System.IO.StringWriter
moMyHashHandler.Serialize(oWriter, poMyHash)
Return oWriter.ToString
End Function
Public Function DeSerializeMyHash(ByVal psSerialized As String) As MyHash
Dim oReader As New System.IO.StringReader(psSerialized)
Dim oRet As MyHash
oRet = DirectCast(moMyHashHandler.Deserialize(oReader), MyHash)
Return oRet
End Function
Public Function Serialize(ByVal poBaseClass As BaseClass) As String
Dim oWriter As New System.IO.StringWriter
moBaseHandler.Serialize(oWriter, poBaseClass)
Return oWriter.ToString
End Function
Public Function DeSerialize(ByVal psSerialized As String) As BaseClass
Dim oReader As New System.IO.StringReader(psSerialized)
Dim oRet As BaseClass = DirectCast(moBaseHandler.Deserialize(oReader), BaseClass)
Return oRet
End Function
End Class
This is a little bit of overkill, creating seperate serializers for the base class and the hash table. But, it's nice, and it doesn't create a new serializer every time you call it, which is useful if you need to do a few. The important thing here is up in the constructor. You create an array of all the types you'll be dealing with, and give that to the constructor of the XMLSerializer object. Every thing else is just easy.
Just to prove that it works, here's my text code:
Private Sub TestSerializer()
Dim i As Int32
Dim oBase As BaseClass
Dim oEx1 As Extended1
Dim oEx2 As Extended2
Dim oHash As New MyHash
Dim oSerializer As New MySerializer
Dim sXMLText As String
Dim oSecondHash As MyHash
'set the hashes custom property, to prove the point
oHash.SomeProperty = 13
'create 5 base classes, and only set the text if they are odd
For i = 1 To 5
oBase = New BaseClass
oBase.ID = i
If (i And 1) = 1 Then
oBase.Text = "Some Text " & i.ToString
End If
oHash.Add(oBase)
Next
'create 5 Ex1's. Only set the text if odd, always set
'the extended text
For i = 6 To 10
oEx1 = New Extended1
oEx1.ID = i
If (i And 1) = 1 Then
oEx1.Text = "Some Text " & i.ToString
End If
oEx1.ExtendedText = "Some Extended Text " & i.ToString
oHash.Add(oEx1)
Next
'create 5 Ex2's. Only set the text if odd, never set
'the extended2 text
For i = 11 To 15
oEx2 = New Extended2
oEx2.ID = i
If (i And 1) = 1 Then
oEx2.Text = "Some Text " & i.ToString
End If
oHash.Add(oEx2)
Next
'serialize the hash
sXMLText = oSerializer.SerializeMyHash(oHash)
'restore it to a new hash
oSecondHash = oSerializer.DeSerializeMyHash(sXMLText)
End Sub
Yeah, i'm doing nothing with the results, but in the debugger I've already verified that this code works :)
As a last bit of spam, here's the XML output, so you can see for yourself what happened.
<?xml version="1.0" encoding="utf-16"?>
<MyHash xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SomeProperty>13</SomeProperty>
<MyItems>
<BaseClass xsi:type="Extended2">
<ID>15</ID>
<Text>Some Text 15</Text>
</BaseClass>
<BaseClass xsi:type="Extended2">
<ID>14</ID>
</BaseClass>
<BaseClass xsi:type="Extended2">
<ID>13</ID>
<Text>Some Text 13</Text>
</BaseClass>
<BaseClass xsi:type="Extended2">
<ID>12</ID>
</BaseClass>
<BaseClass xsi:type="Extended2">
<ID>11</ID>
<Text>Some Text 11</Text>
</BaseClass>
<BaseClass xsi:type="Extended1">
<ID>10</ID>
<ExtendedText>Some Extended Text 10</ExtendedText>
</BaseClass>
<BaseClass xsi:type="Extended1">
<ID>9</ID>
<Text>Some Text 9</Text>
<ExtendedText>Some Extended Text 9</ExtendedText>
</BaseClass>
<BaseClass xsi:type="Extended1">
<ID>8</ID>
<ExtendedText>Some Extended Text 8</ExtendedText>
</BaseClass>
<BaseClass xsi:type="Extended1">
<ID>7</ID>
<Text>Some Text 7</Text>
<ExtendedText>Some Extended Text 7</ExtendedText>
</BaseClass>
<BaseClass xsi:type="Extended1">
<ID>6</ID>
<ExtendedText>Some Extended Text 6</ExtendedText>
</BaseClass>
<BaseClass>
<ID>5</ID>
<Text>Some Text 5</Text>
</BaseClass>
<BaseClass>
<ID>4</ID>
</BaseClass>
<BaseClass>
<ID>3</ID>
<Text>Some Text 3</Text>
</BaseClass>
<BaseClass>
<ID>2</ID>
</BaseClass>
<BaseClass>
<ID>1</ID>
<Text>Some Text 1</Text>
</BaseClass>
</MyItems>
</MyHash>
Pretty cool huh? Now, all you people that have arrived here from google - was I of any help?
PS - I've only just thought of this now, and i'm not going back and adding it in, so here's one more tip that I've found useful. Serializing dates is pretty crappy. If you use this method, and have a date property, here's what I do to get it sorted nicely.
Private mdDate As Date
<System.Xml.Serialization.XmlIgnore()> _
Public Property DateProperty() As Date
Get
Return mdDate
End Get
Set(ByVal Value As Date)
mdDate = Value
End Set
End Property
<System.ComponentModel.Browsable(False)> _
Public Property DatePropertyAsTicks() As Long
Get
Return mdDate.Ticks
End Get
Set(ByVal Value As Long)
mdDate = New Date(Value)
End Set
End Property
You leave your normal date property as is - but mark it so that the serializer ignores it. Then add another property, mark it non browsable so the other devs you work with don't use it (and document the fact that it shouldn't be used!) and get it to return/set Date Ticks. This is just a constant number of Ticks since a specific date. Handy :)
Listening to: brothers in arms (live) - dire straits - (8:54)