Breaking News

Writing a Class/Struct to be XML Serialized and Creating a Class from a Schema


f you read my last article ( Introduction to XML Serialization in .NET ), you may have been left wanting--wanting to know and do more with serialization. I know I was. I was left wanting to describe more than just a simple introduction to serialization in .NET, but I quickly realized that the topic was going to be lengthy. I didn't want you to have to scroll through pages and pages of text just to get to the parts you really care about--the code. As such, I decided to break up the topic into different parts. The last part was meant to be an introduction; this is meant to be continuation and further expansion of that introduction. This article is going to describe how to go about creating a class or structure that can be serialized using XML. It will also describe how to use existing schemas to generate class files using the xsd.exe tool which is included with Visual Studio.

Creating the Class or Structure

In keeping with the grocery theme that I introduced in my last article, here is the Groceries object:

C# ]
public class Groceries
{
    private List _groceryList;

    public Groceries()
    {
        this._groceryList = new List();
        this.Budget = 100M;
    }

    public List GroceryList
    {
        get { return this._groceryList; }
        set { this._groceryList = value; }
    }

    public decimal Budget { get; set; }
}
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:


VB ]
Public Class Groceries
    Private _groceryList As List(Of GroceryItem)
    Private _budget As Decimal

    Public Sub New()
        Me._groceryList = New List(Of GroceryItem)
        Me._budget = 100D
    End Sub

    Public Property GroceryList() As List(Of GroceryItem)
        Get
            Return Me._groceryList
        End Get
        Set(ByVal value As List(Of GroceryItem))
            Me._groceryList = value
        End Set
    End Property

    Public Property Budget() As Decimal
        Get
            Return Me._budget
        End Get
        Set(ByVal value As Decimal)
            Me._budget = value
        End Set
    End Property
End Class
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:


Unlike binary serialization in .NET, we do not have to explicitly denote a class or structure as being serializable. As you will notice in the attached projects, this functionality is already enabled within the runtime--I did not need to add any special attributes or configure the class in any special way. All we have to do is make sure that we set up our class or structure in a manner that will only output the data we want to be in the XML file--assuming we don't want everything.

So when we serialize our object, what will actually get output to the file? Is it all the public objects? All the private objects? The methods? The properties? Well, it is all the publicly accessible data. What do we mean by "publicly accessible data"? We are talking about public fields and private fields accessible through public properties. Notice here that I said "public properties," not "public accessors." If you decide to write traditional OO getter and setter methods, your private data accessed by these methods will not be put into the serialized object. Both your "get" and "set" must be public in order for you class to be serialized.

But what if there is data which we do not want to output to a file--maybe a login/password combination or perhaps a computed field which could easily be recalculated by some other fields? The answer is that we can instruct the runtime to ignore such fields by using an attribute:  XmlIgnore. Fields decorated with XmlIgnore will not be output in the resulting XML when the class or structure is serialized.

You may or may not have seen attributes before--you may have seen them and not realized what they were. Attributes are a kind of declarative programming in .NET. They typically add something extra, or they change the behavior of a class or structure or method. For example, if you've ever had to work with the Win32 API via P\Invoke calls, you have had to add the DllImport attribute to the imported function. This lets the runtime know that you are calling a function defined in an external Dll. So for its use here, the attribute in a way changed the default behavior of the function--"don't look in this class file for the function's definition, rather find it in another file/library." For further information on attributes in .NET, please see Attributes (C# and Visual Basic).

Here is an example of not outputting a particular field. Here we have a property within our GroceryItem class and its public SubTotal property points to the "_subTotal" field. But "_subTotal" is the result of multiplying "_count" by "_price". We will ignore this field, usingXmlIgnore, because we will design this class to calculate the SubTotal when either Price or Count are set.

C# ]
[XmlIgnore]
public decimal SubTotal
{
    get { return this._subTotal; }
    set { this._subTotal = value; }
}
1:
2:
3:
4:
5:
6:


VB ] (1)
 _
Public Property SubTotal() As Decimal
    Get
        Return Me._subTotal
    End Get
    Set(ByVal value As Decimal)
        Me._subTotal = value
    End Set
End Property
1:
2:
3:
4:
5:
6:
7:
8:
9:

Unfortunately, unlike binary serialization in .NET, there are no serialization/deserialization events which we can handle to make the SubTotal field be recalculated once the object is deserialized from the file. We must design our class in such a way that the field will be set for us. A simple way, for the purposes of this article, is to have the setters for the Price and Count properties calculate the SubTotal whenever either is set:

C# ]
public int Count
{
    get { return this._count; }
    set
    {
        this._count = value;
        this._subTotal = this._count * this._price;
    }
}

public decimal Price
{
    get { return this._price; }
    set
    {
        this._price = value;
        this._subTotal = this._price * this._count;
    }
}
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:

VB ]
Public Property Count() As Integer
    Get
        Return Me._count
    End Get
    Set(ByVal value As Integer)
        Me._count = value
        Me._subTotal = Me._count * Me._price
    End Set
End Property

Public Property Price() As Decimal
    Get
        Return Me._price
    End Get
    Set(ByVal value As Decimal)
        Me._price = value
        Me._subTotal = Me._price * Me._count
    End Set
End Property
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:

We have now set up our class to be serialized. What next? Well, we set up our serialization code as demonstrated in the last article. I will not re-post that code here, but the link at the top of the page will take you back, should you need a refresher. I will, however, provide the complete projects (C# and VB) for download.

Now, if we set up our application to create an instance of Groceries as such:

C# ]
static void Main(string[] args)
{
    Groceries g = new Groceries();

    g.Budget = 5000;

    g.GroceryList.Add(new GroceryItem("bread", 3, 1.99M));
    g.GroceryList.Add(new GroceryItem("milk", 1, 2.99M));
    g.GroceryList.Add(new GroceryItem("soup", 5, 1.25M));

    SerializeIt(g, "test.xml");
}
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:

VB ]
Sub Main()
    Dim g As New Groceries

    g.Budget = 5000

    g.GroceryList.Add(New GroceryItem("bread", 3, 1.99D))
    g.GroceryList.Add(New GroceryItem("milk", 1, 2.99D))
    g.GroceryList.Add(New GroceryItem("soup", 5, 1.25D))

    SerializeIt(Of Groceries)(g, "test.xml")

End Sub
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:

what should we expect to see in our output? Well...

Results ]


  
    
      bread
      3
      1.99
    
    
      milk
      1
      2.99
    
    
      soup
      5
      1.25
    
  
  5000
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:

Again, you'll notice that SubTotal was not output due to the use of XmlIgnore. Had we not used XmlIgnore, our output would look like:

Results ]


  
    
      bread
      3
      1.99
      5.97
    
    
      milk
      1
      2.99
      2.99
    
    
      soup
      5
      1.25
      6.25
    
  
  5000
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:

We have our SubTotal again!


Making a Class from a Schema

Thus far, we have created an XML document from our data object. This, in a way, gave us an implicit schema for our data.(2) What if we already have a schema which represents our data object(s) and we want to generate a class file from this so we can work with our object in code without the need of parsing the XML manually? Well, Visual Studio ships with a tool called xsd.exe, which is instantly accessible(3) if you open a Visual Studio [YYYY] Command Prompt.(4) Using this tool, you can pass it a schema file and generate classes which represent your data. If you base schema is dependent on other schemas, you pass those as well.

The xsd.exe tool has several options you can use when generating new class files. Of particular interest to you will probably be:

  • the "\c" switch to indicate we want to generate a class file
  • the "\l" (lowercase "L") switch to indicate the language the class file is in (C# or VB, with a default of C#)
  • the "\o" switch to indicate the output directory of the code file
  • the "\n" switch to indicate the namespace for the resulting code file.
There is no option to supply the name of the file, but the tool typically gives the resulting file some default name or names it after the root element and you can of course rename the file to whatever you like. The tool will indicate the name of the resulting file when it completes. Also, leaving off the "\o" switch will output the class file to the current directory.

As an example, if we had the following schema:

Grcoeries.xsd ]


  
  
    
      
      
      
    
  
  
  
    
      
      
    
  
  
    
      
    
  
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:


We could supply the following command line to the xsd.exe utility to generate a class file:

CS ]
    xsd.exe /c /l:CS Groceries.xsd (5)

VB ]
    xsd.exe /c /l:VB Groceries.xsd

We could then use this generated class to deserialize our object(s) found in an XML file (assuming it validates against our schema) back to an object in memory.

Limitations

While being able to serialize objects to XML is a neat feature in .NET, there are some limitations to be aware of. Firstly, there is no solid maintaining of types when you serialize an object to XML. What does that mean? Well, when you output say a generic list of strings, your XML will not reflect that your list is actually a generic list of strings. It will most likely be output as a parent member with several child nodes, and your generated class, if you use xsd.exe against a schema, will most likely have a class which has a member array of child elements. The runtime will typically convert this to your generic list behind the scenes (using list initializers, I imagine).

Other limitations include:

  • The XmlSerializer cannot deserialize arrays of ArrayLists, arrays of generic lists or collections which implement IDictionary.
  • The XmlSerializer cannot serialize enumerations with a type of unsigned long and a value of 9,223,372,036,854,775,807 or greater.
  • Items marked with the Obsolete attribute are not serialized.

Hopefully this has given you a quick introduction to creating classes compatible with XML serialization/deserialization in .NET. Of course, this is not meant to be exhaustive. For instance, there are other control mechanisms you can use to format your output XML document (such as specifying root nodes and namespaces). I am including the full code I wrote the article with to give you a starting point for your own class definitions.

For more information on serialization and XML validation in .NET, have a look at:



Files

C# Solution Demo

VB Solution Demo


Footnotes
1
In VB, you must either put the attribute on the same line as the function signature or put it above the signature and use a line-continuation character ( _ ). This does not apply in C#.
2
That is not to say that we actually have a schema. If we serialized our object, modified the XML by renaming some nodes and then tried to deserialize the XML, we would most likely receive an exception or unexpected data. Always make sure to validate your data to prevent incorrect modifications to the object outside of you code. See the complete project code for an example of applying a schema to the data for validation.
3
"Instantly accessible" because when you use the Visual Studio Command Prompt, the PATH environment variable is modifed for that particular command window. You can get to the xsd.exe tool without using the VSCP, but you will either need to modify your PATH variable or navigate to the directory housing xsd.exe.
4
"YYYY" corresponds to the version of Visual Studio you are running (e.g. 2005, 2008, 2010, etc.).
5
Remember, for the C# version, we could leave the "\l" switch off since the tool defaults to a language of C#.


References
Northrup, Tony and Shawn Wildermuth. .NET Framework 2.0 Application Development Foundation. Redmond: Microsoft Press. 2006.

http://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlserializer.aspx

No comments