Random Thoughts of a Scatterbrain.

Of XML Schemas and Object Models

Want to leave a question, comment, or some criticism? Click here!

1. Motivation

Actually, very few .Net developers that I've worked with know what a typed DataSet is let alone how to create one. It's one of those perplexing things that always baffles me as typed DataSets are not a bad way to make the best of the flexibility of DataSets while still offering design time support and compile time error checking (to some degree, of course).

At the core of the typed DataSet is an XML schema that describes the types and structure of the DataSet. While typed DataSets are great, if you ever look at the code, it's quite heavy and not necessarily the most optimized structure for over the wire transport. As even the discussion of typed DataSets narrows down the audience of developers that know how to work with schemas to generate a typed DataSet, the concept of generating entire object models using XML schemas is even less understood. Yes, indeed, the XSD.exe tool that ships with Visual Studio.Net can also be used to generate object classes from the same schemas (although they are a bit clumsy to work with).

You may be asking yourself why bother with schemas when you can just create the classes by hand or some other sort of visual code generation tool. Well, for some people (myself included), schemas seem a very natural way to express object models and classes. I mean, after all, the purpose of a schema is to define what an object (typically an XML document) looks like. But certainly, if that was it, it wouldn't really be worth the effort, now would it? For me, there is an added benefit in that it's very easy to define typed collections in schemas. On top of that, the generated classes come with various XML serialization attributes already marked up for you, which is handy if you plan on using the classes as data transfer objects for your web service.

2. Objective

Using XsdObjectGen.exe (a cousin of xsd.exe), create an example object model for an automobile service deparment. Our object model will be defined in an XML schema and we will use tools to generate the object classes. We'll begin with a simple example and move onto more complex topics like type reference and inheritance. This workshop is not meant as an introduction to XML schemas. More detailed coverage of XML schemas can be found on the official W3C website. A good primer for XML schemas can be found over at xml.com. Although I feel that in general, one does not need a thorough understanding of XML schemas to "get" this workshop.

3. Auxiliary Files/Prerequisites

To follow along, you will need the following:

  • XsdObjectGen.exe. This tool is a more powerful version of xsd.exe insofar as generating object classes goes. It offers way more options and the generated code has a nicer programming interface than the code generated by xsd.exe (typed collecitons vs. typed arrays).
  • Good ol' EditPlus. It's certainly not a requirement; you can also configure the external tools in Visual Studio.Net (in fact, that may be the preference). But I tend to work with non-C# code/markup in EditPlus, almost exclusively. You are also free to use the command line as well. If you'd like to use EditPlus, go ahead and install it now. In addition grab one of the schema syntax files.
  • The zipped project (AcmeAutoService.005.zip). If you'd like, you can follow along by downloading the completed project. It can also serve as a reference point.

4. Configuring EditPlus

If you choose to use EditPlus for this workshop, head over to my "Using EditPlus as a TSQL Editor" workshop for a quick intro to EditPlus and how to set it up for use with external tools.

In EditPlus, I have the XsdObjectGen.exe mapped to a user tool.

The commandline options and switches for the executable can be found in the readme. The first step is to add the path to the executable to the "Command" textbox of the EditPlus user tool configuration pane. In my case, the path is:

C:\Program Files\XSDObjectGenerator\XSDObjectGen.exe

Next, we need to configure the command line switches for the executable.

"$(FilePath)" /n:$(FileNameNoExt) /f:"$(FileDir)/$(FileNameNoExt)" /l:cs

Of course, you are free to customize the parameters as you see fit. As used here, the parameters are:

  • "$(FilePath)" is the token (and first argument) that represents the current working file. Note the double quotes around the token. This is necessary in case the path has spaces in it.
  • /n is the namespace that the generated classes will be under. In this case, I've mapped it to the EditPlus expansion token "$(FileNameNoExt)" which translates to the name of the file minus the extension.
  • /f is the output file name of the generated class file. In this case, I've mapped it to output to the same file directory and the same file name minus the extension (which is specified in the next argument).
  • /l specifies the language that the generated code is in. In this case, we will generate C# code.

There are four other arguments/switches; I recommend experimenting with them to see if they offer any additional functionality for your scenario. For the purposes of this workshop, the above should be just fine.

5. A Simple Example

For our first example, we'll start out with a bare-bones schema that defines a Customer object. We'll start out with a bare schema (you can also use the one autogenerated for you when you add a schema to a project in Visual Studio).

Our barebones XSD schema.
<?xml version="1.0" encoding="utf-8"?>
<xs:schema targetNamespace="http://acmeautoservice.com/ObjectModel.xsd" 
    xmlns="http://acmeautoservice.com/ObjectModel.xsd"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">    

</xs:schema>

The first step is to define what our Customer object will look like. Let's say for now, that we will define the following properties for our Customers:

  • FirstName : String. The customer's first name.
  • LastName : String. The customer's last name.
  • Email : String. The customer's email address.
  • DayContactNumber : String. The customer's day time contact number.
  • EveningContactNumber : String. The customer's evening contact number.
  • NumberOfPriorVisits : Int32. The number of prior visits by the customer.
  • SendServiceReminders : Boolean. A Boolean value indicating whether the customer wants to receive automated service reminders.

While XSD may seem intimidating, it is, in fact, quite a simple and very straight forward (at least in the context of what we need it for). Our Customer object, when translated to the schema, looks very literal.

Schema with Customer type defined.
<?xml version="1.0" encoding="utf-8"?>
<xs:schema targetNamespace="http://acmeautoservice.com/ObjectModel.xsd" 
    xmlns="http://acmeautoservice.com/ObjectModel.xsd"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">    
    
    <!--/// Customer type ///-->
    <xs:complexType name="Customer">
        <xs:sequence>
            <xs:element name="FirstName" type="xs:string"/>
            <xs:element name="LastName" type="xs:string"/>
            <xs:element name="Email" type="xs:string"/>
            <xs:element name="DayContactNumber" type="xs:string"/>
            <xs:element name="EveningContactNumber" type="xs:string"/>
            <xs:element name="NumberOfPriorVisits" type="xs:int"/>
            <xs:element name="SendServiceReminders" type="xs:boolean"/>
        </xs:sequence>
    </xs:complexType>
</xs:schema>

You can find out more about how XSD data types are mapped to .Net data types by checking out the .Net DataSets documentation. Although note that the documentation isn't complete (as XsdObjectGen.exe will translate xs:anyType to Object).

Now if you run the tool, the class files that correspond to this schema definition will be automatically generated for you (I adjusted the line feeds and indentation for better presentation).

Generated C# code
// Copyright 2004, Microsoft Corporation
// Sample Code - Use restricted to terms of use defined in the accompanying 
// license agreement (EULA.doc)

//--------------------------------------------------------------
// Autogenerated by XSDObjectGen version 1.3.6.0
// Schema file: ObjectModel.xsd
// Creation Date: 2/8/2006 9:27:25 PM
//--------------------------------------------------------------

using System;
using System.Xml.Serialization;
using System.Collections;
using System.Xml.Schema;
using System.ComponentModel;

namespace ObjectModel
{

    public struct Declarations
    {
        public const string SchemaVersion = 
            "http://acmeautoservice.com/ObjectModel.xsd";
    }

    [XmlType(TypeName="Customer"),XmlRoot,Serializable]    
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public class Customer
    {

        [XmlElement(ElementName="FirstName",
            IsNullable=false,DataType="string")]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public string __FirstName;
        
        [XmlIgnore]
        public string FirstName
        { 
            get return __FirstName}
            set { __FirstName = value; }
        }

        [XmlElement(ElementName="LastName",
            IsNullable=false,DataType="string")]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public string __LastName;
        
        [XmlIgnore]
        public string LastName
        { 
            get return __LastName}
            set { __LastName = value; }
        }

        [XmlElement(ElementName="Email",
            IsNullable=false,DataType="string")]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public string __Email;
        
        [XmlIgnore]
        public string Email
        { 
            get return __Email}
            set { __Email = value; }
        }

        [XmlElement(ElementName="DayContactNumber",
            IsNullable=false,DataType="string")]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public string __DayContactNumber;
        
        [XmlIgnore]
        public string DayContactNumber
        { 
            get return __DayContactNumber}
            set { __DayContactNumber = value; }
        }

        [XmlElement(ElementName="EveningContactNumber",
            IsNullable=false,DataType="string")]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public string __EveningContactNumber;
        
        [XmlIgnore]
        public string EveningContactNumber
        { 
            get return __EveningContactNumber}
            set { __EveningContactNumber = value; }
        }

        [XmlElement(ElementName="NumberOfPriorVisits",
            IsNullable=false,DataType="int")]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public int __NumberOfPriorVisits;
        
        [XmlIgnore]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public bool __NumberOfPriorVisitsSpecified;
        
        [XmlIgnore]
        public int NumberOfPriorVisits
        { 
            get return __NumberOfPriorVisits}
            set { __NumberOfPriorVisits = value; 
                    __NumberOfPriorVisitsSpecified = true; 
                }
        }

        [XmlElement(ElementName="SendServiceReminders",
            IsNullable=false,DataType="boolean")]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public bool __SendServiceReminders;
        
        [XmlIgnore]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public bool __SendServiceRemindersSpecified;
        
        [XmlIgnore]
        public bool SendServiceReminders
        { 
            get return __SendServiceReminders}
            set { __SendServiceReminders = value; 
                    __SendServiceRemindersSpecified = true; 
                }
        }

        public Customer()
        {
        }
    }
}
Generated VB.Net code.
' Copyright 2004, Microsoft Corporation
' Sample Code - Use restricted to terms of use defined in the accompanying 
' license agreement (EULA.doc)

'--------------------------------------------------------------
' Autogenerated by XSDObjectGen version 1.3.6.0
' Schema file: ObjectModel.xsd
' Creation Date: 2/8/2006 9:20:17 PM
'--------------------------------------------------------------

Imports System
Imports System.Xml.Serialization
Imports System.Collections
Imports System.Xml.Schema
Imports System.ComponentModel

Namespace ObjectModel

    Public Module Declarations
        Public Const SchemaVersion As String = _
            "http://acmeautoservice.com/ObjectModel.xsd"
    End Module

    '--------------------------------------------------
    'Customer type
    '--------------------------------------------------
    <XmlType(TypeName:="Customer"),XmlRoot,Serializable, _    
        EditorBrowsable(EditorBrowsableState.Advanced)> _
    Public Class Customer

        '************ FirstName element ************
        <XmlElement(ElementName:="FirstName", _
            IsNullable:=false,DataType:="string"), _
        EditorBrowsable(EditorBrowsableState.Advanced)> _
        Public __FirstName As String
        
        <XmlIgnore> _
        Public Property FirstName As String
            Get
                FirstName __FirstName
            End Get
            Set(Value As String)
                __FirstName Value
            End Set
        End Property

        '************ LastName element ************
        <XmlElement(ElementName:="LastName", _
            IsNullable:=false,DataType:="string"), _
        EditorBrowsable(EditorBrowsableState.Advanced)> _
        Public __LastName As String
        
        <XmlIgnore> _
        Public Property LastName As String
            Get
                LastName __LastName
            End Get
            Set(Value As String)
                __LastName Value
            End Set
        End Property

        '************ Email element ************
        <XmlElement(ElementName:="Email", _
            IsNullable:=false,DataType:="string"), _
        EditorBrowsable(EditorBrowsableState.Advanced)> _
        Public __Email As String
        
        <XmlIgnore> _
        Public Property Email As String
            Get
                Email __Email
            End Get
            Set(Value As String)
                __Email Value
            End Set
        End Property

        '************ DayContactNumber element ************
        <XmlElement(ElementName:="DayContactNumber", _
            IsNullable:=false,DataType:="string"), _
        EditorBrowsable(EditorBrowsableState.Advanced)> _
        Public __DayContactNumber As String
        
        <XmlIgnore> _
        Public Property DayContactNumber As String
            Get
                DayContactNumber __DayContactNumber
            End Get
            Set(Value As String)
                __DayContactNumber Value
            End Set
        End Property

        '************ EveningContactNumber element ************
        <XmlElement(ElementName:="EveningContactNumber", _
            IsNullable:=false,DataType:="string"), _
        EditorBrowsable(EditorBrowsableState.Advanced)> _
        Public __EveningContactNumber As String
        
        <XmlIgnore> _
        Public Property EveningContactNumber As String
            Get
                EveningContactNumber __EveningContactNumber
            End Get
            Set(Value As String)
                __EveningContactNumber Value
            End Set
        End Property

        '************ NumberOfPriorVisits element ************
        <XmlElement(ElementName:="NumberOfPriorVisits", _
            IsNullable:=False,DataType:="int"), _
        EditorBrowsable(EditorBrowsableState.Advanced)> _
        Public __NumberOfPriorVisits As Integer
        
        <XmlIgnore, _
        EditorBrowsable(EditorBrowsableState.Advanced)> _
        Public __NumberOfPriorVisitsSpecified As Boolean
        
        <XmlIgnore> _
        Public Property NumberOfPriorVisits As Integer
            Get
                NumberOfPriorVisits __NumberOfPriorVisits
            End Get
            Set(Value As Integer)
                __NumberOfPriorVisits Value
                __NumberOfPriorVisitsSpecified = True
            End Set
        End Property

        '************ SendServiceReminders element ************
        <XmlElement(ElementName:="SendServiceReminders", _
            IsNullable:=False,DataType:="boolean"), _
        EditorBrowsable(EditorBrowsableState.Advanced)> _
        Public __SendServiceReminders As Boolean
        
        <XmlIgnore, _
        EditorBrowsable(EditorBrowsableState.Advanced)> _
        Public __SendServiceRemindersSpecified As Boolean
        
        <XmlIgnore> _
        Public Property SendServiceReminders As Boolean
            Get
                SendServiceReminders __SendServiceReminders
            End Get
            Set(Value As Boolean)
                __SendServiceReminders Value
                __SendServiceRemindersSpecified = True
            End Set
        End Property

        '************ Constructor ************
        Public Sub New()
        End Sub
    End Class
End Namespace

A quick glance at the generated code shows that the generator has spit out a set of fields and a set of properties for each of the fields. This is by design, apparently, due to the way reflection works in .Net with regards to properties. From the readme file:

Xml*Attribute is what maps the classes and fields to their xml counterpart. As of .NET v1.1, Xml*Attribute can not be applied to a private field or property, but only to a public field or property. Originally the plan was to only expose public properties, and have private __FieldName members within the class to store values. Unfortunately when applying the Xml*Attribute to public properties, the resultant XML generated cannot be guaranteed to be in the proper sequence due to the way .NET reflection works. Therefore Xml*Attributes are only applied to hidden public fields, not properties.

The programmer doesnt access the fields directly, which has the advantage of allowing the code to perform lazy instantiation of objects and value validation. Since the materialization of complex hierarchies is accomplished by the property get logic, always programming against properties is recommended. In all cases, therefore, a pairing of a public property and a hidden public field is used to control programmer and serializer access to data (respectively). The public field will have a __FieldName naming convention and should only be used to check for a null value. It turns out having the __FieldName members as public fields is advantageous to check for null in end-user code, without causing a child object to be created.

Note that in each case, the backer field is the one that's actually mapped to an XML element; all of the properties are marked with the XmlIgnore attribute which tells the serializer to ignore the property when serializing. Aside from this, also note that there are fields that we didn't define, the __SendServiceRemindersSpecified and the __NumberOfPriorVisitsSpecified field. What these two have in common is that they are both non-nullable types in .Net. For any non-nullable types in .Net, a Specified field will be generated which is used to indicate whether the value was set by the user or not.

6. Serialization/Deserialization in Action

Now that we've got our basic class set up, we can go ahead and test it out with an XmlSerializer. If you're not familiar with the concept of serialization/deserialization, simply put, it's the process by which a runtime object graph is moved into (serialize) or out of (deserialize) a stream. In our case, we will be moving our run time objects into and out of a TextStream (for more in depth info on serialization, check out Andrew Troelson's excellent Pro C# 2500 and the .Net 2.0 Platform). Copy/paste the code below or download the project from here.

Sample console serialization/deserialization.
using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using ObjectModel;

namespace AcmeAutoService.Console {
    /// <summary>
    /// Sample usage of our object model.
    /// </summary>
    class SimpleApp {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main(string[] args) {
            SimpleApp app = new SimpleApp();
            app.Run();
        }

        public void Run() {
            // Create a customer object and populate some 
            // properties
            Customer customer = new Customer();
            customer.FirstName "Charles";
            customer.LastName "Chen";
            customer.DayContactNumber "7325551234";
            customer.EveningContactNumber "7325552345";
            customer.Email "cchen@charliedigital.com";
            customer.NumberOfPriorVisits 5;
            customer.SendServiceReminders = true;

            // Create an XML serializer to serialize our 
            // customer to XML.
            XmlSerializer serializer = 
                new XmlSerializer(typeof(Customer));

            // The StringWriter we'll be serializing to.
            StringWriter writer = 
                new StringWriter();

            // Serialize the object to the StringWriter.
            serializer.Serialize(writer, customer);

            String seralizedCustomer writer.ToString();

            // Write to console.
            System.Console.WriteLine(seralizedCustomer);

            System.Console.WriteLine("Press any key to deserialize...");
            System.Console.WriteLine("_______________________________________");
            System.Console.ReadLine();

            Customer customerCopy = 
                (Customer) serializer.Deserialize(
                    new StringReader(seralizedCustomer));

            System.Console.WriteLine("FirstName: {0}", customerCopy.FirstName);
            System.Console.WriteLine("LastName: {0}", customerCopy.LastName);
            System.Console.WriteLine("Email: {0}", customerCopy.Email);
            System.Console.WriteLine("_______________________________________");

            // Exit.
            System.Environment.Exit(0);
        }
    }
}

If you run this code, you should see the following results:

An interesting aspect of this is that it offers one way to implement ICloneable (albeit probably less performant than say using a BinaryFormatter) as customerCopy is an exact copy, but not reference to, the original customer. [AcmeAutoService.001.zip]

7. Advanced Concepts : Type Reference

Beyond creating simple objects, XML schemas also allow type references. So one complexType can have an element that refers to another complexType, just like any of the CLR languages. To demonstrate this, we'll add three new types to our schema. The first type, Vehicle will represent a customer's vehicle. The second type will be an Enum type that indicates when the standard colors that are sold.

New schema with Vehicle and Color types
<?xml version="1.0" encoding="utf-8"?>
<xs:schema targetNamespace="http://acmeautoservice.com/ObjectModel.xsd"
    xmlns="http://acmeautoservice.com/ObjectModel.xsd"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">

    <!--/// Customer type ///-->
    <xs:complexType name="Customer">
        <xs:sequence>
            <xs:element name="FirstName" type="xs:string"/>
            <xs:element name="LastName" type="xs:string"/>
            <xs:element name="Email" type="xs:string"/>
            <xs:element name="DayContactNumber" type="xs:string"/>
            <xs:element name="EveningContactNumber" type="xs:string"/>
            <xs:element name="NumberOfPriorVisits" type="xs:int"/>
            <xs:element name="SendServiceReminders" type="xs:boolean"/>
            <xs:element name="Vehicle" type="Vehicle"/>
        </xs:sequence>
    </xs:complexType>

    <!--/// Car type ///-->
    <xs:complexType name="Vehicle">
        <xs:sequence>
            <xs:element name="ModelName" type="xs:string"/>
            <xs:element name="Color" type="Color"/>
            <xs:element name="Year" type="xs:int"/>
        </xs:sequence>
    </xs:complexType>

    <!--/// Our color enumeration values ///-->
    <xs:simpleType name="Color">
        <xs:restriction base="xs:string">
            <xs:enumeration value="Black" />
            <xs:enumeration value="Silver" />
            <xs:enumeration value="White" />
            <xs:enumeration value="Red" />
            <xs:enumeration value="Blue" />
        </xs:restriction>
    </xs:simpleType>
</xs:schema>

Note that, unlike with the reference types, the Color type is a value type and represented with a simpleType in the schema (as opposed to complexType). Generating the code again yields:

Newly generated classes and Enum
[Serializable]
public enum Color
{
    [XmlEnum(Name="Black")] Black,
    [XmlEnum(Name="Silver")] Silver,
    [XmlEnum(Name="White")] White,
    [XmlEnum(Name="Red")] Red,
    [XmlEnum(Name="Blue")] Blue
}

[XmlType(TypeName="Customer"),XmlRoot,Serializable]
[EditorBrowsable(EditorBrowsableState.Advanced)]
public class Customer
{

    // Other fields and properties excluded...

    [XmlElement(Type=typeof(Vehicle),ElementName="Vehicle",IsNullable=false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public Vehicle __Vehicle;
    
    [XmlIgnore]
    public Vehicle Vehicle
    {
        get
        {
            if (__Vehicle == null) __Vehicle = new Vehicle();        
            return __Vehicle;
        }
        set {__Vehicle = value;}
    }
}

[XmlType(TypeName="Vehicle"),XmlRoot,Serializable]
[EditorBrowsable(EditorBrowsableState.Advanced)]
public class Vehicle
{

    [XmlElement(ElementName="ModelName",IsNullable=false,DataType="string")]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public string __ModelName;
    
    [XmlIgnore]
    public string ModelName
    { 
        get return __ModelName}
        set { __ModelName = value; }
    }

    [XmlElement(ElementName="Color",IsNullable=false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public Color __Color;
    
    [XmlIgnore]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public bool __ColorSpecified;
    
    [XmlIgnore]
    public Color Color
    { 
        get return __Color}
        set { __Color = value; __ColorSpecified = true; }
    }

    [XmlElement(ElementName="Year",IsNullable=false,DataType="int")]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public int __Year;
    
    [XmlIgnore]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public bool __YearSpecified;
    
    [XmlIgnore]
    public int Year
    { 
        get return __Year}
        set { __Year = value; __YearSpecified = true; }
    }

    public Vehicle()
    {
        ModelName = string.Empty;
        __ColorSpecified = true;
        __YearSpecified = true;
    }
}

If we modify our code a bit (just create a Vehicle object and set the property on the customer) and run it again, this is the output that we get: [AcmeAutoService.002.zip]

8. Advanced Concepts : Collections

As I mentioned previously, XsdObjectGen.exe will also create typed collections (which inherit from ArrayList, which is good, because you can use BinarySearch with a custom IComparer) for you. Doing so is as simple as adding one new attribute to our Customer object.

Creating typed collections
<?xml version="1.0" encoding="utf-8"?>
<xs:schema targetNamespace="http://acmeautoservice.com/ObjectModel.xsd"
    xmlns="http://acmeautoservice.com/ObjectModel.xsd"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">

    <!--/// Customer type ///-->
    <xs:complexType name="Customer">
        <xs:sequence>
            <xs:element name="FirstName" type="xs:string"/>
            <xs:element name="LastName" type="xs:string"/>
            <xs:element name="Email" type="xs:string"/>
            <xs:element name="DayContactNumber" type="xs:string"/>
            <xs:element name="EveningContactNumber" type="xs:string"/>
            <xs:element name="NumberOfPriorVisits" type="xs:int"/>
            <xs:element name="SendServiceReminders" type="xs:boolean"/>
            <xs:element name="Vehicle" type="Vehicle" maxOccurs="unbounded"/>
        </xs:sequence>
    </xs:complexType>

    <!--/// Other types omitted... ///-->

</xs:schema>
Newly generated code (with portions omitted)
[Serializable]
[EditorBrowsable(EditorBrowsableState.Advanced)]
public class VehicleCollection : ArrayList
{
    public Vehicle Add(Vehicle obj)
    {
        base.Add(obj);
        return obj;
    }

    public Vehicle Add()
    {
        return Add(new Vehicle());
    }

    public void Insert(int index, Vehicle obj)
    {
        base.Insert(index, obj);
    }

    public void Remove(Vehicle obj)
    {
        base.Remove(obj);
    }

    new public Vehicle this[int index]
    {
        get return (Vehicle) base[index]}
        set base[index] = value; }
    }
}

[XmlType(TypeName="Customer"),XmlRoot,Serializable]
[EditorBrowsable(EditorBrowsableState.Advanced)]
public class Customer
{
    // Other properties and fields omitted

    [XmlElement(Type=typeof(Vehicle),ElementName="Vehicle",IsNullable=false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public VehicleCollection __VehicleCollection;
    
    [XmlIgnore]
    public VehicleCollection VehicleCollection
    {
        get
        {
            if (__VehicleCollection == null) __VehicleCollection = 
                new VehicleCollection();
            return __VehicleCollection;
        }
        set {__VehicleCollection = value;}
    }

    public Customer()
    {
        FirstName = string.Empty;
        LastName = string.Empty;
        Email = string.Empty;
        DayContactNumber = string.Empty;
        EveningContactNumber = string.Empty;
        __NumberOfPriorVisitsSpecified = true;
        __SendServiceRemindersSpecified = true;
    }
}

// Other classes omitted

Note that in this case, we already had a reference to the type to create the collection with. What if you just want to create a standalone type with no references? Well, the only way that I've found to do this is to create a new complex type (I'll typically name it the plural form of the singular object) and add a single element to it with the maxOccurs set to unbounded. Alternatively, you can use syntax like so:

Schema layout to create standalone collection
<?xml version="1.0" encoding="utf-8"?>
<xs:schema targetNamespace="http://acmeautoservice.com/ObjectModel.xsd"
    xmlns="http://acmeautoservice.com/ObjectModel.xsd"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">

    <!--/// 
        Standalone Vehicle collection type (illustration purposes only) 
    ///-->
    <xs:complexType name="Vehicles">
        <xs:sequence>
            <xs:element name="Vehicle">
                <xs:complexType>
                    <xs:sequence>
                        <xs:element name="ModelName" type="xs:string"/>
                        <xs:element name="Color" type="Color"/>
                        <xs:element name="Year" type="xs:int"/>
                    </xs:sequence>
                </xs:complexType>
            </xs:element>
        <xs:sequence>
    </xs:complexType>

</xs:schema>

Note that using this syntax, you cannot directly refer to the Vehicle type from your schema. If you need to reference the object, then simply use two complex types (one for the object, one for the collection). Unfortunately, using either syntax also generates a weird Vehicles class which acts as a wrapper around the typed collection. It's not all bad, I guess, as it allows for lazy initialization of the collection and, in certain situations, you may need a wrapper around your collection (we'll get to this in one sec.).

With our updated code, if I add some vehicles to Customer, I get the following output:

If we had used a wrapper (to create a standalone collection), this is the output we would get:

For the remainder of this workshop, we'll assume that we're not working with the standalone collection type. [AcmeAutoService.003.zip]

9. Advanced Concepts : Inheritance

Beyond simply creating objects, collections, and Enums, we can also create hierarchies. Yup, that's right, you can also specify inheritance using the schema. For this exercise, we'll be modifying our Vehicle type and adding two sub types: Sedans, Coupes, and SUVs. Let's assume that AWD is an option that's only available on SUVs and Coupes may have either 2 or 4 seats (we assume that all other Vehicles have 4 seats).

To accomplish this, we'll be making use of the complexContent and extension elements. The modified schema looks like so:

Modifed schema with inheriting types
<?xml version="1.0" encoding="utf-8"?>
<xs:schema targetNamespace="http://acmeautoservice.com/ObjectModel.xsd"
    xmlns="http://acmeautoservice.com/ObjectModel.xsd"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">

    <!--/// Customer type ///-->
    <xs:complexType name="Customer">
        <xs:sequence>
            <xs:element name="FirstName" type="xs:string"/>
            <xs:element name="LastName" type="xs:string"/>
            <xs:element name="Email" type="xs:string"/>
            <xs:element name="DayContactNumber" type="xs:string"/>
            <xs:element name="EveningContactNumber" type="xs:string"/>
            <xs:element name="NumberOfPriorVisits" type="xs:int"/>
            <xs:element name="SendServiceReminders" type="xs:boolean"/>
            <xs:element name="Vehicle" type="Vehicle" maxOccurs="unbounded"/>
        </xs:sequence>
    </xs:complexType>

    <!--/// Vehicle type ///-->
    <xs:complexType name="Vehicle" abstract="true">
        <xs:sequence>
            <xs:element name="ModelName" type="xs:string"/>
            <xs:element name="Color" type="Color"/>
            <xs:element name="Year" type="xs:int"/>
            <xs:element name="Doors" type="xs:int"/>
        </xs:sequence>
    </xs:complexType>

    <!--/// Sedan type ///-->
    <xs:complexType name="Sedan">
        <xs:complexContent>
            <xs:extension base="Vehicle" />
        </xs:complexContent>            
    </xs:complexType>  

    <!--/// Coupe type ///-->
    <xs:complexType name="Coupe">
        <xs:complexContent>
            <xs:extension base="Vehicle">       
                <xs:sequence>
                    <xs:element name="NumberOfSeats" 
                        type="xs:int" default="2"/>
                </xs:sequence>
            </xs:extension>
        </xs:complexContent>            
    </xs:complexType> 
    
    <!--/// SUV type ///-->
    <xs:complexType name="SUV">
        <xs:complexContent>
            <xs:extension base="Vehicle">       
                <xs:sequence>
                    <xs:element name="IsAllWheelDrive" 
                        type="xs:boolean" default="false"/>
                </xs:sequence>
            </xs:extension>
        </xs:complexContent>            
    </xs:complexType>

    <!--/// Our color enumeration values ///-->
    <xs:simpleType name="Color">
        <xs:restriction base="xs:string">
            <xs:enumeration value="Black" />
            <xs:enumeration value="Silver" />
            <xs:enumeration value="White" />
            <xs:enumeration value="Red" />
            <xs:enumeration value="Blue" />
        </xs:restriction>
    </xs:simpleType>
</xs:schema>

When the new code is generated, we can see that the Vehicle class is now abstract. Also notice that the Customer type now includes additional attributes.

Now with inheritance (some code omitted)
// Code omitted...

[XmlType(TypeName="Customer"),XmlRoot,Serializable]
[EditorBrowsable(EditorBrowsableState.Advanced)]
[XmlInclude(typeof(SUV))]
[XmlInclude(typeof(Coupe))]
[XmlInclude(typeof(Sedan))]
public class Customer
{
    // Code omitted...
}

[XmlType(TypeName="Vehicle"),XmlRoot,Serializable]
[EditorBrowsable(EditorBrowsableState.Advanced)]
public abstract class Vehicle
{

    [XmlElement(ElementName="ModelName",IsNullable=false,DataType="string")]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public string __ModelName;
    
    [XmlIgnore]
    public string ModelName
    { 
        get return __ModelName}
        set { __ModelName = value; }
    }

    [XmlElement(ElementName="Color",IsNullable=false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public Color __Color;
    
    [XmlIgnore]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public bool __ColorSpecified;
    
    [XmlIgnore]
    public Color Color
    { 
        get return __Color}
        set { __Color = value; __ColorSpecified = true; }
    }

    [XmlElement(ElementName="Year",IsNullable=false,DataType="int")]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public int __Year;
    
    [XmlIgnore]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public bool __YearSpecified;
    
    [XmlIgnore]
    public int Year
    { 
        get return __Year}
        set { __Year = value; __YearSpecified = true; }
    }

    [XmlElement(ElementName="Doors",IsNullable=false,DataType="int")]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public int __Doors;
    
    [XmlIgnore]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public bool __DoorsSpecified;
    
    [XmlIgnore]
    public int Doors
    { 
        get return __Doors}
        set { __Doors = value; __DoorsSpecified = true; }
    }

    public Vehicle()
    {
        ModelName = string.Empty;
        __ColorSpecified = true;
        __YearSpecified = true;
        __DoorsSpecified = true;
    }
}

[XmlType(TypeName="SUV"),XmlRoot,Serializable]
[EditorBrowsable(EditorBrowsableState.Always)]
public class SUV : Vehicle
{

    [XmlElement(ElementName="IsAllWheelDrive",IsNullable=false,DataType="boolean")]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public bool __IsAllWheelDrive;
    
    [XmlIgnore]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public bool __IsAllWheelDriveSpecified;
    
    [XmlIgnore]
    public bool IsAllWheelDrive
    { 
        get return __IsAllWheelDrive}
        set { __IsAllWheelDrive = value; __IsAllWheelDriveSpecified = true; }
    }

    public SUV() : base()
    {
        IsAllWheelDrive = false;
    }
}

[XmlType(TypeName="Coupe"),XmlRoot,Serializable]
[EditorBrowsable(EditorBrowsableState.Always)]
public class Coupe : Vehicle
{

    [XmlElement(ElementName="NumberOfSeats",IsNullable=false,DataType="int")]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public int __NumberOfSeats;
    
    [XmlIgnore]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public bool __NumberOfSeatsSpecified;
    
    [XmlIgnore]
    public int NumberOfSeats
    { 
        get return __NumberOfSeats}
        set { __NumberOfSeats = value; __NumberOfSeatsSpecified = true; }
    }

    public Coupe() : base()
    {
        NumberOfSeats 2;
    }
}


[XmlType(TypeName="Sedan"),XmlRoot,Serializable]
[EditorBrowsable(EditorBrowsableState.Always)]
public class Sedan : Vehicle
{

    public Sedan() : base()
    {
    }
}

Accordingly, we also have to modify our code since we can no longer create instances of Vehicle since it's an abstract type now.

Modified code block (portion