rotman
rotman

Reputation: 1651

Problem with serializing a dictionary wrapper

I defined two classes. First one...

[Serializable]
public class LocalizationEntry
{
    public LocalizationEntry()
    {
        this.CatalogName = string.Empty;
        this.Identifier = string.Empty;
        this.Translation = new Dictionary<string, string>();
        this.TranslationsList = new List<Translation>();
    }

    public string CatalogName
    {
        get;
        set;
    }

    public string Identifier
    {
        get;
        set;
    }

    [XmlIgnore]
    public Dictionary<string, string> Translation
    {
        get;
        set;
    }

    [XmlArray(ElementName = "Translations")]
    public List<Translation> TranslationsList
    {
        get
        {
            var list = new List<Translation>();

            foreach (var item in this.Translation)
            {
                list.Add(new Translation(item.Key, item.Value));
            }

            return list;
        }
        set
        {
            foreach (var item in value)
            {
                this.Translation.Add(item.Language, item.Text);
            }
        }
    }
}

...where public List<Translation> TranslationsList is a wrapper for non-serializable public Dictionary<string, string> Translation.

Pair of key and value is defined as follows:

[Serializable]
public class Translation
{
    [XmlAttribute(AttributeName = "lang")]
    public string Language
    {
        get;
        set;
    }

    [XmlText]
    public string Text
    {
        get;
        set;
    }

    public Translation()
    {

    }

    public Translation(string language, string translation)
    {
        this.Language = language;
        this.Text = translation;
    }
}

At last code used to serialize:

static void Main(string[] args)
{
    LocalizationEntry entry = new LocalizationEntry()
    {
        CatalogName = "Catalog",
        Identifier = "Id",
    };

    entry.Translation.Add("PL", "jabłko");
    entry.Translation.Add("EN", "apple");
    entry.Translation.Add("DE", "apfel");

    using (FileStream stream = File.Open(@"C:\entry.xml", FileMode.Create))
    {
        XmlSerializer serializer = new XmlSerializer(typeof(LocalizationEntry));
        serializer.Serialize(stream, entry);
    }

    LocalizationEntry deserializedEntry;
    using (FileStream stream = File.Open(@"C:\entry.xml", FileMode.Open))
    {
        XmlSerializer serializer = new XmlSerializer(typeof(LocalizationEntry));
        deserializedEntry = (LocalizationEntry)serializer.Deserialize(stream);
    }
}

The problem is that after deserialization deserializedEntry.TranslationsList is empty. I set a breakpoint at setter of LocalizationEntry.TransalionsList and it comes from deserializer empty as well. Product of serialization is of course valid. Is there any gap in my code?

EDIT:

Here is generated XML:

<?xml version="1.0"?>
<LocalizationEntry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <CatalogName>Catalog</CatalogName>
  <Identifier>Id</Identifier>
  <Translations>
    <Translation lang="PL">jabłko</Translation>
    <Translation lang="EN">apple</Translation>
    <Translation lang="DE">apfel</Translation>
  </Translations>
</LocalizationEntry>

Upvotes: 2

Views: 631

Answers (3)

J. Tihon
J. Tihon

Reputation: 4459

I've created a sample, which will allow you to avoid the unnecessary hidden property when using the XmlSerializer:

class Program
{
    static void Main(string[] args)
    {
        LocalizationEntry entry = new LocalizationEntry()
        {
            CatalogName = "Catalog",
            Identifier = "Id",
            Translations =
            {
                { "PL", "jabłko" },
                { "EN", "apple" },
                { "DE", "apfel" }
            }
        };

        using (MemoryStream stream = new MemoryStream())
        {
            XmlSerializer serializer = new XmlSerializer(typeof(LocalizationEntry));
            serializer.Serialize(stream, entry);

            stream.Seek(0, SeekOrigin.Begin);
            LocalizationEntry deserializedEntry = (LocalizationEntry)serializer.Deserialize(stream);
            serializer.Serialize(Console.Out, deserializedEntry);
        }
    }
}

public class LocalizationEntry
{
    public LocalizationEntry() { this.Translations = new TranslationCollection(); }
    public string CatalogName { get; set; }
    public string Identifier { get; set; }

    [XmlArrayItem]
    public TranslationCollection Translations { get; private set; }
}

public class TranslationCollection
    : Collection<Translation>
{
    public TranslationCollection(params Translation[] items)
    {
        if (null != items)
        {
            foreach (Translation item in items)
            {
                this.Add(item);
            }
        }
    }

    public void Add(string language, string text)
    {
        this.Add(new Translation
        {
            Language = language,
            Text = text
        });
    }
}

public class Translation
{
    [XmlAttribute(AttributeName = "lang")]
    public string Language { get; set; }

    [XmlText]
    public string Text { get; set; }
}

There are some drawbacks when working with the XmlSerializer class itself. The .NET guidelines encourage you the not provide public-setters for collection-properties (like your translation list). But when you look at the code generated by the XmlSerializer, you'll see that it will use the Setter regardless of it is accessible. This results in a compile-error when the interim class is dynamically loaded by the XmlSerializer. The only way to avoid this, is to make the XmlSerializer think, that it can't actually create an instance of the list and thus won't try to call set for it. If the XmlSerializer detects that it can't create an instance it will throw an exception instead of using the Setter and the interim class is compiled successfully. I've used the param-keyword to trick the serializer into thinking that there is no default-constructor.

The only drawback from this solution is that you have to use a non-generic, non-interface type for the property (TranslationCollection) in my example.

Upvotes: 0

d4nt
d4nt

Reputation: 15769

The problem is that your TranslationList property is not being set by the Xml Deserializer. The set method will be hit but only by the call to this.TranslationsList = new List(); in the LocalisationEntry constructor. I'm not yet sure why but I suspect it's because it doesn't know how to convert an array of Translation objects back into a List.

I added the following code and it worked fine:

[XmlArray(ElementName = "Translations")]
public Translation[] TranslationArray
{
    get
    {
        return TranslationsList.ToArray();
    }

    set
    {
        TranslationsList = new List<Translation>(value);
    }
}

[XmlIgnore]
public List<Translation> TranslationsList
....

Upvotes: 2

Jamie Treworgy
Jamie Treworgy

Reputation: 24334

I am guessing the problem has to do with this:

public List<Translation> TranslationsList

The get/set operators are designed only for something to get or assign a fully-formed list. If you tried to use this in your own code, for example, every time you would do something like

TranslationsList.Add(item)

It would just create a new list from the existing dictionary and not actually deal with your item. I bet the deserializer works much the same way: uses set to create the new object once, then uses get as it adds each item from the XML. Since all that happens in get is it copies from the dictionary (which is empty when you begin your deserialization) you end up with nothing.

Try replacing this with just a field:

public List<Translation> TranslationsList;

and then explicitly call the code to copy the dictionary to this list before you serialize, and copy it from this list to the dictionary after you deserialize. Assuming that works, you can probably figure out a more seamless way to implement what you're trying to do.

Upvotes: 1

Related Questions