使用自定义 XmlSerialization 反序列化复杂对象

Deserialize complex object using custom 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>

我需要帮助才能有效恢复它?

已添加

IMember

中添加新集合 Notes
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 class

中实施此 属性
[XmlArray("Notes")]
public List<Note> Notes { get; set; }

我无法在这一行反序列化 Notes 信息。

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

有没有任何简单的方法可以使用 XmlSerializer 或任何自行处理一切的框架进行反序列化?

这是适合您的 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...())都会提高其阅读位置。成员 objects 的内部反序列化也是如此。 牢记这一点,很明显您不能总是在循环开始时发出 Read(),而只能在循环结束时发出。这样您就可以使用 continue 关键字跳过它,以防循环体中的某些其他代码(如本例中的 Deserialize())已经推进了 XmlReader。同样适用于您的方法版本开头的 MoveToContent() 。我最初也确实错过了成员集合可以为空的事实。在那种情况下,必须完全省略 GenericCollection 的反序列化,因为(再次)不要弄乱 reader.

虽然这会反序列化 object 实例并将它们添加到各自的列表中,但不会重建引用(本例中成员 class 的 Parent 字段)。这就是事情变得棘手的地方:引用本质上是一个内存地址。正因为如此,序列化它的价值并再次反序列化它是没有意义的。因为 objects 现在很可能驻留在另一个内存位置,所以反序列化地址将是完全错误的。

基本上有两种方法可以解决这个问题:

  1. 序列化的 object 可以以一种自动创建这些引用的方式构建,当 object 被构建或粘合在一起时。这样就根本不需要序列化和反序列化。缺点是:这仅适用于可以通过这种方式获得的引用(当前示例中就是这种情况)

  2. 每个可以作为引用目标的 object 都可以通过标识符字段进行扩展,这与数据库中的主键非常相似。该标识符(例如 guid)随后将被序列化和反序列化。每个引用字段(本例中成员 class 的 Parent 字段)将被序列化为它引用的 object 的标识符值(可以通过添加辅助字段 ParentID,由Parent字段的setter自动设置)。当一切都被反序列化时,这些引用必须通过遍历 object 的整个树来重建。从好的方面来说,这使人们能够重建任意引用。但是必须意识到这给代码增加了一些真正的复杂性。

第一种方法可以通过以下方式完成:

正在 运行() 函数中更改此...

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" };

...将 class 成员的 属性 家谱更改为此...

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

... 并将其插入 class 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; }
}

在第二个示例中公开了您添加到原始问题的概念证明。