Welcome to CrankyGoblin.Com Sign in | Join | Help

Public Class GeoffAppleby

Inherits Microsoft.VisualBasic.MVP : Implements IBrainFart
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)
Posted: Saturday, November 13, 2004 3:38 PM by Geoff Appleby

Comments

Fari said:

Not the most technical way but wonderfully easy and it works perfect.
I myself prefer the IXMLSerializable way and the fact that this interface is finally documented on .Net2 tempts me more. however this approach is far easier to implement specially for the classes exposed through WS, you dont need to infer custom built xml schema any more.

Thanks
# January 31, 2005 6:30 AM
Leave a Comment

(required) 

(required) 

(optional)

(required) 

To submit your comment, click on these pictures:
  • Geoff the big mouth
  • Geoff has an idea
  • Sleepy Geoff
Gaptcha Image - No Peeking! Gaptcha Image - No Peeking! Gaptcha Image - No Peeking!
Gaptcha Image - No Peeking! Gaptcha Image - No Peeking! Gaptcha Image - No Peeking!
Gaptcha Image - No Peeking! Gaptcha Image - No Peeking! Gaptcha Image - No Peeking!
Can't recognise the people in these pictures? Look here for a quick introduction.
There's a time limit for you to get your comment submitted before this set of pictures expires. If you think it's been longer than 10 minutes, get some new pictures first (you won't lose what you've typed so far).
Get some new pictures 

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS