Десериализация XML с указанием родительского объекта

У меня есть XML-файл с описанием сайта. Он состоит из сайта в качестве корневого узла, который может иметь страницы, а страницы могут иметь такие объекты, как кнопка или TextBox и диалоги. Диалоги также могут иметь объекты.

В соответствующих классах C# все происходит от Element. Когда я десериализирую XML, как я могу получить ссылку на Parent создаваемого элемента?

Мне сказали, что сложные типы не могут быть десериализованы таким образом, но если я добавлю поле ID к каждому элементу в XML, то на родительский элемент можно будет ссылаться с помощью этого и, следовательно, на правильную десериализацию. Как бы я это реализовал? Я не хотел бы вручную добавлять поле ID для каждого элемента в файле XML...

Мой класс элементов:

public class Element 
{
    public string Name { get; set; }
    public string TagName { get; set; }
    public string XPath { get; set; }

    [XmlElement(ElementName = "Site", Type = typeof(Site))]
    [XmlElement(ElementName = "Page", Type = typeof(Page))]
    [XmlElement(ElementName = "Dialog", Type = typeof(Dialog))]
    public Element Parent { get; set; }

    [XmlArray("Children", IsNullable = false)]
    [XmlArrayItem(Type = typeof(TextBox))]
    [XmlArrayItem(Type = typeof(Page))]
    [XmlArrayItem(Type = typeof(Button))]
    [XmlArrayItem(Type = typeof(Dialog))]
    public Collection<Element> Children { get; set; }

}

Моя десериализация:

public Site GetSiteFromXml(string filePath, string fileName)
{
    XmlSerializer serializer = new XmlSerializer(typeof(Site));
    return serializer.Deserialize(new XmlTextReader(Path.Combine(filePath, fileName))) as Site;
}

Мой XML-файл:

<Site>
  <Name>WebSiteName</Name>
  <Url>https://site.url</Url>
  <Children>
    <Page>
      <Name>HomePage</Name>
      <Address>#page=1</Address>
      <Children>
        <Button>
          <Name>LoginDialogButton</Name>
          <Id>LoginIcon</Id>
          <XPath>//*[@id="LoginIcon"]</XPath>
          <Enabled>true</Enabled>
          <Action>OpenLoginDialog</Action>
        </Button>
        <Dialog>
          <Name>LoginPopUpDialog</Name>
          <Id>loginModal</Id>
          <Children>
            <TextBox>
              <Name>UserNameInput</Name>
            </TextBox>
            <TextBox>
              <Name>PasswordInput</Name>
            </TextBox>
            <Button>
              <Name>LoginButton</Name>
              <Action>DialogDismiss</Action>
            </Button>
          </Children>
        </Dialog>
      </Children>
    </Page>
  </Children>
</Site>

1 ответ

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

public class Element
{
    public Element() { Children = new Collection<Element>(); }

    public string Name { get; set; }
    public string TagName { get; set; }
    public string XPath { get; set; }

    [XmlIgnore]
    public Element Parent { get; set; }

    [XmlIgnore]
    public Collection<Element> Children { get; set; }

    [XmlArray("Children", IsNullable = false)]
    [XmlArrayItem(Type = typeof(TextBox))]
    [XmlArrayItem(Type = typeof(Page))]
    [XmlArrayItem(Type = typeof(Button))]
    [XmlArrayItem(Type = typeof(Dialog))]
    [XmlArrayItem(Type = typeof(Site))]
    public Element[] ChildArrayCopy
    {
        get
        {
            return Children.ToArray();
        }
        set
        {
            Children.Clear();
            if (value != null)
                foreach (var child in value)
                    child.SetParent(this);
        }
    }
}

public static class ElementExtensions
{
    public static void SetParent(this Element child, Element parent)
    {
        if (child == null)
            throw new ArgumentNullException();
        if (child.Parent == parent)
            return; // Nothing to do.
        if (child.Parent != null)
            child.Parent.Children.Remove(child);
        child.Parent = parent;
        if (parent != null)
            parent.Children.Add(child);
    }
}

Ваш XML теперь можно успешно десериализовать и сериализовать.

Кстати, я бы рассмотрел замену вашего public Collection<Element> Children { get; set; } с public ReadOnlyCollection<Element> Children { get; }, Затем держите детей в личном списке и возвращайте list.AsReadOnly(), Сделав это, вы теперь можете гарантировать, что список детей остается актуальным в Parent сеттер:

public class Element
{

    public Element() { }

    public string Name { get; set; }
    public string TagName { get; set; }
    public string XPath { get; set; }

    private readonly List<Element> children = new List<Element>();
    private Element parent = null;

    [XmlIgnore]
    public Element Parent
    {
        get
        {
            return parent;
        }
        set
        {
            if (parent == value)
                return;
            if (parent != null)
                parent.children.Remove(this);
            parent = value;
            if (parent != null)
                parent.children.Add(this);
        }
    }

    [XmlIgnore]
    public ReadOnlyCollection<Element> Children
    {
        get
        {
            return children.AsReadOnly();
        }
    }

    [XmlArray("Children", IsNullable = false)]
    [XmlArrayItem(Type = typeof(TextBox))]
    [XmlArrayItem(Type = typeof(Page))]
    [XmlArrayItem(Type = typeof(Button))]
    [XmlArrayItem(Type = typeof(Dialog))]
    [XmlArrayItem(Type = typeof(Site))]
    public Element[] ChildArrayCopy
    {
        get
        {
            return Children.ToArray();
        }
        set
        {
            if (value != null)
                foreach (var child in value)
                    child.Parent = this;
        }
    }
}
Другие вопросы по тегам