Десериализовать сложный объект с помощью настраиваемой XmlSerialization

Я написал следующий пример кода, чтобы сохранить слегка сложный объект FamilyTreeFile в XML и восстановить его в исходной форме.

public class XmlSerializationTest
{
    const string FileName = @"FamilyTree.xml";

    public void Run()
    {
        var rootMember = new Member() { Name = "Johny", Parent = null };
        var member1 = new Member() { Name = "Andy", Parent = rootMember };
        var member2 = new Member() { Name = "Adam", Parent = rootMember };
        var member3 = new Member() { Name = "Andrew", Parent = rootMember };
        var member4 = new Member() { Name = "Davis", Parent = member2 };
        var member5 = new Member() { Name = "Simon", Parent = member4 };

        rootMember.FamilyTree = new GenericCollection();
        rootMember.FamilyTree.Add(member1);
        rootMember.FamilyTree.Add(member2);
        rootMember.FamilyTree.Add(member3);
        member2.FamilyTree = new GenericCollection();
        member2.FamilyTree.Add(member4);
        member4.FamilyTree = new GenericCollection();
        member4.FamilyTree.Add(member5);

        var familyTree = new GenericCollection() { rootMember };

        IFamilyTreeFile file = new FamilyTreeFile()
        {
            FamilyTree = familyTree
        };

        Serialize(file);
        file = Deserialize();
    }

    public void Serialize(IFamilyTreeFile obj)
    {
        var xmlSerializer = new XmlSerializer(typeof(FamilyTreeFile));
        using (TextWriter writer = new StreamWriter(FileName))
        {
            xmlSerializer.Serialize(writer, obj);
        }
    }

    public IFamilyTreeFile Deserialize()
    {
        XmlSerializer serializer = new XmlSerializer(typeof(FamilyTreeFile));
        using (Stream stream = File.Open(FileName, FileMode.Open))
        {
            return (IFamilyTreeFile)serializer.Deserialize(stream);
        }
    }
}

public interface IMember
{
    string Name { get; set; }
    IMember Parent { get; set; }
    GenericCollection FamilyTree { get; set; }
}

[Serializable]
public class Member : IMember
{
    [XmlAttribute]
    public string Name { get; set; }
    [XmlIgnore]
    public IMember Parent { get; set; }
    public GenericCollection FamilyTree { get; set; }

    public Member()
    {
        //FamilyTree = new GenericCollection();
    }
}

[Serializable]
public class GenericCollection : List<IMember>, IXmlSerializable
{
    public System.Xml.Schema.XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        reader.MoveToContent();
        if (reader.Name == "FamilyTree")
        {
            do
            {
                reader.Read();
                if (reader.Name == "Member" && reader.IsStartElement())
                {
                    Type type = System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
                    .Where(x => x.Name == reader.Name)
                    .FirstOrDefault();
                    if (type != null)
                    {
                        var xmlSerializer = new XmlSerializer(type);
                        var member = (IMember)xmlSerializer.Deserialize(reader);
                        this.Add(member);
                    }
                }

                if (reader.Name == "FamilyTree" && reader.NodeType == System.Xml.XmlNodeType.EndElement)
                    break;
            }
            while (!reader.EOF);
        }
    }

    public void WriteXml(XmlWriter writer)
    {
        foreach (IMember rule in this)
        {
            var namespaces = new XmlSerializerNamespaces();
            namespaces.Add(String.Empty, String.Empty);
            XmlSerializer xmlSerializer = new XmlSerializer(rule.GetType());
            xmlSerializer.Serialize(writer, rule, namespaces);
        }
    }
}

public interface IFamilyTreeFile
{
    GenericCollection FamilyTree { get; set; }
}

public class FamilyTreeFile : IFamilyTreeFile
{
    public GenericCollection FamilyTree { get; set; }
}

Образец кода генерирует следующий XML-файл, который точно соответствует моим потребностям, но я не могу прочитать его с помощью метода ReadXml.

<?xml version="1.0" encoding="utf-8"?>
<FamilyTreeFile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <FamilyTree>
    <Member Name="Johny">
      <FamilyTree>
        <Member Name="Andy" />
        <Member Name="Adam">
          <FamilyTree>
            <Member Name="Davis">
              <FamilyTree>
                <Member Name="Simon" />
              </FamilyTree>
            </Member>
          </FamilyTree>
        </Member>
        <Member Name="Andrew" />
      </FamilyTree>
    </Member>
  </FamilyTree>
</FamilyTreeFile>

Мне нужна помощь в том, как я могу эффективно восстановить его?

ДОБАВЛЕНО

При добавлении новой коллекции Notes в IMember

public interface IMember
{
    string Name { get; set; }
    IMember Parent { get; set; }
    GenericCollection FamilyTree { get; set; }
    List<Note> Notes { get; set; }
}

[Serializable]
public class Note
{
    [XmlAttribute]
    public string Text { get; set; }
}

Реализация этого свойства в Member классе

[XmlArray("Notes")]
public List<Note> Notes { get; set; }

Я не могу десериализовать информацию Notes в этой строке.

var member = (IMember)xmlSerializer.Deserialize(reader);

Нет ли простого способа десериализации с помощью XmlSerializer или какой-либо инфраструктуры, которая сама все обрабатывает?


person Furqan Safdar    schedule 09.04.2015    source источник


Ответы (1)


Вот вам рабочая версия метода GenericCollection.ReadXml:

public void ReadXml(XmlReader reader)
{
    // no need to advace upfront so MoveToContent was taken out (would 
    // mess with subsequent inner deserializations anyway)

    // very important: there may be no members, so check IsEmptyElement
    if (reader.Name == "FamilyTree" && !reader.IsEmptyElement) 
    {
        do
        {
            if (reader.Name == "Member" && reader.IsStartElement())
            {
                Type type = System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
                                  .Where(x => x.Name == reader.Name)
                                  .FirstOrDefault();
                if (type != null)
                {
                    var xmlSerializer = new XmlSerializer(type);
                    var member = (IMember) xmlSerializer.Deserialize(reader);
                    this.Add(member);
                }
                continue; // to omit .Read because Deserialize did already 
                // advance us to next element
            }

            if (reader.Name == "FamilyTree" && reader.NodeType == XmlNodeType.EndElement)
                break;

            reader.Read();
        } while (!reader.EOF);
    }
}

Что вы, скорее всего, упустите в своей версии, так это то, что каждый вызов метода XmlReader, такого как Read...() или Move...(), продвигает его позицию чтения. Внутренняя десериализация объектов-членов делает то же самое. Помня об этом, должно стать ясно, что вы просто не всегда можете выдавать Read() в начале цикла, а только в самом конце. Таким образом, вы можете пропустить его с помощью ключевого слова continue, если какой-либо другой код в теле цикла (например, Deserialize() в нашем случае) уже продвинул XmlReader. То же самое относится к MoveToContent() в начале вашей версии метода. Что я изначально упустил, так это то, что коллекция участников может быть пустой. В этом случае десериализацию GenericCollection следует полностью исключить, чтобы (опять же) не запутать читателя.

Хотя это десериализует экземпляры объектов и добавляет их в соответствующие списки, ссылки (поле Parent класса Member в этом примере) не восстанавливаются. Вот где все усложняется: ссылка - это, по сути, адрес памяти. При этом нет смысла сериализовать его значение и снова десериализовать его. Поскольку теперь объекты, вероятнее всего, будут находиться в другом месте памяти, десериализованный адрес будет совершенно неправильным.

Есть два основных способа решить эту проблему:

  1. Сериализованные объекты могут быть созданы таким образом, чтобы эти ссылки автоматически создавались, когда объекты конструируются или склеиваются. Таким образом, не требуется сериализации и десериализации. Недостаток: это возможно только для ссылок, которые могут быть получены таким образом (как в текущем примере).

  2. Каждый объект, который может быть целью ссылки, может быть расширен полем идентификатора, очень похожим на первичный ключ в базе данных. Затем этот идентификатор (например, guid) должен быть сериализован и десериализован. Каждое поле ссылки (поле Parent класса Member в этом примере) должно быть сериализовано как значение идентификатора объекта, на который он ссылается (может быть выполнено путем добавления вспомогательного поля ParentID, которое устанавливается автоматически установщиком родительского поля) . Когда все десериализовано, эти ссылки должны быть восстановлены путем обхода всего дерева объектов. С другой стороны, это позволяет восстанавливать произвольные ссылки. Но нужно знать, что это действительно усложняет код.

Первый подход может быть выполнен:

Изменив это в вашей функции Run () ...

var rootMember = new Member() { Name = "Johny"};
var member1 = new Member() { Name = "Andy" };
var member2 = new Member() { Name = "Adam" };
var member3 = new Member() { Name = "Andrew" };
var member4 = new Member() { Name = "Davis" };
var member5 = new Member() { Name = "Simon" };

... изменить свойство FamilyTree класса Member на это ...

public GenericCollection FamilyTree
{
    get { return _FamilyTree; }
    set
    {
        _FamilyTree = value;
        _FamilyTree.Owner = this;
    }
}

... и вставьте это в класс GenericCollection

private IMember _Owner;
public IMember Owner
{
    get { return _Owner; }
    set
    {
        _Owner = value;
        foreach (var member in this)
        {
            member.Parent = value;
        }
    }
}

public void Add(IMember item)
{
    item.Parent = Owner;
    base.Add(item);
}

Второй подход реализован в следующем небольшом консольном приложении:

class Program
{
    public static string FileName = @"FamilyTree.xml";

    static void Main(string[] args)
    {
        // make some members
        var rootMember = new Member() { Name = "Johny" };
        var member1 = new Member() { Name = "Andy" };
        var member2 = new Member() { Name = "Adam" };
        var member3 = new Member() { Name = "Andrew" };
        var member4 = new Member() { Name = "Davis" };
        var member5 = new Member() { Name = "Simon" };

        // construct some arbitrary references between them
        member1.Reference = member4;
        member3.Reference = member1;
        member5.Reference = member2;

        // let member 3 have some notes
        member3.Notes = new List<Note>();
        member3.Notes.Add(new Note() { Text = "note1" });
        member3.Notes.Add(new Note() { Text = "note2" });

        // add all of the to the family tree
        rootMember.FamilyTree.Add(member1);
        rootMember.FamilyTree.Add(member2);
        rootMember.FamilyTree.Add(member3);
        member2.FamilyTree.Add(member4);
        member4.FamilyTree.Add(member5);

        var familyTree = new GenericCollection() { rootMember };

        IFamilyTreeFile file = new FamilyTreeFile()
        {
            FamilyTree = familyTree
        };

        Console.WriteLine("--- input ---");
        Serialize(file);
        PrintTree(file.FamilyTree, 0);
        Console.WriteLine();
        Console.WriteLine("--- output ---");
        file = Deserialize();
        file.FamilyTree.RebuildReferences(file.FamilyTree); // this is where the refereces
        // are put  together again after deserializing the object tree.
        PrintTree(file.FamilyTree, 0);
        Console.ReadLine();
    }

    private static void PrintTree(GenericCollection c, int indent)
    {
        foreach (var member in c)
        {
            string line = member.Name.PadLeft(indent, ' ');
            if (member.Reference != null)
            {
                line += " (Ref: " + member.Reference.Name + ")";
                if (member.Notes != null && member.Notes.Count > 0)
                {
                    line += " (Notes: ";
                    foreach (var note in member.Notes)
                    {
                        line += note.Text + ",";
                    }
                    line += ")";
                }
            }
            Console.WriteLine(line);
            PrintTree(member.FamilyTree, indent + 4);
        }
    }

    public static void Serialize(IFamilyTreeFile obj)
    {
        var xmlSerializer = new XmlSerializer(typeof(FamilyTreeFile));
        using (TextWriter writer = new StreamWriter(FileName))
        {
            xmlSerializer.Serialize(writer, obj);
        }
    }

    public static IFamilyTreeFile Deserialize()
    {
        XmlSerializer serializer = new XmlSerializer(typeof(FamilyTreeFile));
        using (Stream stream = File.Open(FileName, FileMode.Open))
        {
            return (IFamilyTreeFile)serializer.Deserialize(stream);
        }
    }
}

public interface IMember
{
    Guid ID { get; set; }
    string Name { get; set; }
    IMember Reference { get; set; }
    Guid ReferenceID { get; set; }
    GenericCollection FamilyTree { get; set; }
    List<Note> Notes { get; set; }
    void RebuildReferences(GenericCollection in_Root);
}

[Serializable]
public class Member : IMember
{
    private GenericCollection _FamilyTree;
    private IMember _Reference;

    [XmlAttribute]
    public Guid ID { get; set; }
    [XmlAttribute]
    public string Name { get; set; }
    [XmlAttribute]
    public Guid ReferenceID { get; set; }
    [XmlIgnore]
    public IMember Reference
    {
        get { return _Reference; }
        set
        {
            ReferenceID = value.ID;
            _Reference = value;
        }
    }
    [XmlArray("Notes")]
    public List<Note> Notes { get; set; }

    public GenericCollection FamilyTree
    {
        get { return _FamilyTree; }
        set
        {
            _FamilyTree = value;
            _FamilyTree.Owner = this;
        }
    }

    public Member()
    {
        ID = Guid.NewGuid();
        FamilyTree = new GenericCollection();
    }

    public void RebuildReferences(GenericCollection in_Root)
    {
        if (!ReferenceID.Equals(Guid.Empty))
            Reference = in_Root.FindMember(ReferenceID);

        FamilyTree.RebuildReferences(in_Root);
    }
}

[Serializable]
public class Note
{
    [XmlAttribute]
    public string Text { get; set; }
}

[Serializable]
public class GenericCollection : List<IMember>, IXmlSerializable
{
    public System.Xml.Schema.XmlSchema GetSchema()
    {
        return null;
    }

    private IMember _Owner;
    public IMember Owner
    {
        get { return _Owner; }
        set
        {
            _Owner = value;
        }
    }

    public void Add(IMember item)
    {
        base.Add(item);
    }

    public void ReadXml(XmlReader reader)
    {
        // no need to advace upfront so MoveToContent was taken out (would 
        // mess with subsequent inner deserializations anyway)

        // very important: there may be no members, so check IsEmptyElement
        if (reader.Name == "FamilyTree" && !reader.IsEmptyElement)
        {
            do
            {
                if (reader.Name == "Member" && reader.IsStartElement())
                {
                    Type type = System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
                                      .Where(x => x.Name == reader.Name)
                                      .FirstOrDefault();
                    if (type != null)
                    {
                        var xmlSerializer = new XmlSerializer(type);
                        var member = (IMember)xmlSerializer.Deserialize(reader);
                        this.Add(member);
                    }
                    continue; // to omit .Read because Deserialize did already 
                    // advance us to next element
                }

                if (reader.Name == "FamilyTree" && reader.NodeType == XmlNodeType.EndElement)
                    break;

                reader.Read();
            } while (!reader.EOF);
        }
    }

    public void WriteXml(XmlWriter writer)
    {
        foreach (IMember rule in this)
        {
            var namespaces = new XmlSerializerNamespaces();
            namespaces.Add(String.Empty, String.Empty);
            XmlSerializer xmlSerializer = new XmlSerializer(rule.GetType());
            xmlSerializer.Serialize(writer, rule, namespaces);
        }
    }

    public void RebuildReferences(GenericCollection in_Root)
    {
        foreach (IMember meber in this)
        {
            meber.RebuildReferences(in_Root);
        }
    }

    public IMember FindMember(Guid in_ID)
    {
        IMember FoundMember = null;
        foreach (IMember member in this)
        {
            if (member.ID.Equals(in_ID))
                return member;

            FoundMember = member.FamilyTree.FindMember(in_ID);
            if (FoundMember != null)
                return FoundMember;
        }
        return null;
    }
}

public interface IFamilyTreeFile
{
    GenericCollection FamilyTree { get; set; }
}

public class FamilyTreeFile : IFamilyTreeFile
{
    public GenericCollection FamilyTree { get; set; }
}

Подтверждение концепции вашего добавления к исходному вопросу раскрывается во втором примере.

person Peter Hommel    schedule 10.04.2015
comment
Спасибо за указание, но как я могу получить родительскую информацию, которую я проигнорировал из-за ошибки сериализации? Пожалуйста, помогите и в этом. - person Furqan Safdar; 12.04.2015
comment
Я имею в виду два разных возможных подхода. Пожалуйста, не стесняйтесь, ведь мне нужно время на внедрение и тестирование. Я отредактирую свой ответ, когда закончу ... - person Peter Hommel; 14.04.2015
comment
Спасибо за ваши усилия, но любезно освободите время, чтобы обновить свой ответ вторым подходом. Это не только поможет мне учиться, но и другим может оказаться полезным. - person Furqan Safdar; 15.04.2015
comment
@Perter, большое спасибо, я также буду оценивать второй подход, но в настоящее время я все еще сталкиваюсь с одной проблемой десериализации, когда я добавил новую коллекцию в IMember, скажем, Notes типа List ‹Note›, где у Note есть одно свойство с именем Текст строки типа. Я добавил в свой вопрос новый блок по этому поводу. - person Furqan Safdar; 23.04.2015
comment
Я только что исправил ошибку в исходном коде ответа. Теперь все должно быть хорошо. - person Peter Hommel; 24.04.2015
comment
Моя проблема с добавленным вопросом все еще не решена в моем собственном большом решении, однако в этом фрагменте кода для справки он работает нормально. Я сопоставил все немного, но не повезло. Тем не менее после десериализации члена читатель переходит к Notes отдельно. Ты хоть представляешь, в чем может быть причина? - person Furqan Safdar; 29.04.2015
comment
@FSX: Извините за задержку, но до сих пор я был очень занят. У вас все еще есть проблемы с десериализацией? Мне кажется вполне логичным, что Notes десериализуется после десериализации субчлена, учитывая порядок полей в вашем примере. Почему это проблема? - person Peter Hommel; 11.05.2015
comment
Уже довольно поздно, но я заметил очень странное поведение: если вы измените порядок свойства Notes и объявите его после свойства FamilyTree в классе Member, вы можете увидеть проблему, которую я обсуждал на раннем этапе. Он не может десериализовать Notes и заходит в бесконечный цикл. Почему это так? - person Furqan Safdar; 21.09.2015