XML Serialization และประเภทที่สืบทอด


85

ต่อจากคำถามก่อนหน้าของฉันฉันได้ดำเนินการเพื่อรับโมเดลวัตถุของฉันเพื่อทำให้เป็นอนุกรมกับ XML แต่ตอนนี้ฉันประสบปัญหาแล้ว (แปลกใจ!)

ปัญหาที่ฉันมีคือฉันมีคอลเล็กชันซึ่งเป็นประเภทคลาสฐานนามธรรมซึ่งบรรจุโดยประเภทที่ได้จากคอนกรีต

ฉันคิดว่ามันจะเป็นการดีที่จะเพิ่มแอตทริบิวต์ XML ให้กับคลาสทั้งหมดที่เกี่ยวข้องและทุกอย่างจะเป็นสีพีช น่าเศร้าที่ไม่เป็นเช่นนั้น!

ดังนั้นฉันได้ทำการขุดใน Google และตอนนี้ฉันเข้าใจแล้วว่าทำไมมันถึงไม่ทำงาน ในความXmlSerializerเป็นจริงแล้วการสะท้อนที่ชาญฉลาดเพื่อทำให้วัตถุเป็นอนุกรมเป็น / จาก XML และเนื่องจากมันขึ้นอยู่กับประเภทนามธรรมจึงไม่สามารถเข้าใจได้ว่ามันกำลังพูดถึงอะไร ละเอียด.

ฉันเจอหน้านี้ใน CodeProject ซึ่งดูเหมือนว่ามันอาจช่วยได้มาก (ยังอ่าน / ใช้อย่างเต็มที่) แต่ฉันคิดว่าฉันอยากจะนำปัญหานี้ไปที่ตาราง StackOverflow ด้วยเพื่อดูว่าคุณเรียบร้อยหรือไม่ แฮ็ก / เทคนิคเพื่อให้สิ่งนี้เริ่มต้นใช้งานได้เร็วที่สุด / เบาที่สุด

สิ่งหนึ่งที่ฉันควรเพิ่มก็คือฉันไม่ต้องการไปตามXmlIncludeเส้นทาง มีการเชื่อมต่อกับมันมากเกินไปและพื้นที่นี้ของระบบอยู่ระหว่างการพัฒนาอย่างหนักดังนั้นจึงน่าจะเป็นเรื่องที่น่าปวดหัวในการบำรุงรักษาอย่างแท้จริง!


1
การดูข้อมูลโค้ดที่เกี่ยวข้องบางส่วนที่ดึงมาจากคลาสที่คุณพยายามทำให้เป็นอนุกรมจะเป็นประโยชน์
Rex M

Mate: ฉันเปิดอีกครั้งเพราะฉันรู้สึกว่าคนอื่นจะเห็นว่าสิ่งนี้มีประโยชน์ แต่
อย่า

สับสนเล็กน้อยเนื่องจากไม่มีอะไรในกระทู้นี้มานานแล้ว?
Rob Cooper

คำตอบ:


54

แก้ไขปัญหา!

ตกลงดังนั้นในที่สุดฉันก็มี (เป็นที่ยอมรับมีจำนวนมากของความช่วยเหลือจากที่นี่ !)

สรุป:

เป้าหมาย:

  • ฉันไม่ต้องการลงเส้นทางXmlIncludeเนื่องจากปวดหัวในการบำรุงรักษา
  • เมื่อพบวิธีแก้ปัญหาแล้วฉันต้องการให้นำไปใช้ในแอปพลิเคชันอื่นได้อย่างรวดเร็ว
  • อาจใช้คอลเล็กชันประเภทบทคัดย่อรวมทั้งคุณสมบัตินามธรรมแต่ละรายการ
  • ฉันไม่อยากจะกังวลกับการต้องทำสิ่ง "พิเศษ" ในชั้นเรียนที่เป็นรูปธรรม

ระบุประเด็น / ประเด็นที่ควรทราบ:

  • XmlSerializerให้การสะท้อนที่ค่อนข้างเท่ แต่มีข้อ จำกัดมากเมื่อพูดถึงประเภทนามธรรม (กล่าวคือจะใช้ได้เฉพาะกับอินสแตนซ์ของประเภทนามธรรมเท่านั้นไม่ใช่คลาสย่อย)
  • ตัวตกแต่งแอตทริบิวต์ Xml กำหนดวิธีที่ XmlSerializer ปฏิบัติต่อคุณสมบัติที่พบ นอกจากนี้ยังสามารถระบุประเภททางกายภาพได้ แต่จะสร้างการมีเพศสัมพันธ์ที่แน่นหนาระหว่างคลาสและซีเรียลไลเซอร์ (ไม่ดี)
  • เราสามารถใช้ XmlSerializer ของเราเองโดยการสร้างคลาสที่ดำเนินIXmlSerializable

การแก้ไขปัญหา

ฉันสร้างคลาสทั่วไปซึ่งคุณระบุประเภททั่วไปเป็นประเภทนามธรรมที่คุณจะใช้งาน สิ่งนี้ทำให้คลาสสามารถ "แปล" ระหว่างชนิดนามธรรมและประเภทคอนกรีตได้เนื่องจากเราสามารถฮาร์ดโค้ดการหล่อ (กล่าวคือเราสามารถรับข้อมูลได้มากกว่าที่ XmlSerializer สามารถทำได้)

จากนั้นฉันก็ใช้อินเทอร์เฟซIXmlSerializableซึ่งค่อนข้างตรงไปตรงมา แต่เมื่อทำให้เป็นอนุกรมเราจำเป็นต้องตรวจสอบให้แน่ใจว่าเราเขียนประเภทของคลาสคอนกรีตลงใน XML เพื่อให้เราสามารถส่งกลับได้เมื่อทำการยกเลิกการทำให้เป็นอนุกรม สิ่งสำคัญคือต้องทราบว่าต้องมีคุณสมบัติครบถ้วนเนื่องจากส่วนประกอบที่ทั้งสองคลาสอยู่ในนั้นมีแนวโน้มที่จะแตกต่างกัน แน่นอนว่ามีการตรวจสอบประเภทเล็ก ๆ น้อย ๆ และสิ่งที่ต้องเกิดขึ้นที่นี่

เนื่องจาก XmlSerializer ไม่สามารถแคสต์ได้เราจึงจำเป็นต้องระบุรหัสเพื่อทำเช่นนั้นดังนั้นตัวดำเนินการโดยนัยจึงทำงานมากเกินไป (ฉันไม่เคยรู้มาก่อนเลยว่าคุณสามารถทำได้!)

รหัสสำหรับ AbstractXmlSerializer คือ:

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;

namespace Utility.Xml
{
    public class AbstractXmlSerializer<AbstractType> : IXmlSerializable
    {
        // Override the Implicit Conversions Since the XmlSerializer
        // Casts to/from the required types implicitly.
        public static implicit operator AbstractType(AbstractXmlSerializer<AbstractType> o)
        {
            return o.Data;
        }

        public static implicit operator AbstractXmlSerializer<AbstractType>(AbstractType o)
        {
            return o == null ? null : new AbstractXmlSerializer<AbstractType>(o);
        }

        private AbstractType _data;
        /// <summary>
        /// [Concrete] Data to be stored/is stored as XML.
        /// </summary>
        public AbstractType Data
        {
            get { return _data; }
            set { _data = value; }
        }

        /// <summary>
        /// **DO NOT USE** This is only added to enable XML Serialization.
        /// </summary>
        /// <remarks>DO NOT USE THIS CONSTRUCTOR</remarks>
        public AbstractXmlSerializer()
        {
            // Default Ctor (Required for Xml Serialization - DO NOT USE)
        }

        /// <summary>
        /// Initialises the Serializer to work with the given data.
        /// </summary>
        /// <param name="data">Concrete Object of the AbstractType Specified.</param>
        public AbstractXmlSerializer(AbstractType data)
        {
            _data = data;
        }

        #region IXmlSerializable Members

        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null; // this is fine as schema is unknown.
        }

        public void ReadXml(System.Xml.XmlReader reader)
        {
            // Cast the Data back from the Abstract Type.
            string typeAttrib = reader.GetAttribute("type");

            // Ensure the Type was Specified
            if (typeAttrib == null)
                throw new ArgumentNullException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because no 'type' attribute was specified in the XML.");

            Type type = Type.GetType(typeAttrib);

            // Check the Type is Found.
            if (type == null)
                throw new InvalidCastException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because the type specified in the XML was not found.");

            // Check the Type is a Subclass of the AbstractType.
            if (!type.IsSubclassOf(typeof(AbstractType)))
                throw new InvalidCastException("Unable to Read Xml Data for Abstract Type '" + typeof(AbstractType).Name +
                    "' because the Type specified in the XML differs ('" + type.Name + "').");

            // Read the Data, Deserializing based on the (now known) concrete type.
            reader.ReadStartElement();
            this.Data = (AbstractType)new
                XmlSerializer(type).Deserialize(reader);
            reader.ReadEndElement();
        }

        public void WriteXml(System.Xml.XmlWriter writer)
        {
            // Write the Type Name to the XML Element as an Attrib and Serialize
            Type type = _data.GetType();

            // BugFix: Assembly must be FQN since Types can/are external to current.
            writer.WriteAttributeString("type", type.AssemblyQualifiedName);
            new XmlSerializer(type).Serialize(writer, _data);
        }

        #endregion
    }
}

ดังนั้นจากตรงนั้นเราจะบอก XmlSerializer ให้ทำงานกับ serializer ของเราได้อย่างไรแทนที่จะเป็นค่าเริ่มต้น เราต้องส่งผ่านประเภทของเราภายในคุณสมบัติประเภทแอตทริบิวต์ Xml ตัวอย่างเช่น:

[XmlRoot("ClassWithAbstractCollection")]
public class ClassWithAbstractCollection
{
    private List<AbstractType> _list;
    [XmlArray("ListItems")]
    [XmlArrayItem("ListItem", Type = typeof(AbstractXmlSerializer<AbstractType>))]
    public List<AbstractType> List
    {
        get { return _list; }
        set { _list = value; }
    }

    private AbstractType _prop;
    [XmlElement("MyProperty", Type=typeof(AbstractXmlSerializer<AbstractType>))]
    public AbstractType MyProperty
    {
        get { return _prop; }
        set { _prop = value; }
    }

    public ClassWithAbstractCollection()
    {
        _list = new List<AbstractType>();
    }
}

ที่นี่คุณจะเห็นว่าเรามีคอลเลกชันและคุณสมบัติเดียวที่ถูกเปิดเผยและสิ่งที่เราต้องทำคือเพิ่มพารามิเตอร์type named ในการประกาศ Xml ง่าย ๆ ! : ง

หมายเหตุ: หากคุณใช้รหัสนี้ฉันขอขอบคุณอย่างยิ่งกับการตะโกนออกมา นอกจากนี้ยังช่วยผลักดันให้ผู้คนเข้าสู่ชุมชนมากขึ้น :)

ตอนนี้ แต่ไม่แน่ใจว่าจะทำอย่างไรกับคำตอบที่นี่เนื่องจากพวกเขาทั้งหมดมีโปรและข้อเสีย ฉันจะปรับปรุงสิ่งที่ฉันรู้สึกว่ามีประโยชน์ (ไม่มีความผิดต่อผู้ที่ไม่ได้เป็น) และปิดสิ่งนี้เมื่อฉันมีตัวแทน :)

ปัญหาที่น่าสนใจและสนุกกับการแก้ปัญหา! :)


ฉันพบปัญหานี้ด้วยตัวเองเมื่อนานมาแล้ว โดยส่วนตัวแล้วฉันลงเอยด้วยการละทิ้ง XmlSerializer และใช้อินเทอร์เฟซ IXmlSerializable โดยตรงเนื่องจากคลาสทั้งหมดของฉันจำเป็นต้องใช้งานได้ มิฉะนั้นการแก้ปัญหาจะค่อนข้างคล้ายกัน เขียนได้ดี :)
Thorarin

เราใช้คุณสมบัติ XML_ ที่เราแปลงรายการเป็น Arrays :)
Arcturus

2
เนื่องจากจำเป็นต้องใช้ตัวสร้างแบบไม่มีพารามิเตอร์เพื่อสร้างอินสแตนซ์คลาสแบบไดนามิก
Silas Hansen

1
สวัสดี! ฉันมองหาวิธีแก้ปัญหาแบบนี้มาระยะหนึ่งแล้ว ฉันคิดว่ามันยอดเยี่ยม! แม้ว่าฉันจะคิดไม่ออกว่าจะใช้มันอย่างไรคุณช่วยยกตัวอย่างได้ไหม? คุณจัดลำดับชั้นเรียนหรือรายการที่มีวัตถุของคุณหรือไม่?
Daniel

1
รหัสที่ดี โปรดทราบว่าสามารถประกาศคอนทราสต์แบบไม่มีพารามิเตอร์privateหรือprotectedบังคับว่าไม่สามารถใช้ได้กับคลาสอื่น
tcovo

9

สิ่งหนึ่งที่ต้องพิจารณาคือความจริงที่ว่าในตัวสร้าง XmlSerialiser คุณสามารถส่งอาร์เรย์ประเภทที่ serialiser อาจมีปัญหาในการแก้ไข ฉันต้องใช้มันไม่กี่ครั้งที่ต้องมีการรวบรวมหรือชุดโครงสร้างข้อมูลที่ซับซ้อนและประเภทเหล่านั้นอาศัยอยู่ในแอสเซมบลีที่แตกต่างกันเป็นต้น

XmlSerialiser Constructor พร้อมพารามิเตอร์ extraTypes

แก้ไข: ฉันจะเพิ่มว่าวิธีนี้มีประโยชน์เหนือ XmlInclude แอตทริบิวต์และอื่น ๆ ที่คุณสามารถหาวิธีค้นหาและรวบรวมรายการประเภทคอนกรีตที่เป็นไปได้ของคุณในรันไทม์และใส่เข้าไป


นี่คือสิ่งที่ฉันพยายามทำ แต่มันไม่ง่ายอย่างที่คิด: stackoverflow.com/questions/3897818/…
Luca

นี่เป็นโพสต์เก่ามาก แต่สำหรับใครก็ตามที่ต้องการใช้สิ่งนี้เช่นเดียวกับเราโปรดทราบว่าตัวสร้างของ XmlSerializer ที่มีพารามิเตอร์ extraTypes จะไม่แคชแอสเซมบลีที่สร้างขึ้นทันที ซึ่งทำให้เราเสียเวลาหลายสัปดาห์ในการดีบักหน่วยความจำรั่ว ดังนั้นหากคุณต้องการใช้ประเภทพิเศษกับรหัสของคำตอบที่ยอมรับให้แคชซีเรียลไลเซอร์ ลักษณะการทำงานนี้มีการบันทึกไว้ที่นี่: support.microsoft.com/en-us/kb/886385
Julien Lebot

3

อย่างจริงจังกรอบการทำงานที่ขยายได้ของ POCO จะไม่ทำให้อนุกรมกับ XML ได้อย่างน่าเชื่อถือ ฉันพูดแบบนี้เพราะฉันรับรองได้ว่าจะมีคนมาด้วยขยายชั้นเรียนของคุณและทำให้ยุ่งยาก

คุณควรพิจารณาการใช้ XAML ในการจัดลำดับกราฟวัตถุของคุณ ได้รับการออกแบบมาเพื่อทำสิ่งนี้ในขณะที่การทำให้เป็นอนุกรม XML ไม่ได้

Xaml serializer และ deserializer จัดการ generics โดยไม่มีปัญหาคอลเลกชันของคลาสพื้นฐานและอินเทอร์เฟซเช่นกัน (ตราบใดที่คอลเลกชันเองใช้IListหรือIDictionary) มีข้อแม้บางประการเช่นการทำเครื่องหมายคุณสมบัติคอลเลกชันแบบอ่านอย่างเดียวDesignerSerializationAttributeของคุณด้วยรหัส แต่การแก้ไขโค้ดของคุณใหม่เพื่อจัดการกับกรณีมุมเหล่านี้ไม่ใช่เรื่องยาก


ลิงค์ดูเหมือนจะตาย
bkribbs

โอ้ดี ฉันจะเรียกมันว่าบิต แหล่งข้อมูลอื่น ๆ มากมายเกี่ยวกับเรื่องนี้

2

เพียงแค่อัปเดตอย่างรวดเร็วฉันยังไม่ลืม!

เพียงแค่ทำการค้นคว้าเพิ่มเติมดูเหมือนว่าฉันจะเป็นผู้ชนะเพียงแค่ต้องจัดเรียงรหัส

จนถึงตอนนี้ฉันมีสิ่งต่อไปนี้:

  • XmlSeralizerเป็นพื้นชั้นที่ไม่ดีบางอย่างที่สะท้อนในชั้นเรียนก็มี serializing มันเป็นตัวกำหนดคุณสมบัติที่ต่อเนื่องขึ้นอยู่กับประเภท
  • สาเหตุที่ทำให้ปัญหาเกิดขึ้นเนื่องจากมีประเภทที่ไม่ตรงกันเกิดขึ้นคาดว่าจะมีBaseTypeแต่ในความเป็นจริงได้รับDerivedType .. ในขณะที่คุณอาจคิดว่าจะปฏิบัติกับมันหลายรูปแบบ แต่ก็ไม่ได้เกี่ยวข้องกับภาระพิเศษทั้งหมดของ การสะท้อนและการตรวจสอบประเภทซึ่งไม่ได้ออกแบบมาให้ทำ

ลักษณะการทำงานนี้ดูเหมือนจะสามารถถูกแทนที่ได้ (โค้ดที่รอดำเนินการ) โดยการสร้างคลาสพร็อกซีเพื่อทำหน้าที่เป็นตัวเชื่อมระหว่างซีเรียลไลเซอร์ โดยพื้นฐานแล้วจะกำหนดประเภทของคลาสที่ได้รับจากนั้นจึงทำให้เป็นอนุกรมตามปกติ จากนั้นคลาสพร็อกซีนี้จะฟีด XML นั้นสำรองบรรทัดไปยัง serializer หลัก

ดูพื้นที่นี้! ^ _ ^


2

แน่นอนว่าเป็นวิธีแก้ปัญหาของคุณ แต่มีอีกปัญหาหนึ่งซึ่งค่อนข้างบั่นทอนความตั้งใจของคุณที่จะใช้รูปแบบ XML "แบบพกพา" สิ่งที่ไม่ดีเกิดขึ้นเมื่อคุณตัดสินใจเปลี่ยนคลาสในโปรแกรมเวอร์ชันถัดไปและคุณต้องรองรับทั้งสองรูปแบบของการทำให้เป็นอนุกรม - รูปแบบใหม่และแบบเก่า (เนื่องจากลูกค้าของคุณยังคงใช้ไฟล์ / ฐานข้อมูลเก่าอยู่หรือเชื่อมต่อกับ เซิร์ฟเวอร์ของคุณโดยใช้ผลิตภัณฑ์รุ่นเก่าของคุณ) แต่คุณไม่สามารถใช้ serializator นี้ได้อีกต่อไปเพราะคุณใช้ไฟล์

type.AssemblyQualifiedName

ซึ่งดูเหมือนว่า

TopNamespace.SubNameSpace.ContainingClass+NestedClass, MyAssembly, Version=1.3.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089

ที่มีแอตทริบิวต์แอสเซมบลีของคุณและเวอร์ชัน ...

ตอนนี้ถ้าคุณพยายามเปลี่ยนเวอร์ชันแอสเซมบลีของคุณหรือคุณตัดสินใจที่จะลงนามการดีซีเรียลไลเซชันนี้จะไม่ทำงาน


1

ฉันได้ทำสิ่งที่คล้ายกับสิ่งนี้ สิ่งที่ฉันทำตามปกติคือตรวจสอบให้แน่ใจว่าแอตทริบิวต์การทำให้เป็นอนุกรม XML ทั้งหมดอยู่ในคลาสคอนกรีตและเพียงแค่มีคุณสมบัติในการเรียกคลาสนั้นไปยังคลาสพื้นฐาน (หากจำเป็น) เพื่อดึงข้อมูลที่จะ de / serialized เมื่อ serializer เรียกใช้ คุณสมบัติเหล่านั้น เป็นงานเขียนโค้ดมากกว่าเล็กน้อย แต่ทำงานได้ดีกว่าการพยายามบังคับให้ serializer ทำสิ่งที่ถูกต้อง


1

ดียิ่งขึ้นโดยใช้สัญกรณ์:

[XmlRoot]
public class MyClass {
    public abstract class MyAbstract {} 
    public class MyInherited : MyAbstract {} 
    [XmlArray(), XmlArrayItem(typeof(MyInherited))] 
    public MyAbstract[] Items {get; set; } 
}

2
นี่เป็นสิ่งที่ดีมากถ้าคุณรู้จักชั้นเรียนของคุณเป็นการละลายที่สง่างามที่สุด หากคุณโหลดคลาสใหม่ที่สืบทอดมาจากแหล่งภายนอกคุณจะไม่สามารถใช้งานได้
Vladimir
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.