[ Team LiB ] |
9.3 XML SerializationXML serialization addresses the requirements mentioned in the previous section. In XML serialization, no assumptions are made about the program that produces the XML or the one that reads it. You may lose some precise CLR type detail, but interoperation with disparate applications is better than with runtime serialization. In order to completely divorce the XML from the CLR, the serialization uses XML Schema datatypes. I mentioned in Table 8-2 that not every XSD datatype has a corresponding CLR datatype. Although each XSD datatype mapped to exactly one CLR datatype, many CLR datatypes could, potentially, each be represented by a number of different XSD datatypes. The essential point to remember when differentiating runtime serialization from XML serialization is that, in the runtime serialization, the object being serialized actively controls the format of the serialization, whereas in the XML serialization, the object is passively serialized. The XmlSerializer type contains the methods Serialize( ) and Deserialize( ). Any object can be serialized to XML and, by default, all fields in an object are serialized as elements. Certain attributes can also be used to decorate existing classes and methods. Table 9-2 shows a complete listing of attributes that affect XML serialization.
With these attributes, you can take an arbitrary C# type and tell XmlSerializer exactly how you would like to serialize it to XML. Example 9-7 shows a new XML format for an instance of the personnel records from Example 9-4. Example 9-7. Angus Hardware personnel records in XML<?xml version="1.0"?> <personnel> <employee firstname="Niel" middleinitial="M" lastname="Bornstein" hiredate="2001-01-01T00:00:00.0000000-05:00"> <addresses> <address type="Home"> <street>999 Wilford Trace</street> <city>Atlanta</city> <state>Georgia</state> <zip>30037</zip> </address> </addresses> </employee> </personnel> If you were to just serialize a Personnel object to XML, all the data would appear in elements as shown in Example 9-8. Example 9-8. XML serialized without attributes<?xml version="1.0"?> <Personnel xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/ 2001/XMLSchema-instance"> <Employees> <Employee> <FirstName>Niel</FirstName> <MiddleInitial>M</MiddleInitial> <LastName>Bornstein</LastName> <Addresses> <Address> <AddressType>Home</AddressType> <Street> <string>999 Wilford Trace</string> </Street> <City>Atlanta</City> <State>GA</State> <Zip>30037</Zip> </Address> </Addresses> <HireDate>2001-01-01T00:00:00.0000000-05:00</HireDate> </Employee> </Employees> </Personnel> That's fine, if you want all your data in elements, but some people prefer a healthy mix of elements and attributes; this element-centric output does not match the format in Example 9-7. In addition, the element names don't match the format you want. To generate the XML you want, repeat the code from Example 9-4, with the addition of attributes to control the serialization. Let me step through the changes in each class. First, AddressType doesn't need to change at all: public enum AddressType { Home, Office, Billing, Shipping, Mailing, Day, Evening, FAX } If you'll look again at Example 9-7, you'll see that each state is actually listed by its full name, not the abbreviation as listed in the State enumeration. Here I've added an XmlEnumAttribute for each state name. Note that I've skipped some in the interest of space: public enum State { [XmlEnum(Name="Alaska")] AK, [XmlEnum(Name="Alabama")] AL, [XmlEnum(Name="Arkansas")] AR, [XmlEnum(Name="Arizona")] AZ, // ... [XmlEnum(Name="Washington")] WA, [XmlEnum(Name="Wisconsin")] WI, [XmlEnum(Name="West Virginia")] WV, [XmlEnum(Name="Wyoming")] WY } The Address class has one attribute, type, and four elements. Here I've added XmlAttributeAttribute and XmlElementAttribute, as appropriate. The AttributeName and ElementName fields of each attribute are used to set the names of the XML attributes and elements, respectively: public class Address { [XmlAttribute(AttributeName="type")] public AddressType AddressType; [XmlElement(ElementName="street")] public string[ ] Street; [XmlElement(ElementName="city")] public string City; [XmlElement(ElementName="state")] public State State; [XmlElement(ElementName="zip")] public string Zip; } Similar to Address, the TelephoneNumber class has one attribute and three elements. Again, I've decorated each member with the appropriate attribute. Note also that here, as in Address, I've set the names of the attributes and elements to match the ones in the XML; that is, they all start with lowercase letters: public class TelephoneNumber { [XmlAttribute(AttributeName="type")] public AddressType AddressType; [XmlElement(ElementName="areacode")] public string AreaCode; [XmlElement(ElementName="exchange")] public string Exchange; [XmlElement(ElementName="number")] public string Number; } Now we come to the meat of the personnel record, the Employee. This class has three attributes: firstname, middleinitial, and lastname, which I've treated with the appropriate attribute. However, the Employee class also has two additional elements, addresses and telephones. These two elements actually contain nested arrays of elements, so I've used the XmlArray and XmlArrayItem attributes to help the serializer figure out what to do with the XML elements it reads: public class Employee { [XmlAttribute(AttributeName="firstname")] public string FirstName; [XmlAttribute(AttributeName="middleinitial")] public string MiddleInitial; [XmlAttribute(AttributeName="lastname")] public string LastName; [XmlArray(ElementName="addresses")] [XmlArrayItem(ElementName="address")] public Address [ ] Addresses; [XmlArray(ElementName="telephones")] [XmlArrayItem(ElementName="telephone")] public TelephoneNumber [ ] TelephoneNumbers; [XmlAttribute(AttributeName="hiredate")] public DateTime HireDate; } Here's the document element, personnel, which is decorated with XmlRootAttribute. Although the Employees member is an array of Employee objects, it is not a nested array, like addresses and telephones. By adding the XmlElement attribute directly to the member, the XmlSerializer knows that this member is to be serialized as an array of employee elements, without a separate top-level element: [XmlRoot(ElementName="personnel")] public class Personnel { [XmlElement(ElementName="employee")] public Employee [ ] Employees; } Finally, I've made some changes to the Serializer class, which I introduced in Example 9-5. Serializer's Main( ) method still uses the CreatePersonnel( ) to create some personnel records, but it then instantiates an XmlSerializer to deserialize the objects it created back out to a file: public class Serializer { public static void Main(string [ ] args) { Personnel personnel = CreatePersonnel( ); XmlSerializer serializer = new XmlSerializer(typeof(Personnel)); using (FileStream stream = File.OpenWrite("Personnel.xml")) { serializer.Serialize(stream,personnel); } } } Unlike the SoapFormatter and BinaryFormatter, the XmlSerializer constructor takes the Type of the object being serialized as a parameter. This is because, unlike the formatters, the serializer is actually created specifically to handle one particular type of object.
Deserializing an object from XML is as simple as calling the XmlSerializer's Deserialize( ) method: XmlSerializer serializer = new XmlSerializer(typeof(Personnel)); using (FileStream stream = File.OpenRead("Personnel.xml")) { personnel = (Personnel)serializer.Deserialize(stream); } This data is being serialized to and deserialized from files, but it could be any Stream. When deserializing from an XmlReader, you can ensure that the data stream is valid for the XmlSerializer instance you're using. The CanDeserialize( ) method takes an XmlReader parameter, and returns a Boolean value indicating whether the XmlReader contains data that can be deserialized by the XmlSerializer. This is convenient, because when you're deserializing data from a source outside of your control, you don't always know what the file contains. The CanDeserialize( ) method can be used to control processing when you're unsure of the XML stream's contents. At runtime, you can override the attributes that affect serialization with the XmlAttributeOverrides class. This class serves as the container for a collection of XmlAttributes instances, each one of which holds the overridden attributes for a particular type. XmlAttributes has a property for each type of XML attribute; for example, the XmlAttributeAttribute can be set with the XmlAttribute property. For those attributes that can exist in multiples, such as XmlElementAttribute, the property returns a collection of those attributes. For example, the XmlElements property returns a XmlElementAttributes collection, to which you can add XmlElementAttribute instances. XmlAttributeOverrides is convenient if you want to serialize an object for which you don't have or can't alter the source code. You can customize the serialization in exactly the same ways as you could by applying the attributes in the source. I've altered the same program we've been using to change the name of the root element from personnel to employees. The new lines are highlighted: Personnel personnel = CreatePersonnel( ); XmlAttributeOverrides overrides = new XmlAttributeOverrides( ); XmlAttributes attributes = new XmlAttributes( ); attributes.XmlRoot = new XmlRootAttribute("employees"); overrides.Add(typeof(Personnel), attributes); XmlSerializer serializer = new XmlSerializer(typeof(Personnel), overrides); using (FileStream stream = File.OpenWrite("Personnel.xml")) { serializer.Serialize(stream,personnel); } |
[ Team LiB ] |