Maneira correta de implementar IXmlSerializable?

153

Depois que um programador decide implementar IXmlSerializable, quais são as regras e práticas recomendadas para implementá-lo? Ouvi dizer que GetSchema()deve retornar nulle ReadXmldeve passar para o próximo elemento antes de retornar. Isso é verdade? E o que dizer WriteXml- ele deve escrever um elemento raiz para o objeto ou presume-se que a raiz já esteja gravada? Como os objetos filhos devem ser tratados e gravados?

Aqui está uma amostra do que tenho agora. Vou atualizá-lo à medida que obtiver boas respostas.

public class MyCalendar : IXmlSerializable
{
    private string _name;
    private bool _enabled;
    private Color _color;
    private List<MyEvent> _events = new List<MyEvent>();


    public XmlSchema GetSchema() { return null; }

    public void ReadXml(XmlReader reader)
    {
        if (reader.MoveToContent() == XmlNodeType.Element && reader.LocalName == "MyCalendar")
        {
            _name    = reader["Name"];
            _enabled = Boolean.Parse(reader["Enabled"]);
            _color   = Color.FromArgb(Int32.Parse(reader["Color"]));

            if (reader.ReadToDescendant("MyEvent"))
            {
                while (reader.MoveToContent() == XmlNodeType.Element && reader.LocalName == "MyEvent")
                {
                    MyEvent evt = new MyEvent();
                    evt.ReadXml(reader);
                    _events.Add(evt);
                }
            }
            reader.Read();
        }
    }

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteAttributeString("Name",    _name);
        writer.WriteAttributeString("Enabled", _enabled.ToString());
        writer.WriteAttributeString("Color",   _color.ToArgb().ToString());

        foreach (MyEvent evt in _events)
        {
            writer.WriteStartElement("MyEvent");
            evt.WriteXml(writer);
            writer.WriteEndElement();
        }
    }
}

public class MyEvent : IXmlSerializable
{
    private string _title;
    private DateTime _start;
    private DateTime _stop;


    public XmlSchema GetSchema() { return null; }

    public void ReadXml(XmlReader reader)
    {
        if (reader.MoveToContent() == XmlNodeType.Element && reader.LocalName == "MyEvent")
        {
            _title = reader["Title"];
            _start = DateTime.FromBinary(Int64.Parse(reader["Start"]));
            _stop  = DateTime.FromBinary(Int64.Parse(reader["Stop"]));
            reader.Read();
        }
    }

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteAttributeString("Title", _title);
        writer.WriteAttributeString("Start", _start.ToBinary().ToString());
        writer.WriteAttributeString("Stop",  _stop.ToBinary().ToString());
    }
}

XML de amostra correspondente

<MyCalendar Name="Master Plan" Enabled="True" Color="-14069085">
    <MyEvent Title="Write Code" Start="-8589241828854775808" Stop="-8589241756854775808" />
    <MyEvent Title="???" Start="-8589241828854775808" Stop="-8589241756854775808" />
    <MyEvent Title="Profit!" Start="-8589247048854775808" Stop="-8589246976854775808" />
</MyCalendar>
Greg
fonte
3
Você poderia adicionar uma amostra xml a esta pergunta? Isso tornaria mais simples a leitura junto com o código. Obrigado!
Rory
Que tal lidar com o caso em que há um comentário XML etc após o último evento no seu xml. ou seja, você deve terminar o método ReadXml () com algo que verifique se você leu até o elemento final? Atualmente, isso pressupõe que o último Read () faça isso, mas nem sempre.
Rory
7
@Rory - Exemplo adicionado. Antes tarde do que nunca?
Greg
@Greg Good info. Você também não gostaria que ReadXml e WriteXml usassem a cultura invariável? Acho que você pode ter problemas se o usuário se mudar para outro país e alterar as configurações de região e idioma. Nesse caso, o código pode não desserializar corretamente. Eu li que é uma prática recomendada para usar sempre a cultura invariável ao fazer a serialização
pública sem fio

Respostas:

100

Sim, GetSchema () deve retornar nulo .

Método IXmlSerializable.GetSchema Este método está reservado e não deve ser usado. Ao implementar a interface IXmlSerializable, você deve retornar uma referência nula (Nada no Visual Basic) desse método e, em vez disso, se especificar um esquema personalizado é necessário, aplique XmlSchemaProviderAttribute à classe.

Para leitura e gravação, o elemento do objeto já foi gravado, portanto, você não precisa adicionar um elemento externo na gravação. Por exemplo, você pode apenas começar a ler / escrever atributos nos dois.

Para gravação :

A implementação WriteXml que você fornece deve gravar a representação XML do objeto. A estrutura grava um elemento wrapper e posiciona o gravador XML após seu início. Sua implementação pode escrever seu conteúdo, incluindo elementos filho. A estrutura então fecha o elemento wrapper.

E para ler :

O método ReadXml deve reconstituir seu objeto usando as informações que foram gravadas pelo método WriteXml.

Quando esse método é chamado, o leitor é posicionado no início do elemento que agrupa as informações do seu tipo. Ou seja, logo antes da tag de início, que indica o início de um objeto serializado. Quando esse método retorna, ele deve ter lido todo o elemento do começo ao fim, incluindo todo o seu conteúdo. Diferentemente do método WriteXml, a estrutura não trata o elemento wrapper automaticamente. Sua implementação deve fazer isso. A não observação dessas regras de posicionamento pode fazer com que o código gere exceções inesperadas de tempo de execução ou corrompa dados.

Concordo que isso é um pouco incerto, mas tudo se resume a "é seu trabalho a Read()tag do elemento final do wrapper".

Marc Gravell
fonte
Que tal escrever e ler os elementos do Evento? Parece hackish escrever manualmente o elemento inicial. Acho que vi alguém usar um XmlSerializer no método write para escrever cada elemento filho.
Greg
@Greg; qualquer uso é bom ... sim, você pode usar um XmlSerializer aninhado, se precisar, mas não é a única opção.
Marc Gravell
3
Obrigado por essas precisões, o código de exemplo no MSDN é bastante inútil e pouco claro sobre isso. Fiquei preso muitas vezes e fiquei pensando sobre o comportamento assimétrico do Read / WriteXml.
21139 jdehaan
1
@ MarcGravell Eu sei que este é um tópico antigo. "A estrutura grava um elemento wrapper e posiciona o gravador XML após seu início." É aqui que estou lutando. Existe uma maneira de forçar a estrutura a ignorar esta etapa de manipulação automática do wrapper? Eu tenho uma situação onde eu preciso pular esta etapa: stackoverflow.com/questions/20885455/...
James
@ James não para o melhor de meu conhecimento
Marc Gravell
34

Escrevi um artigo sobre o assunto com exemplos, já que a documentação do MSDN ainda não é clara e os exemplos que você pode encontrar na Web são na maioria das vezes implementados incorretamente.

Armadilhas são manipulação de localidades e elementos vazios ao lado do que Marc Gravell já mencionou.

http://www.codeproject.com/KB/XML/ImplementIXmlSerializable.aspx

jdehaan
fonte
Excelente artigo! Definitivamente vou referenciá-lo da próxima vez que estiver procurando serializar alguns dados.
Greg
Obrigado! a quantidade de feedback positivo recompensa a quantidade de tempo investido em escrevê-lo. Eu aprecio profundamente que você goste! Não hesite em pedir para criticar alguns pontos.
21139 jdehaan
Exemplos são muito mais úteis do que citar o MSDN.
Obrigado pelo projeto de código, eu votaria nisso também, se pudesse. O material sobre os atributos foi totalmente abrangente comparado ao MSDN. Por exemplo, minha classe: IXMLSerializable quebrou quando prefixada pelo xsd.exe gerado [Serializable (), XmlType (Namespace = "MonitorService")].
John John
8

Sim, a coisa toda é um campo minado, não é? A resposta de Marc Gravell praticamente cobre, mas eu gostaria de acrescentar que, em um projeto em que trabalhei, achamos bastante estranho ter que escrever manualmente o elemento XML externo. Também resultou em nomes inconsistentes de elementos XML para objetos do mesmo tipo.

Nossa solução foi definir nossa própria IXmlSerializableinterface, derivada da do sistema, que adicionou um método chamado WriteOuterXml(). Como você pode imaginar, esse método simplesmente escreveria o elemento externo, depois chamaria e WriteXml(), em seguida, escreveria o final do elemento. Obviamente, o serializador XML do sistema não chamaria esse método; portanto, só foi útil quando fizemos nossa própria serialização, para que possa ou não ser útil no seu caso. Da mesma forma, adicionamos um ReadContentXml()método que não leu o elemento externo, apenas seu conteúdo.

EMP
fonte
5
Com o C # 3.0, você provavelmente pode fazer isso escrevendo um método de extensão, mas uma ideia interessante.
Marc Gravell
2

Se você já possui uma representação XmlDocument de sua classe ou prefere a maneira XmlDocument de trabalhar com estruturas XML, uma maneira rápida e suja de implementar o IXmlSerializable é passar esse xmldoc para as várias funções.

AVISO: XmlDocument (e / ou XDocument) é uma ordem de magnitude mais lenta que xmlreader / writer, portanto, se o desempenho é um requisito absoluto, esta solução não é para você!

class ExampleBaseClass : IXmlSerializable { 
    public XmlDocument xmlDocument { get; set; }
    public XmlSchema GetSchema()
    {
        return null;
    }
    public void ReadXml(XmlReader reader)
    {
        xmlDocument.Load(reader);
    }

    public void WriteXml(XmlWriter writer)
    {
        xmlDocument.WriteTo(writer);
    }
}
Thijs Dalhuijsen
fonte
0

A implementação da interface é abordada pelas outras respostas, mas eu queria lançar meus 2 centavos para o elemento raiz.

Eu aprendi no passado a preferir colocar o elemento raiz como metadados. Isso tem alguns benefícios:

  • Se houver um objeto nulo, ele ainda poderá serializar
  • Do ponto de vista da legibilidade do código, faz sentido

Abaixo está um exemplo de um dicionário serializável em que o elemento raiz do dicionário é definido dessa maneira:

using System.Collections.Generic;

[System.Xml.Serialization.XmlRoot("dictionary")]
public partial class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, System.Xml.Serialization.IXmlSerializable
{
            public virtual System.Xml.Schema.XmlSchema GetSchema()
    {
        return null;
    }

    public virtual void ReadXml(System.Xml.XmlReader reader)
    {
        var keySerializer = new System.Xml.Serialization.XmlSerializer(typeof(TKey));
        var valueSerializer = new System.Xml.Serialization.XmlSerializer(typeof(TValue));
        bool wasEmpty = reader.IsEmptyElement;
        reader.Read();
        if (wasEmpty)
            return;
        while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
        {
            reader.ReadStartElement("item");
            reader.ReadStartElement("key");
            TKey key = (TKey)keySerializer.Deserialize(reader);
            reader.ReadEndElement();
            reader.ReadStartElement("value");
            TValue value = (TValue)valueSerializer.Deserialize(reader);
            reader.ReadEndElement();
            Add(key, value);
            reader.ReadEndElement();
            reader.MoveToContent();
        }

        reader.ReadEndElement();
    }

    public virtual void WriteXml(System.Xml.XmlWriter writer)
    {
        var keySerializer = new System.Xml.Serialization.XmlSerializer(typeof(TKey));
        var valueSerializer = new System.Xml.Serialization.XmlSerializer(typeof(TValue));
        foreach (TKey key in Keys)
        {
            writer.WriteStartElement("item");
            writer.WriteStartElement("key");
            keySerializer.Serialize(writer, key);
            writer.WriteEndElement();
            writer.WriteStartElement("value");
            var value = this[key];
            valueSerializer.Serialize(writer, value);
            writer.WriteEndElement();
            writer.WriteEndElement();
        }
    }

    public SerializableDictionary() : base()
    {
    }

    public SerializableDictionary(IDictionary<TKey, TValue> dictionary) : base(dictionary)
    {
    }

    public SerializableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer) : base(dictionary, comparer)
    {
    }

    public SerializableDictionary(IEqualityComparer<TKey> comparer) : base(comparer)
    {
    }

    public SerializableDictionary(int capacity) : base(capacity)
    {
    }

    public SerializableDictionary(int capacity, IEqualityComparer<TKey> comparer) : base(capacity, comparer)
    {
    }

}
VoteCoffee
fonte