The easiest Serializable generic Class

Looking around the NETMF libs, I found very difficult to get a compatible serialization mechanism that let transfer simple class objects to/from .NET regular framework.
So I decided to make a very simple serializer/deserializer for .NET / .NETMF that is compatible each other.
I didn’t want to use DPWS and all the elephant libs and code complexity.
I use for TCP/IP socket and it’s fine. May be solve the problem to someone else.



// This is NETMF Base class
using System;
using Microsoft.SPOT;
using System.Text;
using System.Reflection;
using System.IO;
using System.Xml;
using System.Ext.Xml;
using System.Runtime.InteropServices;

namespace SerializableClass
{
    public class SerializableClassBase
    {
        public object _this_instance;

        public SerializableClassBase()
        {
            _this_instance = DateTime.Now.Ticks;
        }

        public SerializableClassBase(object instance)
        {
            _this_instance = instance;
        }

        public string Serialize()
        {
            StringBuilder sb = new StringBuilder();
            MemoryStream ms = new MemoryStream();
            XmlWriter xml = XmlWriter.Create(ms);            
            Type t = this.GetType();
            
            xml.WriteRaw("<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n");
            xml.WriteStartElement("message");
            xml.WriteStartElement(t.Name);
            foreach (FieldInfo f in t.GetFields())
            {
                if (f.GetValue(this) != null)
                {
                    if (f.FieldType.Name != "DateTime")
                        xml.WriteElementString(f.Name, f.GetValue(this).ToString());
                    else
                    {
                        long ticks = ((DateTime)f.GetValue(this)).Ticks;
                        xml.WriteElementString(f.Name, ticks.ToString());
                    }
                }
            }
            xml.WriteEndElement(); // </"t.name">
            xml.WriteEndElement(); // </message>
            //byte[] buff = ms.ToArray();
            //char[] cc = Encoding.UTF8.GetChars(ms.ToArray());
            sb.Append(Encoding.UTF8.GetChars(ms.ToArray()));
            return sb.ToString();
        }

        public int Deserialize(string xmls)
        {
            int ret = 0;
            Type t = this.GetType();
            MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(xmls));
            try
            {
                XmlReader rdr = XmlReader.Create(ms);
                //rdr.MoveToContent();
                string tmp_value = "";
                while (rdr.Read())
                {
                    if (rdr.NodeType == XmlNodeType.Element)
                    {
                        foreach (FieldInfo f in t.GetFields())
                        {
                            if (rdr.Name == f.Name)
                            {
                                tmp_value = rdr.ReadElementString(f.Name);
                                if (tmp_value != null)
                                {
                                    switch (f.FieldType.Name)
                                    {
                                        case "String":
                                            f.SetValue(this, tmp_value);
                                            break;
                                        case "Single":
                                            f.SetValue(this, (Single) Double.Parse(tmp_value));
                                            break;
                                        case "Double":
                                            f.SetValue(this, Double.Parse(tmp_value));
                                            break;
                                        case "Int":
                                        case "Int32":
                                            f.SetValue(this, Int32.Parse(tmp_value));
                                            break;
                                        case "UInt":
                                        case "UInt32":
                                            f.SetValue(this, UInt32.Parse(tmp_value));
                                            break;
                                        case "Int16":
                                            f.SetValue(this, Int16.Parse(tmp_value));
                                            break;
                                        case "UInt16":
                                            f.SetValue(this, UInt16.Parse(tmp_value));
                                            break;
                                        case "Byte":
                                            f.SetValue(this, Byte.Parse(tmp_value));
                                            break;                                             
                                        case "Char":
                                            f.SetValue(this, System.Convert.ToChar(tmp_value[0]));
                                            break;
                                        case "DateTime":
                                            f.SetValue(this, new DateTime(long.Parse(tmp_value)));
                                            break;
                                        default:
                                            break;
                                    }
                                }
                            }
                            ret++; // just count members worked
                        }
                    }
                    if (rdr.EOF)
                        break;
                }
            }        
            catch (XmlException ex)
            {
                string msg = ex.Message; // manage error : I do nothing.
            }
            ms.Close();
            return ret;
        }        
    }
}

This is just a base class for derive your class where to declare members …


    public class MySerializebleClass: SerializableClassBase
    {
        public string name;
        public string Description;
        public int Value;
        public Single TestFloat;
        public DateTime Ticks;
    }
...
// here is an example of how to use it.
               MySerializebleClass myserclass;

                myserclass = new MySerializebleClass() { name = "MyClassName", Description = "Test a String data", 
                                                        TestFloat = 150.25F, Value = 42, Ticks = DateTime.Now  };
                string serialized = myserclass.Serialize();
// To deserialize the message on the other end:
                MySerializebleClass myserclass2;                
                myserclass2.Deserialize(serialized);

////////////////////
The content of var "serialized" will be:
<?xml version="1.0" encoding="utf-8"?>
<message>
	<MySerializebleClass>
		<name>MyClassName</name>
		<Description>Test a String data</Description>
		<Value>42</Value>
		<TestFloat>150.25</TestFloat>
		<Ticks>129852926764217872</Ticks>
		<_this_instance>129852926764217872</_this_instance>
	</MySerializebleClass>
</message>
///////////////

So the message is simply a plain XML that can be sent via TCP/IP, XBee com, UART and so on. The only problem is DateTime vars that need to be sent in ticks format to deserialize on .netmf



// regular .NET Framework
using System;
using System.Text;
using System.Reflection;
using System.IO;
using System.Xml;
using System.Runtime.InteropServices;
....

Changing just this “using” statement, you can use the class on the .NET environment.

Fill free to make it better … When it will be well tested I’ll set in codeshare

NOTE : this works for .NETMF 4.2 ( due to StringBuilder)

1 Like

This looks nice and will simplify transfering data between systems. I wonder if you can tweak this enough so that you can fully interoping between NETMF and .NET. I think the serialized xml would need to look more like this instead:


<?xml version="1.0"?>
<MySerializebleClass xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <name>asdf</name>
  <Description>asdf</Description>
  <Value>23</Value>
  <TestFloat>2.5</TestFloat>
  <Ticks>2012-06-27T17:00:11.8725272-07:00</Ticks>
</MySerializebleClass>

So remove the and add the namespaces to the node.

Some general feedback:

Anything that implements IDisposable (MemoryStream, XmlWriter, etc) should use a using statement (using statement - C# Reference | Microsoft Learn) to ensure it is properly disposed. An example would be to change your Serialize method to:


        public string Serialize()
        {
            StringBuilder sb = new StringBuilder();
            using (MemoryStream ms = new MemoryStream())
            {
                using (XmlWriter xml = XmlWriter.Create(ms))
                {
                    Type t = this.GetType();

                    xml.WriteRaw("<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n");
                    xml.WriteStartElement("message");
                    xml.WriteStartElement(t.Name);
                    foreach (FieldInfo f in t.GetFields())
                    {
                        if (f.GetValue(this) != null)
                        {
                            if (f.FieldType.Name != "DateTime")
                                xml.WriteElementString(f.Name, f.GetValue(this).ToString());
                            else
                            {
                                long ticks = ((DateTime)f.GetValue(this)).Ticks;
                                xml.WriteElementString(f.Name, ticks.ToString());
                            }
                        }
                    }
                    xml.WriteEndElement(); // </"t.name">
                    xml.WriteEndElement(); // </message>
                    //byte[] buff = ms.ToArray();
                    //char[] cc = Encoding.UTF8.GetChars(ms.ToArray());
                    sb.Append(Encoding.UTF8.GetChars(ms.ToArray()));
                }
            }

            return sb.ToString();
        }

Since this is meant to be a base class you should use the abstract modifier (abstract - C# Reference | Microsoft Learn) to ensure it doesn’t get instantiated.

Though perhaps this class should be rewriten as a static class with static methods. This way is it not part of every class that you want to serialize (size). You can follow the implementation model of System.Xml.Serialization.XmlSerializer (XmlSerializer Class (System.Xml.Serialization) | Microsoft Learn) in .NET.

Very handy indeed. Please make sure this ends up on the codeshare.

@ JREndean - thank you for feedback.
Good catch. I will do abstract.
Aahah I really went to fast and forgot “using”.

Nice! Thanks! Definitely put on CodeShare so it doesn’t get lost.

I modified your code to make a new class that can be used just like System.Xml.Serialization.XmlSerializer. With this the same code can be used on NETMF/Gadgeteer and desktop .NET. It is not fully complete, nor does it correctly verify input or handle errors, but you are welcome to it or you can just adapt it back to your base class.


using System;
using System.Ext.Xml;
using System.IO;
using System.Reflection;
using System.Text;
using System.Xml;

namespace System.Xml.Serialization
{
    public class XmlSerializer
    {
        private readonly Type typeToSerialize;

        public XmlSerializer(Type type)
        {
            this.typeToSerialize = type;
        }

        public void Serialize(Stream stream, object instance)
        {
            using (XmlWriter xmlWriter = XmlWriter.Create(stream))
            {
                xmlWriter.WriteRaw("<?xml version=\"1.0\"?>");
                xmlWriter.WriteStartElement(this.typeToSerialize.Name);

                foreach (FieldInfo fieldInfo in this.typeToSerialize.GetFields())
                {
                    if (fieldInfo.FieldType.IsEnum)
                    {
                        // TODO:
                        continue;
                    }

                    if (fieldInfo.GetValue(instance) != null)
                    {
                        switch (fieldInfo.FieldType.Name)
                        {
                            case "Boolean":
                                // needs to be lower case T and F
                                xmlWriter.WriteElementString(fieldInfo.Name, fieldInfo.GetValue(instance).ToString().ToLower());
                                break;

                            case "Char":
                                xmlWriter.WriteElementString(fieldInfo.Name, Encoding.UTF8.GetBytes(fieldInfo.GetValue(instance).ToString())[0].ToString());
                                break;

                            case "DateTime":
                                // should be ToString("o") but that is not supported contrary to documentation
                                xmlWriter.WriteElementString(fieldInfo.Name, ((DateTime)fieldInfo.GetValue(instance)).ToString("s"));
                                break;

                            default:
                                xmlWriter.WriteElementString(fieldInfo.Name, fieldInfo.GetValue(instance).ToString());
                                break;
                        }
                    }
                }

                xmlWriter.WriteEndElement();
            }
        }

        public object Deserialize(Stream stream)
        {
            object instance = this.typeToSerialize.GetConstructor(new Type[0]).Invoke(null);
            XmlReader xmlReader = XmlReader.Create(stream);

            while (xmlReader.Read())
            {
                if (xmlReader.NodeType == XmlNodeType.Element)
                {
                    var fields = this.typeToSerialize.GetFields();

                    foreach (FieldInfo fieldInfo in this.typeToSerialize.GetFields())
                    {
                        if (xmlReader.Name == fieldInfo.Name)
                        {
                            string tempValue = xmlReader.ReadElementString(fieldInfo.Name);

                            if (fieldInfo.FieldType.IsEnum)
                            {
                                // TODO:
                                continue;
                            }

                            switch (fieldInfo.FieldType.Name)
                            {
                                case "Boolean":
                                    fieldInfo.SetValue(instance, tempValue == "true");
                                    break;

                                case "Byte":
                                    fieldInfo.SetValue(instance, Convert.ToByte(tempValue));
                                    break;
                                case "SByte":
                                    fieldInfo.SetValue(instance, Convert.ToSByte(tempValue));
                                    break;
                                
                                case "Char":
                                    fieldInfo.SetValue(instance, Convert.ToChar(Convert.ToByte(tempValue)));
                                    break;

                                case "DateTime":
                                    fieldInfo.SetValue(instance, GetDateTimeFromString(tempValue));
                                    break;

                                case "Double":
                                    fieldInfo.SetValue(instance, Convert.ToDouble(tempValue));
                                    break;

                                case "Guid":
                                    fieldInfo.SetValue(instance, GetGuidFromString(tempValue));
                                    break;

                                case "Int16":
                                    fieldInfo.SetValue(instance, Convert.ToInt16(tempValue));
                                    break;
                                case "UInt16":
                                    fieldInfo.SetValue(instance, Convert.ToUInt16(tempValue));
                                    break;

                                case "Int32":
                                    fieldInfo.SetValue(instance, Convert.ToInt32(tempValue));
                                    break;
                                case "UInt32":
                                    fieldInfo.SetValue(instance, Convert.ToUInt32(tempValue));
                                    break;

                                case "Int64":
                                    fieldInfo.SetValue(instance, Convert.ToInt64(tempValue));
                                    break;
                                case "UInt64":
                                    fieldInfo.SetValue(instance, Convert.ToUInt64(tempValue));
                                    break;

                                case "Object":
                                    // TODO:
                                    break;

                                case "Single":
                                    fieldInfo.SetValue(instance, (Single)Convert.ToDouble(tempValue));
                                    break;

                                case "String":
                                    fieldInfo.SetValue(instance, tempValue);
                                    break;

                                default:
                                    break;
                            }

                            continue;
                        }
                    }
                }
            }

            return instance;
        }

        private object GetGuidFromString(string tempValue)
        {
            byte[] array = new byte[16];
            
            // TODO:
            //foreach (string section in tempValue.Split('-'))
            //{
            //    for (int i = 0; i < section.Length; i+=2)
            //    {
            //        array[i / 2] = Convert.ToByte(section.Substring(i, 2));
            //    }
            //}

            return new Guid(array);
        }

        private DateTime GetDateTimeFromString(string tempValue)
        {
            int year;
            int month;
            int day;
            int hour;
            int minute;
            int second;
            int millisecond;

            year = int.Parse(tempValue.Substring(0, 4));
            month = int.Parse(tempValue.Substring(5, 2));
            day = int.Parse(tempValue.Substring(8, 2));
            hour = int.Parse(tempValue.Substring(11, 2));
            minute = int.Parse(tempValue.Substring(14, 2));
            second = int.Parse(tempValue.Substring(17, 2));
            // NETMF serializes out to this
            //2012-06-27T20:02:40
            // .NET does this
            //2012-06-27T20:36:57.995-07:00
            //                     ^ ^^^^^^
            //                     |     |
            //           milliseconds   timezone (-7 from GMT) (sometimes)
            //millisecond = tempValue.Length == 19 ? 0 : int.Parse(tempValue.Substring(20, 2));
            millisecond = 0;

            return new DateTime(year, month, day, hour, minute, second, millisecond);
        }
    }

Here is a sample class to test the serialization:


    public class TestSerializableClass
    {
        public enum MyEnum
        {
            One,
            Two,
            Three,
        }

        public bool Bool;
        public Boolean Boolean;

        public byte ByteL;
        public Byte ByteU;
        public SByte SByte;

        public char CharL;
        public char CharU;

        public DateTime DateTime;

        public decimal DecimalL;
        public Decimal DecimalU;

        public double DoubleL;
        public Double DoubleU;

        public MyEnum Enum;

        public Guid Guid;

        public short Short;
        public Int16 Int16;
        public ushort Ushort;
        public UInt16 UInt16;

        public int Int;
        public Int32 Int32;
        public uint Uint;
        public UInt32 UInt32;

        public long Long;
        public Int64 Int64;
        public ulong Ulong;
        public UInt64 UInt64;

        public object ObjectL;
        public object ObjectU;

        public Single Single;
        public float Float;

        public string StringL;
        public String StringU;
    }

Here is the sample code to call it. This code works in NETMF and .NET and you should be able to serialize in one and use the string to deserialize in another


   TestSerializableClass temp = 
        new TestSerializableClass()
        {
            Bool = true,
            Boolean = false,

            ByteL = 128,
            ByteU = 254,
            SByte = (SByte)(-128),

            CharL = 'l',
            CharU = 'U',

            DateTime = new DateTime(2012, 06, 27, 13, 30, 55, 995),

            DecimalL = 123.45M,
            DecimalU = 54.321M,

            DoubleL = 123.4,
            DoubleU = 43.211,

            Enum = TestSerializableClass.MyEnum.Two,

            Guid = Guid.NewGuid(),

            Short = 1,
            Int16 = 1,
            Ushort = 1,
            UInt16 = 1,

            Int = 1,
            Int32 = 1,
            Uint = 1,
            UInt32 = 1,

            Long = 1,
            Int64 = 1,
            Ulong = 1,
            UInt64 = 1,

            ObjectL = new string('a', 5),
            ObjectU = new object(),

            Single = 1,
            Float = 1,

            StringL = "String L",
            StringU = "String U",
        };

    // test serialization
    StringBuilder sb = new StringBuilder();
    XmlSerializer xs = new XmlSerializer(typeof(TestSerializableClass));
    using (MemoryStream ms = new MemoryStream())
    {
        xs.Serialize(ms, temp);
        sb.Append(Encoding.UTF8.GetChars(ms.ToArray()));
    }
    // check that it serialized
    string serialized = sb.ToString();

    // test deserialization
    TestSerializableClass de = null;
    using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(serialized)))
    {
        de = (TestSerializableClass)xs.Deserialize(ms);
    }
    // check that it deserialized
    char t = de.CharU;

@ JREndean

:slight_smile:

Thanks again … You are going very far … your suggestion and solution does make really sense.
Super-nice suggestion the XmlSerializer as a clone of .NET framework … This can be a solution that avoid to maintain 2 libs !!
(At the end of the day, why not put XmlSerializer in NETMF 4.3 ?)

I’m having some issue with single & double type, due to .NET (desktop) international settings ("," instead of “.”) … so I need to work on this also.

ADDENDUM: XmlSerializer works great. On the PC side there’s an issue with the MemoryStream:


// Need to reset to position 0 the stream before calling Deserialize()
...
                        ms.Write(bytes, 0, bytesRec);

                        ms.Position = 0;  // set position to begin of stream!
                        mysertest = (MySerializebleClass) xs.Deserialize(ms);
...


Minor issue: IIRC, FieldInfo.GetValue is pretty expensive to call - you might want to call that just once in the Serialize loop.

Would this also work with Hashtables and Arraylists?

I know the full .NET serializer has issues with some types of collections.

Good point! We shuold get it the one time as an object then use that for the comparison to null and when writing the values to XML.
Ugh, I also left in a line of temp code that needs to be removed from Deserialize(). It was getting late :wink:


var fields = this.typeToSerialize.GetFields();

I think we can ask for it to be added on the Codeplex site

Yea, I didn’t even get a chance to think about globalization. Hopefully the CultureInfo class will help. If not then we will have to potential get real messy with string parsing.

It should be realatively straight forward to get this to work with arrays (aka. string, int), but I am unsure about other collection types. We will have to explicitly check for those types and write the code to serailize and deserialze them since the NETMF version does not implement ISerailizable.

Gotcha … I guess the decision would need to be made if you want that extra overhead/bloat in a NETMF system.

Thanks for getting this together! I could see where this would be helpful for a system that would pass data (sensor, location, etc …) from a device to a full NET app.

Guys, don’t forget that target is a small device, and my target was an “easy” and light helper to pass info to and from pc. If you go too further than dpws may be used instead.
XmlSerializer on .net can’t work on array list members if I remind correctly.

I think we can serialize primitive arrays pretty easily and might want to limit it to that. In my example we should remove Object serialization as well.

I also remembered what cannot be serialized (without custom code) and it is anything that implements IDictionary, so Hash tables are out.

[quote=“JREndean”]I also remembered what cannot be serialized (without custom code) and it is anything that implements IDictionary, so Hash tables are out.
[/quote]
Having virtual methods in the base class that get called if the serializer/deserializer doesn’t know the type might be a good idea. That’d keep the base simple and give people who want to do more complex types an easy in…

Good idea. Alternatively, events like desktop .NET does (UnknownElement, etc).

I now have it serializing arrays properly with the exception of byte[] since the .NET serializer converts byte[] to base64 encoded string so I will have to modify the logic for that. I also want to try to get enums to serialize properly then there is just general cleanup/refactoring and error handling.

dobova - did you get a chance to fix the culture formating problem?


    public class TestSerializableClass
    {
        ...
        public bool[] ArrayBool;
        public byte[] ArrayByte;
        public sbyte[] ArraySByte;
        public char[] ArrayChar;
        public DateTime[] ArrayDateTime;
        // NETMF does not support these
        //public decimal[] ArrayDecimal;
        public double[] ArrayDouble;
        public Guid[] ArrayGuid;
        public short[] ArrayShort;
        public ushort[] ArrayUShort;
        public int[] ArrayInt;
        public uint[] ArrayUInt;
        public long[] ArrayLong;
        public ulong[] ArrayULong;
        public float[] ArrayFloat;
        public string[] ArrayString;
    }

Sorry to jump in here out of the blue, I just read this chain and noticed you guys talking about localization etc. I am sure you are aware, but XML does not suffer from localization issues, floating point types and dates etc. should be stored in XML using the XML standard regardless of the current culture.

On the desktop side there is a very nice and convenient yet under used class called XmlConvert, it has methods which convert data to the XML standard string format. This class is also used in the desktop XmlSerializer class. Of course you do not have this convenient function in NETMF, but the one good thing is that you can assume the standard format and directly serialize/deserialize according to that.

Double/Single - No thousands separator always a ‘.’ as the decimal separator (I suspect that this is what .NETMF is doing anyway)
DateTime - yyyy-MM-ddTHH:mm:ss

For more details on dates without getting into the ISO standard, take a look here
http://www.w3schools.com/schema/schema_dtypes_date.asp

Hope this helps in some way…

@ taylorza and @ JREndean: XmlSerializer on .NET side doesn’t suffer of culturinfo problem. I had problems with my class code on .NET.

@ JREndean:
GUID passing is really a mess from .NET to .NETMF becouse the .NETMF constructor expect 3 sections reverted and the last 2 straight also with an array. I’ll get into it.
the GUID constractor is:


new Guid (UInt32, UInt16, UInt16, Byte, Byte, Byte, Byte, Byte, Byte, Byte, Byte)

So the array need to correctly filled with bytes.array o… 4, 5…6, 7 … 8 are a UInt representation, instead from 9 … 16 are straight.