views:

68

answers:

4

Hi

I'm trying to figure out serialization of .net arrays to XML. Here's a piece of code that I've come up with:

    public class Program
    {
        public class Person 
        {
            public string Firstname { get; set; }
            public string Lastname { get; set; }
            public uint Age { get; set; }
        }

        static void Main ()
        {
            Person[] p = 
            {
                new Person{Age = 20, Firstname = "Michael", Lastname = "Jackson"},
                new Person{Age = 21, Firstname = "Bill", Lastname = "Gates"},
                new Person{Age = 22, Firstname = "Steve", Lastname = "Jobs"}
            };

            SerializeObject<Person[]>(p);
        }

        static void SerializeObject<T>(T obj) where T : class
        {
            string fileName = Guid.NewGuid().ToString().Replace("-", "") + ".xml";
            using (FileStream fs = File.Create(fileName)) 
            {
                XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
                ns.Add("", "");
                XmlSerializer ser = new XmlSerializer(typeof(T));
                ser.Serialize(fs, obj, ns);
            }
        }
    }

Here's an XML content that this example writes down to the XML file:

<ArrayOfPerson>
  <Person>
    <Firstname>Michael</Firstname>
    <Lastname>Jackson</Lastname>
    <Age>20</Age>
  </Person>
  <Person>
    <Firstname>Bill</Firstname>
    <Lastname>Gates</Lastname>
    <Age>21</Age>
  </Person>
  <Person>
    <Firstname>Steve</Firstname>
    <Lastname>Jobs</Lastname>
    <Age>22</Age>
  </Person>
</ArrayOfPerson>

But this is not really what I want. I would like it to look like this:

<Persons>
  <Person>
    <Firstname>Michael</Firstname>
    <Lastname>Jackson</Lastname>
    <Age>20</Age>
  </Person>
  <Person>
    <Firstname>Bill</Firstname>
    <Lastname>Gates</Lastname>
    <Age>21</Age>
  </Person>
  <Person>
    <Firstname>Steve</Firstname>
    <Lastname>Jobs</Lastname>
    <Age>22</Age>
  </Person>
</Persons>

How could I get it working this way? Thanks in advance!

+3  A: 

You need a container class like so:

    /// <summary>
    /// Represents an Person collection.
    /// </summary>
    [Serializable]
    [XmlRoot("Persons", IsNullable = false)]
    public sealed class Persons
    {
        /// <summary>
        /// The person collection.
        /// </summary>
        private Collection<Person> persons;

        /// <summary>
        /// Initializes a new instance of the <see cref="Persons"/> class.
        /// </summary>
        /// <param name="persons">The person list.</param>
        public Persons(Collection<Person> persons)
        {
            this.persons = persons;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Persons"/> class.
        /// </summary>
        /// <param name="persons">The person array.</param>
        public Persons(Person[] persons)
            : this(new Collection<Person>(persons))
        {
        }

        /// <summary>
        /// Prevents a default instance of the <see cref="Persons"/> class from being created.
        /// </summary>
        private Persons()
        {
        }

        /// <summary>
        /// Copies the collection of Person objects to an array and returns
        /// it.
        /// </summary>
        /// <returns>An array of Person objects based on the
        /// collection.</returns>
        public Person[] ToArray()
        {
            Person[] personArray = new Person[this.persons.Count];

            this.persons.CopyTo(personArray, 0);
            return personArray;
        }

        /// <summary>
        /// Gets or sets the persons.
        /// </summary>
        /// <value>The persons.</value>
        [XmlElement("Person")]
        public Collection<Person> ThePersons
        {
            get
            {
                return this.persons;
            }

            set
            {
                this.persons = value;
            }
        }

        /// <summary>
        /// Gets the length of the persons.
        /// </summary>
        /// <value>The length of the persons.</value>
        [XmlIgnore]
        public int Length
        {
            get
            {
                return this.persons.Count;
            }
        }

        /// <summary>
        /// Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>A <see cref="IEnumerator&lt;Person&gt;"/> that can be used to
        /// iterate through the collection.</returns>
        public IEnumerator<Person> GetEnumerator()
        {
            return (IEnumerator<Person>)this.persons.GetEnumerator();
        }
    }

Initialize it with your completed array and return it as per your Main() method:

    static void Main ()
    {
        Person[] p = 
        {
            new Person{Age = 20, Firstname = "Michael", Lastname = "Jackson"},
            new Person{Age = 21, Firstname = "Bill", Lastname = "Gates"},
            new Person{Age = 22, Firstname = "Steve", Lastname = "Jobs"}
        };

        SerializeObject<Persons>(new Persons(p));

        Person[] p2 = DeserializeObject<Persons>("filename.xml").ToArray();
    }

The deserializer method is pretty simple then:

    static T DeserializeObject<T>(string fileName) where T : class
    {
        using (FileStream fs = File.OpenRead(fileName))
        {
            XmlSerializer ser = new XmlSerializer(typeof(T));
            return (T)ser.Deserialize(fs);
        }
    }

Option 2 (building on Nix's answer):

    static void SerializeObject<T>(T obj, Type t) where T : class
    {
        string fileName = Guid.NewGuid().ToString().Replace("-", "") + ".xml";
        using (FileStream fs = File.Create(fileName))
        {
            XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
            ns.Add("", "");
            XmlRootAttribute root = new XmlRootAttribute(t.Name + "s");
            XmlSerializer ser = new XmlSerializer(typeof(T), root);
            ser.Serialize(fs, obj, ns);
        }
    }

can be called as such:

        SerializeObject<Person[]>(p, typeof(Person));
Jesse C. Slicer
+1, good answer (except that you don't need the [Serializable] attribute for XML serialization)
Thomas Levesque
@Thomas: True fact, thanks. I did a copy/paste from a project where we have XML serialization and Remoting going on.
Jesse C. Slicer
Thanks a lot, works great!
the_V
But now the question is... how to get output xml deserialized to Persons object? Any thoughts?
the_V
@the_V: I modded the container class slightly to support deserialization and threw in a quick method to show how to deserialize.
Jesse C. Slicer
Great, thank you very much.
the_V
+1  A: 

You can do it by adding a root attribute to your serializer. See below.

 static void SerializeObject<T>(T obj) where T : class
    {
        string fileName = Guid.NewGuid().ToString().Replace("-", "") + ".xml";
        using (FileStream fs = File.Create(fileName)) 
        {
            XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
            ns.Add("", "");

            XmlRootAttribute root = new XmlRootAttribute( typeof(T).Name + "s");

            XmlSerializer
              ser = new XmlSerializer(typeof(Person[]), root);
              ser.Serialize(fs, obj, ns);
        }
    }

Alternatively you could pass in a func that does the name selecting. Your code would be

 SerializeObject<Person[]>(p, per=>p.GetType().Name);



static void SerializeObject<T>(T obj, Func<T,string> nameSelector) where T : class
{
    string fileName = Guid.NewGuid().ToString().Replace("-", "") + ".xml";
    using (FileStream fs = File.Create(fileName))
    {
        XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
        ns.Add("", "");

        XmlRootAttribute root = new XmlRootAttribute(nameSelector(obj));

        XmlSerializer
          ser = new XmlSerializer(typeof(Person[]), root);
        ser.Serialize(fs, obj, ns);
    }
}
Nix
Your solution now only works for the `Persons` case and not for any other generic `<T>` which is passed into his `SerializeObject` method.
Jesse C. Slicer
You could pass in "ArrayName" parameter. Or you could take the type and add an S if you really wanted to.
Nix
+1  A: 

In addition, this link on controlling serialization through Attributes has always been usefull to me.

http://msdn.microsoft.com/en-us/library/2baksw0z(v=VS.100).aspx

asawyer
It won't help in that case, since you can't add an attribute on the Array type
Thomas Levesque
This is true, but seeing as it was a question on xml serialization I thought I'd share a link that has helped me with it in the past, and it certianly will be helpfull if the_V does what Jesse C Slicer suggests, and split the Person off into a seperate class with properties.
asawyer
+2  A: 

You just need to make some small changes to your code, in addition to the suggestions already provided.

First the SerializeObject generic method needs to be redeclared thus:

// important: declare the input parameter to be an **array** of T, not T.
static void SerializeObject<T>(T[] obj) where T : class
{
    string fileName = Guid.NewGuid().ToString().Replace("-", "") + ".xml";
    using (FileStream fs = File.Create(fileName))
    {
        XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
        ns.Add("", "");

        // override default root node name.  based on your question, 
        // i'm just going to append an "s" to the base type 
        // (e.g., Person becomes Persons)
        var rootName = typeof(T).Name + "s";
        XmlRootAttribute root = new XmlRootAttribute(rootName);

        // add the attribute to the serializer constructor...
        XmlSerializer ser = new XmlSerializer(obj.GetType(), root);

        ser.Serialize(fs, obj, ns);
    }
}

Secondly, in the Main() method, replace SerializeObject<Person[]>(p) with SerializeObject<Person>(p). Thus your Main() method will look like this:

static void Main(string[] args)
{
    Person[] p = 
    {
        new Person{Age = 20, Firstname = "Michael", Lastname = "Jackson"},
        new Person{Age = 21, Firstname = "Bill", Lastname = "Gates"},
        new Person{Age = 22, Firstname = "Steve", Lastname = "Jobs"}
    };

    SerializeObject<Person>(p);
}

The resulting XML will look like this:

<Persons>
  <Person>
    <Firstname>Michael</Firstname>
    <Lastname>Jackson</Lastname>
    <Age>20</Age>
  </Person>
  <Person>
    <Firstname>Bill</Firstname>
    <Lastname>Gates</Lastname>
    <Age>21</Age>
  </Person>
  <Person>
    <Firstname>Steve</Firstname>
    <Lastname>Jobs</Lastname>
    <Age>22</Age>
  </Person>
</Persons>

To override the <Person> element name to something else, set the XmlType attribute on the class, like so:

[XmlType("personEntry")]
public class Person
{
    public string Firstname { get; set; }
    public string Lastname { get; set; }
    public uint Age { get; set; }
}

The resulting XML looks like this:

<Persons>
  <personEntry>
    <Firstname>Michael</Firstname>
    <Lastname>Jackson</Lastname>
    <Age>20</Age>
  </personEntry>
  <personEntry>
    <Firstname>Bill</Firstname>
    <Lastname>Gates</Lastname>
    <Age>21</Age>
  </personEntry>
  <personEntry>
    <Firstname>Steve</Firstname>
    <Lastname>Jobs</Lastname>
    <Age>22</Age>
  </personEntry>
</Persons>
code4life
Nice stuff! Thank you very much!
the_V
Considering this code, could I control name of array child nodes somehow? For example render <personEntry> instead of <Person>...
the_V
@the_V: Yes you can. I've updated my post.
code4life