Serializing to XML via DataContract: custom output?
Asked Answered
G

5

5

I have a custom Fraction class, which I'm using throughout my whole project. It's simple, it consists of a single constructor, accepts two ints and stores them. I'd like to use the DataContractSerializer to serialize my objects used in my project, some of which include Fractions as fields. Ideally, I'd like to be able to serialize such objects like this:

<Object>
    ...
    <Frac>1/2</Frac> // "1/2" would get converted back into a Fraction on deserialization.
    ...
</Object>

As opposed to this:

<Object>
    ...
    <Frac>
        <Numerator>1</Numerator>
        <Denominator>2</Denominator>
    </Frac>
    ...
</Object>

Is there any way to do this using DataContracts?

I'd like to do this because I plan on making the XML files user-editable (I'm using them as input for a music game, and they act as notecharts, essentially), and want to keep the notation as terse as possible for the end user, so they won't need to deal with as many walls of text.

EDIT: I should also note that I currently have my Fraction class as immutable (all fields are readonly), so being able to change the state of an existing Fraction wouldn't be possible. Returning a new Fraction object would be OK, though.

Granados answered 26/10, 2010 at 2:27 Comment(2)
Would you mind explaining why you'd prefer the output in that format? It might generate more pertinent answers, or point you in a direction you hadn't thought of.Horwath
@Horwath Good point, rereading my question I was a bit vague. I'll edit in a bit.Granados
A
6

If you add a property that represents the Frac element and apply the DataMember attribute to it rather than the other properties you will get what you want I believe:

[DataContract]
public class MyObject {
    Int32 _Numerator;
    Int32 _Denominator;
    public MyObject(Int32 numerator, Int32 denominator) {
        _Numerator = numerator;
        _Denominator = denominator;
    }
    public Int32 Numerator {
        get { return _Numerator; }
        set { _Numerator = value; }
    }
    public Int32 Denominator {
        get { return _Denominator; }
        set { _Denominator = value; }
    }
    [DataMember(Name="Frac")]
    public String Fraction {
        get { return _Numerator + "/" + _Denominator; }
        set {
            String[] parts = value.Split(new char[] { '/' });
            _Numerator = Int32.Parse(parts[0]);
            _Denominator = Int32.Parse(parts[1]);
        }
    }
}
Altissimo answered 26/10, 2010 at 3:10 Comment(1)
Unfortunately, Numerator and Denominator are readonly, so I can't assign to them once the instance has been created.Granados
D
5

DataContractSerializer will use a custom IXmlSerializable if it is provided in place of a DataContractAttribute. This will allow you to customize the XML formatting in anyway you need... but you will have to hand code the serialization and deserialization process for your class.

public class Fraction: IXmlSerializable 
{
    private Fraction()
    {
    }
    public Fraction(int numerator, int denominator)
    {
        this.Numerator = numerator;
        this.Denominator = denominator;
    }
    public int Numerator { get; private set; }
    public int Denominator { get; private set; }

    public XmlSchema GetSchema()
    {
        throw new NotImplementedException();
    }

    public void ReadXml(XmlReader reader)
    {
        var content = reader.ReadInnerXml();
        var parts = content.Split('/');
        Numerator = int.Parse(parts[0]);
        Denominator = int.Parse(parts[1]);
    }

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteRaw(this.ToString());
    }

    public override string ToString()
    {
        return string.Format("{0}/{1}", Numerator, Denominator);
    }
}
[DataContract(Name = "Object", Namespace="")]
public class MyObject
{
    [DataMember]
    public Fraction Frac { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var myobject = new MyObject
        {
            Frac = new Fraction(1, 2)
        };

        var dcs = new DataContractSerializer(typeof(MyObject));

        string xml = null;
        using (var ms = new MemoryStream())
        {
            dcs.WriteObject(ms, myobject);
            xml = Encoding.UTF8.GetString(ms.ToArray());
            Console.WriteLine(xml);
            // <Object><Frac>1/2</Frac></Object>
        }

        using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(xml)))
        {
            ms.Position = 0;
            var obj = dcs.ReadObject(ms) as MyObject;

            Console.WriteLine(obj.Frac);
            // 1/2
        }
    }
}
Decipher answered 26/10, 2010 at 4:21 Comment(2)
Like the other answers, this would have been perfect if it weren't for the fact that Numerator and Denominator are readonly.Granados
If you won't give up on the readonly fields than you could move the logic to create the instance of the Fraction object to the level of MyObjectDecipher
T
3

This MSDN article describes IDataContractSurrogate Interface which:

Provides the methods needed to substitute one type for another by the DataContractSerializer during serialization, deserialization, and export and import of XML schema documents.

Although way too late, still may help someone. Actually, allows to change XML for ANY class.

Towny answered 26/4, 2013 at 11:6 Comment(0)
S
1

You can do this with the DataContractSerializer, albeit in a way that feels hacky to me. You can take advantage of the fact that data members can be private variables, and use a private string as your serialized member. The data contract serializer will also execute methods at certain points in the process that are marked with [On(De)Serializ(ed|ing)] attributes - inside of those, you can control how the int fields are mapped to the string, and vice-versa. The downside is that you lose the automatic serialization magic of the DataContractSerializer on your class, and now have more logic to maintain.

Anyways, here's what I would do:

[DataContract]
public class Fraction
{
    [DataMember(Name = "Frac")]
    private string serialized;

    public int Numerator { get; private set; }
    public int Denominator { get; private set; }

    [OnSerializing]
    public void OnSerializing(StreamingContext context)
    {
        // This gets called just before the DataContractSerializer begins.
        serialized = Numerator.ToString() + "/" + Denominator.ToString();
    }

    [OnDeserialized]
    public void OnDeserialized(StreamingContext context)
    {
        // This gets called after the DataContractSerializer finishes its work
        var nums = serialized.Split("/");
        Numerator = int.Parse(nums[0]);
        Denominator = int.Parse(nums[1]);
    }
}
Spermogonium answered 27/10, 2010 at 20:22 Comment(5)
This would have been awesome, but my Fraction class needs to be immutable, so the Numerator and Denominator only have get acessors, and the backing fields are readonly.Granados
I would think that this solution would still work for you - the fields are immutable, inasmuch as they do not expose any public mutators and the fields are only written to once - during deserialization. Will this not meet your needs?Spermogonium
I don't think it's the case, from what I've tried (unless I'm doing something horribly wrong). When I tried setting my numerator and denominator fields in the OnDeserialized method earlier out of curiosity, VS yelled at me for trying to set readonly fields outside of the constructor.Granados
Understandable. The solution I've provided guarantees immutability, but not through fields marked readonly. Instead, the guarantee comes from the fact that no public mutators are exposed. The key here is that if you use automatic properties with a private set accessor instead of a readonly field, this example will work for you.Spermogonium
Sorry to comment on old post. But I believe this code will be serialized into <Object>...<Fraction><Frac>1/2</Frac></Fraction>..</Object> instead of <Object>...<Frac>1/2</Frac> ...</Object> as the OP intended.Brie
I
0

You'll have to switch back to the XMLSerializer to do that. The DataContractSerializer is a bit more restrictive in terms of being able to customise the output.

Inhumation answered 26/10, 2010 at 2:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.