views:

57

answers:

4

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.

A: 

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.

Cam
+2  A: 

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]);
        }
    }
}
Steve Ellinger
Unfortunately, Numerator and Denominator are readonly, so I can't assign to them once the instance has been created.
Mark LeMoine
+1  A: 

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
        }
    }
}
Matthew Whited
Like the other answers, this would have been perfect if it weren't for the fact that Numerator and Denominator are readonly.
Mark LeMoine
That's easily fixed... see my update
Matthew Whited
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 `MyObject`
Matthew Whited
A: 

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]);
    }
}
Ben
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.
Mark LeMoine
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?
Ben
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.
Mark LeMoine
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.
Ben