only for RuBoard |
Now that you have gone through the basics of XML Schemas, you're ready to work with them. You are going to work with schemas by using the xsd.exe tool, using the System.Xml namespace in .NET, and by using the schema designer in Visual Studio .NET.
The .NET Framework SDK comes with several tools that make working with XML technologies simpler. One such tool is xsd.exe . This tool makes it easy to work with a specific XSD Schema by creating a class hierarchy that matches the schema, which allows you to work with the document as a class structure programmatically rather than as an XML document. You can then serialize the class structure to an XML document. The xsd.exe utility is covered in Chapter 4, and serialization will be covered in depth in Chapter 10.
Look at an example that uses the xsd.exe utility. Save the purchase order schema from Listing 2.5 as sales.xsd because you will use xsd.exe to generate a set of classes based on this schema. Then you are able to use those classes to populate data and create a valid XML file using serialization. Open the Visual Studio .NET command prompt (located at Start, Programs, Microsoft Visual Studio .NET 7.0,Visual Studio .NET Tools). I changed the directory path to the location of the XSD file and entered the following command-line prompt:
xsd sales.xsd /classes /language:vb /outputdir:c:\temp
This prompt yielded a class file with a .vb extension in the c:\temp directory
named sales.vb . Listing 2.6 shows the generated code.
'---------------------------------------------------------------------------- ' <autogenerated> ' This code was generated by a tool. ' Runtime Version: 1.0.2914.16 ' ' Changes to this file may cause incorrect behavior and will be lost if ' the code is regenerated. ' </autogenerated> '---------------------------------------------------------------------------- Option Strict Off Option Explicit On Imports System.Xml.Serialization ' 'This source code was auto-generated by xsd, Version=1.0.2914.16. ' <System.Xml.Serialization.XmlRootAttribute([Namespace]:="http://localhost/chapters/03/ xmlschemas/schemas/sales/Sales.xsd", IsNullable:=false)> Public Class PURCHASEORDER <System.Xml.Serialization.XmlElementAttribute("CUSTOMER", IsNullable:=false)> _ Public CUSTOMER() As customerType <System.Xml.Serialization.XmlArrayAttribute(IsNullable:=false), System.Xml.Serialization.XmlArrayItemAttribute("ITEM", GetType(itemType), IsNullable:=false)> _ Public ORDER() As itemType End Class Public Class customerType <System.Xml.Serialization.XmlElementAttribute(IsNullable:=false)> Public NAME As String <System.Xml.Serialization.XmlElementAttribute(IsNullable:=false)> Public PHONE As String <System.Xml.Serialization.XmlElementAttribute(IsNullable:=false)> Public EMAIL As String <System.Xml.Serialization.XmlElementAttribute(IsNullable:=false)> Public BILLING As addressType End Class Public Class addressType <System.Xml.Serialization.XmlElementAttribute(IsNullable:=false)> Public Name As String <System.Xml.Serialization.XmlElementAttribute(IsNullable:=false)> Public Street1 As String <System.Xml.Serialization.XmlElementAttribute(IsNullable:=false)> Public Street2 As String <System.Xml.Serialization.XmlElementAttribute(IsNullable:=false)> Public City As String <System.Xml.Serialization.XmlElementAttribute(IsNullable:=false)> Public State As String <System.Xml.Serialization.XmlElementAttribute(DataType:="integer", IsNullable:=false)> Public Zip As String End Class <System.Xml.Serialization.XmlTypeAttribute([Namespace]:="http://localhost/chapters/03/ xmlschemas/schemas/sales/Sales.xsd")> Public Class itemType <System.Xml.Serialization.XmlElementAttribute(IsNullable:=false)> Public ITEMNAME As String <System.Xml.Serialization.XmlElementAttribute(IsNullable:=false)> Public DESCRIPTION As String <System.Xml.Serialization.XmlElementAttribute(IsNullable:=false)> Public SIZE As String Public PRICE As Decimal <System.Xml.Serialization.XmlIgnoreAttribute()> Public PRICESpecified As Boolean <System.Xml.Serialization.XmlAttributeAttribute()> Public name As String <System.Xml.Serialization.XmlAttributeAttribute()> Public value As String <System.Xml.Serialization.XmlAttributeAttribute()> Public description As String <System.Xml.Serialization.XmlAttributeAttribute()> Public color As colorEnum <System.Xml.Serialization.XmlIgnoreAttribute()> Public colorSpecified As Boolean End Class Public Enum colorEnum Blue Red Black Green End Enum
As with most code generators, shortcomings exist in the code. Naming things in XML documents usually differs from naming items in code, but the utility uses the names in your schema document for local variable names . The .NET Framework Design Guidelines specifies pascal casing for public properties and fields, but the generated code uses capital letters for elements and lowercase letters for attributes. I also prefer to use property procedures rather than direct field access in classes. However, the generated code serves as a good baseline for developing a set of classes to work with the XML document. Be assured that an XML document generated from this structure is generated in the proper format.
One real shortcoming of this utility is the fact that the facets for your simple types are not preserved. For example, the regular expression that you specified for emailType is not represented within the code and there is no restriction within your class hierarchy to enforce the constraints set out. Of course, this is a function of the XML schema document, but it would have been more impressive if the object hierarchy could enforce the constraints specified in the XML Schema document.
Popular Utility Development OpportunityAlthough the implementation of constraining facets is not represented within generated classes, this shortcoming presents some interesting possibilities for developing your own utility that preserves the constraining facet behaviors using the CodeDom classes and System.Xml. Serialization.XmlCodeExporter to generate the code. |
Another shortcoming is how you name the elements within a schema. XSD Schemas maintain different name tables for each schema component. This means that an attribute can have the same name as an element within the same class. Notice that in the itemType class in the preceding code, there are two different declarations for description : The capitalized version, DESCRIPTION , which represents the element (I use uppercase letters for elements by convention within this document), and the lowercase version that represents an attribute called description . Remember that XML is case-sensitive. But, because Visual Basic is not case-sensitive, the preceding generated code would not compile because the name description is declared twice. This would not be a limitation in C# because C# is case-sensitive. Either way, you will probably opt not to stick with the default naming within the document.
To rename the internal variables in your code but still preserve the generated XML Schema, the designers of .NET included attributes to describe exactly what the serialized XML should look like. To do this, simply rename the internal variable for the lowercase description attribute and specify the name of the attribute in the serialization attribute as follows :
<System.Xml.Serialization.XmlAttributeAttribute("description")> Public AttribDescription As String
After that is corrected, you can work with the generated class hierarchy. Listing 2.7 is a sample code listing for working with the new class hierarchy created in Listing 2.6. You will create a simple ASP page that will populate the typed DataSet with data and serialize the DataSet to XML. The ASP layout code is a simple interface that consists of labels and textboxes for the various properties. There are also one drop-down list box and three buttons . The UI code is unimportant, so you need to focus on the code-behind Visual Basic code.
The first section simply declares the UI elements to be used in the codebehind class.
Imports System Imports System.Xml Imports System.Xml.Schema Public Class sales Inherits System.Web.UI.Page Protected WithEvents Label1 As System.Web.UI.WebControls.Label Protected WithEvents Label2 As System.Web.UI.WebControls.Label Protected WithEvents txtPhone As System.Web.UI.WebControls.TextBox Protected WithEvents Label3 As System.Web.UI.WebControls.Label Protected WithEvents txtEmail As System.Web.UI.WebControls.TextBox Protected WithEvents Label4 As System.Web.UI.WebControls.Label Protected WithEvents txtBillToName As System.Web.UI.WebControls.TextBox Protected WithEvents Label5 As System.Web.UI.WebControls.Label Protected WithEvents txtStreet1 As System.Web.UI.WebControls.TextBox Protected WithEvents Label6 As System.Web.UI.WebControls.Label Protected WithEvents txtStreet2 As System.Web.UI.WebControls.TextBox Protected WithEvents Label7 As System.Web.UI.WebControls.Label Protected WithEvents txtCity As System.Web.UI.WebControls.TextBox Protected WithEvents Label8 As System.Web.UI.WebControls.Label Protected WithEvents txtState As System.Web.UI.WebControls.TextBox Protected WithEvents Label9 As System.Web.UI.WebControls.Label Protected WithEvents txtZip As System.Web.UI.WebControls.TextBox Protected WithEvents Label10 As System.Web.UI.WebControls.Label Protected WithEvents txtItemName As System.Web.UI.WebControls.TextBox Protected WithEvents Label11 As System.Web.UI.WebControls.Label Protected WithEvents txtDescription As System.Web.UI.WebControls.TextBox Protected WithEvents Label12 As System.Web.UI.WebControls.Label Protected WithEvents txtSize As System.Web.UI.WebControls.TextBox Protected WithEvents Label14 As System.Web.UI.WebControls.Label Protected WithEvents txtAttribName As System.Web.UI.WebControls.TextBox Protected WithEvents Label15 As System.Web.UI.WebControls.Label Protected WithEvents txtAttribValue As System.Web.UI.WebControls.TextBox Protected WithEvents Label16 As System.Web.UI.WebControls.Label Protected WithEvents txtAttribDescription As System.Web.UI.WebControls.TextBox Protected WithEvents Label17 As System.Web.UI.WebControls.Label Protected WithEvents DropDownList1 As System.Web.UI.WebControls.DropDownList Protected WithEvents Button1 As System.Web.UI.WebControls.Button Protected WithEvents Button2 As System.Web.UI.WebControls.Button Protected WithEvents Button3 As System.Web.UI.WebControls.Button Protected WithEvents txtName As System.Web.UI.WebControls.TextBox #Region " Web Form Designer Generated Code " 'This call is required by the Web Form Designer. <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent() End Sub Private Sub Page_Init(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Init 'CODEGEN: This method call is required by the Web Form Designer 'Do not modify it using the code editor. InitializeComponent() End Sub #End Region
When the page loads, you might want to add sample items to the drop-down list to work with your generated class hierarchy:
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load If Not IsPostBack then DropDownList1.Items.Add("0 - Blue") DropDownList1.Items.Add("1 - Red") DropDownList1.Items.Add("2 - Black") DropDownList1.Items.Add("3 - Green") End if End Sub
The code for the Click event of the Button1 object populates the hierarchy with the contents of the form and serializes the hierarchy of objects to an XML file. You must look at the individual steps to achieve this. Begin by declaring the object references and creating a new object instance for each reference:
Private Sub Serialize(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles _ Button1.Click Dim po As XMLSchemas.PURCHASEORDER = New PURCHASEORDER() Dim cust As XMLSchemas.customerType = New customerType() Dim item As XMLSchemas.itemType = New XMLSchemas.itemType() Dim billing As XMLSchemas.addressType = New XMLSchemas.addressType()
So far, you have only set up the object references by using the types exposed by generated classes. To create instances of the objects, you need to create an object array to hold the object references. The generated classes support a CreateInstance method to create an instance of a class contained as a child. You can use the CreateInstance method to create instances of classes based on their type:
'Create a 1-element array of customerType objects po.CUSTOMER = po.CUSTOMER.CreateInstance(cust.GetType, 1) 'Create a 1-element array of itemType objects po.ORDER = po.ORDER.CreateInstance(item.GetType, 1)
The first parameter to the CreateInstance method is a System.Type argument. The cust object is an object reference, not a type, so you need to get the type that it represents by using the GetType method. The second parameter to CreateInstance simply states how many elements the array will contain. For simplicity, suppose that it will hold one object.
After the containing arrays are instantiated and associated with the parent objects, you can populate the array contents, as shown here:
With cust .EMAIL = Me.txtEmail.Text .NAME = Me.txtBillToName.Text .PHONE = Me.txtPhone.Text End With 'Set the first element in the customer array to the populated customerType object po.CUSTOMER(0) = cust With billing .Name = Me.txtName.Text .Street1 = Me.txtStreet1.Text .City = Me.txtCity.Text .State = Me.txtState.Text .Zip = Me.txtZip.Text End With 'Set the customer's billing address property cust.BILLING = billing With item .color = DropDownList1.SelectedIndex 'The color is always specified .colorSpecified = True .ITEMNAME = "Big Screen TV" 'We did not include the price element on the UI .PRICESpecified = False .SIZE = Me.txtSize.Text .name = Me.txtAttribName.Text .value = Me.txtAttribValue.Text End With po.ORDER(0) = item
After the object hierarchy is completely populated, serialize the contents of the entire hierarchy to a file. We hard-coded the path to the XML file for brevity, but you probably want to use some other means of dynamically retrieving the file's name and/or contents. Again, use the GetType method to retrieve the po object's type so that the XmlSerializer knows what to serialize. The Serialize method is then called to serialize the po object to the specified XML file, as shown here:
'Serialize the purchase order to an XML file. Dim serializer As System.Xml.Serialization.XmlSerializer Dim file As System.IO.FileStream = New System.IO.FileStream("c:\temp\xmlfromclass.xml", _ IO.FileMode.OpenOrCreate) serializer = New System.Xml.Serialization.XmlSerializer(po.GetType) serializer.Serialize(file, po) file.Close() End Sub
To rehydrate the object based on the persisted XML file, do the reverse of the serialization process. Instantiate a po object to be deserialized and then populate the web form based on the contents of the hierarchy:
Private Sub Deserialize(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click Dim po As XMLSchemas.PURCHASEORDER = New XMLSchemas.PURCHASEORDER() Dim serializer As System.Xml.Serialization.XmlSerializer Dim file As System.IO.FileStream = New System.IO.FileStream("c:\temp\xmlfromclass.xml", _ IO.FileMode.Open) 'Open the XML file and re-hydrate the purchase order object serializer = New System.Xml.Serialization.XmlSerializer(po.GetType) po = serializer.Deserialize(file) file.Close() 'Populate the web form elements' text property With po.CUSTOMER(0) txtName.Text = .NAME txtEmail.Text = .EMAIL txtPhone.Text = .PHONE End With With po.CUSTOMER(0).BILLING txtStreet1.Text = .Street1 txtStreet2.Text = .Street2 txtCity.Text = .City txtState.Text = .State txtZip.Text = .Zip End With With po.ORDER(0) txtItemName.Text = .ITEMNAME txtDescription.Text = .DESCRIPTION txtSize.Text = .SIZE txtAttribName.Text = .name txtAttribValue.Text = .value DropDownList1.SelectedIndex = .color End With End Sub End Class
This example shows how easy it is to work with the generated class files and how quick it is to generate the classes from an XSD Schema. This section also gives a brief introduction to serialization.
You have seen that new tools in Visual Studio .NET help generate XML Schemas, so now it's time to look at how XSD Schemas are interwoven into .NET. XSD Schemas are tightly integrated into ADO.NET. In fact, the DataSet in ADO.NET is structured around XSD Schemas to represent the structure of the data. The DataSet class provides the capability to read schemas, validate the contained data against schemas, and to infer schemas based on the data they contain. Because a DataSet is a view of data, its structure can be represented in an XML Schema. Working with schemas and DataSets is a key part of the ADO.NET strategy because the underlying data can easily interoperate even with non-.NET clients . Although you do not have to know much about XML or XSD Schemas to work with ADO.NET, a firm understanding of XML and schemas help you to gain mastery of .NET development.
One of the easiest operations with a DataSet is to generate a schema based on the DataSet's contents. In the following code, you read a table from a database, use the WriteXMLSchema method to write the schema to an interim stream, and return the contents of the stream to the calling function:
Private Function GetSchemaFromQuery(ByVal query As String, _ ByVal connectionString As String) As String Dim connection As System.Data.SqlClient.SqlConnection Dim adapter As System.Data.SqlClient.SqlDataAdapter Dim mydataSet As System.Data.DataSet = New Data.DataSet() Dim memStream As System.IO.MemoryStream = New System.IO.MemoryStream() Dim writer As System.Xml.XmlTextWriter Dim reader As System.IO.StreamReader 'Get the data from the database connection = New SqlClient.SqlConnection(connectionString) adapter = New SqlClient.SqlDataAdapter(query, connection) adapter.Fill(mydataSet) 'Release database resources adapter.Dispose() connection.Close() connection.Dispose() 'Write the schema to the memory stream writer = New System.Xml.XmlTextWriter(memStream, System.Text.Encoding.UTF8) mydataSet.WriteXmlSchema(writer) 'reset the position of the stream to the beginning memStream.Position = 0 'Read the data from the memory stream reader = New System.IO.StreamReader(memStream) Return reader.ReadToEnd 'Release resources reader.Close() memStream.Close() mydataSet.Dispose() End Function
ADO.NET provides several methods for creating a schema structure from the XML data in a DataSet. The InferXMLSchema method of the DataSet object creates a schema that's based on the contents of the XML data and allows you to specify namespaces to ignore. Similarly, the ReadXML and ReadXMLSchema both perform similar functions.
One key feature of ADO.NET is its capability to work with a strongly typed DataSet. A strongly typed DataSet is a class that inherits from the DataSet class and uses information from an XML Schema to generate a new class as a set of first-class properties and methods (MSDN, "Introduction to DataSets"). As you saw earlier with the xsd.exe utility, the structure expressed in an XSD Schema file can map to the hierarchy of a DataSet class as a collection of tables. Figure 2.3 gives you a view of the hierarchical structure of a DataSet and the classes it contains.
DataSets are covered in more detail in Chapter 8.
With all the integration of schemas into .NET and Visual Studio .NET, no wonder there is a mature set of classes in the .NET Framework to work with schemas. Although you can work with schemas using the DataSet class, the rest of this chapter focuses on working with the System.Xml.Schema classes and performing validation against schema documents.
The DataSet class enables you to work with a schema document as a set of tables, but you might find that you need to programmatically access a schema and determine its exact structure. The classes in the System.Xml.Schema namespace provide this functionality without writing custom parsing routines to parse the document simply as a raw XML document.
The designers of the framework designed a close representation of the W3C recommendation model. Class naming is relatively consistent with the recommendation model (although you need to pay attention to terms and definitions, such as particle, content, and model).
The root node of a schema document is the schema element. The class framework provides this node through the XmlSchema class. All schema elements are children of the XmlSchema class, so they are at the top of the hierarchy. You can use the System.Xml.Schema classes to generate the following schema document shown in Listing 2.8. The output first is provided first so it's easier to reference the code that is used to create the schema document .
<?xml version="1.0" encoding="utf-8" ?> <xsd:schema id="addressbook" targetNamespace="urn:schemas-vbdna net:addressbook" elementFormDefault="qualified" xmlns:tns="urn:schemas-vbdna net:addressbook" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <xsd:complexType name="addressType"> <xsd:sequence minOccurs="1" maxOccurs="1"> <xsd:sequence> <xsd:element name="street1" minOccurs="1" maxOccurs="1" type="xsd:string" /> <xsd:element name="street2" minOccurs="0" maxOccurs="1" type="xsd:string" /> <xsd:element name="city" minOccurs="1" maxOccurs="1" type="xsd:string" /> <xsd:element name="state" minOccurs="1" maxOccurs="1" type="xsd:string" /> <xsd:element name="zip" minOccurs="1" maxOccurs="1" type="tns:zipType" /> </xsd:sequence> </xsd:sequence> </xsd:complexType> <xsd:simpleType name="zipType"> <xsd:restriction base="xsd:string"> <xsd:pattern value="\d{5}(-\d{4})?" /> </xsd:restriction> </xsd:simpleType> <xsd:simpleType name="emailType"> <xsd:restriction base="xsd:string"> <xsd:pattern value="\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*" /> </xsd:restriction> </xsd:simpleType> <xsd:element name="addressBook"> <xsd:complexType> <xsd:sequence minOccurs="0" maxOccurs="unbounded"> <xsd:element name="entry"> <xsd:complexType> <xsd:sequence> <xsd:element name="name" type="xsd:string" /> <xsd:element name="address" type="tns:addressType" /> <xsd:element name="email" type="tns:emailType" minOccurs="0" maxOccurs="unbounded" /> </xsd:sequence> </xsd:complexType> </xsd:element> </xsd:sequence> </xsd:complexType> </xsd:element> </xsd:schema>
Begin by defining the XmlSchema class object and defining the namespace that will be used as the targetNamespace in Listing 2.9. The XmlSchema class object contains an Items collection that adds items as children of the schema node.
using System; using System.Xml; using System.Xml.Schema; namespace XmlSchemaCS { public class CustomerSchemaFunctions { public const string W3C_SCHEMA_NS ="http://www.w3.org/2001/XMLSchema"; private Boolean m_IsValid = false; ///this is a comment for the boolean variable private string m_targetNameSpace = ""; //Only ctor, with parameter public CustomerSchemaFunctions(string nameSpace) { m_targetNameSpace = nameSpace; } . . . public void WriteSchemaXml(string xsdFile) /// <summary>function to write the schema as xml</summary> { ///Create the schema root node with the targetNamespace XmlSchema schema = new XmlSchema(); schema.TargetNamespace=m_targetNameSpace; schema.AttributeFormDefault = XmlSchemaForm.Unqualified; schema.ElementFormDefault = XmlSchemaForm.Qualified; ; XmlQualifiedName schemaString = new XmlQualifiedName("string", W3C_SCHEMA_NS); schema.Items.Add(CreateZipType()); schema.Items.Add(CreateEmailType()); schema.Items.Add(CreateAddressType()); schema.Items.Add(CreateEntryType()); . . . } }
The code in Listing 2.9 is not a complete working example yet, but shows the beginnings of the complete sample (shown in Listing 2.10).
Looking at the XML Schema document, the children of the schema node are the addressBook element, the zipType , emailType simple types, and the addressType complex type. These will be added as children of the schema node. The XmlSchema class object contains an Items collection that adds items as children of the schema node.
Working with the XmlSchema classes require you to think backwards . Instead of creating a simpleType , adding a restriction, then adding the facets, you must work in reverse. Create the facet, create the restriction, add the facet to the facets collection of the restriction, and add the restriction to the simpleType . The simpleType class does not expose an Items collection like the XmlSchema object. Instead, you specify the child elements of the xsd:simpleType element by using the XmlSchemaSimpleType.Content property. The following code creates the root schema node and a simple type emailType . I split the creation of the simpleType and the schema element to better show the scope of each method:
private XmlSchemaSimpleType CreateEmailType() { XmlSchemaSimpleTypeRestriction restriction = new XmlSchemaSimpleTypeRestriction (); restriction.BaseTypeName = new System.Xml.XmlQualifiedName("string",W3C_SCHEMA_NS); XmlSchemaPatternFacet pattern = new XmlSchemaPatternFacet(); pattern.Value ="\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*"; restriction.Facets.Add (pattern); XmlSchemaSimpleType emailType = new XmlSchemaSimpleType(); emailType.Name = "emailType"; emailType.Content = restriction; return(emailType); }
Notice that the WriteSchemaXml method in Listing 2.9 calls schema.Items.Add() . This is how the simpleType is actually appended to the schema. Simply creating an XmlSchemaObject object does not append it to the schema object; it must be explicitly added through the Add method. Failure to do this results in the type missing from the generated schema.
Creating a complexType is similar to creating an XmlSchemaSimpleType . To create a complex type, use the Schema.XmlSchemaComplexType object, as shown in Listing 2.10. As with the XmlSchemaSimpleType object, the XmlSchemaComplexType does not support an Items collection. To specify child content, use the Particle property to specify an all, choice, or sequence group particle.
private XmlSchemaElement GenElement ( string name, int minOccurs, int maxOccurs) { XmlSchemaElement returnElement = new XmlSchemaElement(); returnElement.Name = name; returnElement.MinOccurs = minOccurs; returnElement.MaxOccurs = maxOccurs; return (returnElement); } private XmlSchemaElement GenElement (string name, int minOccurs, int maxOccurs, System.Xml.XmlQualifiedName qualifiedName) { XmlSchemaElement element = new XmlSchemaElement(); element.Name = name; element.MinOccurs = minOccurs; element.MaxOccurs = maxOccurs; element.SchemaTypeName = qualifiedName; return (element); } private XmlSchemaComplexType CreateAddressType() { System.Xml.XmlQualifiedName schemaString = new System.Xml.XmlQualifiedName("string",W3C_SCHEMA_NS); XmlSchemaSequence seq = new XmlSchemaSequence(); seq.MinOccurs=1; seq.MaxOccurs=1; seq.Items.Add (GenElement("street1",1,1,schemaString)); seq.Items.Add (GenElement("street2",0,1,schemaString)); seq.Items.Add (GenElement("city",1,1,schemaString)); seq.Items.Add (GenElement("state",1,1,schemaString)); seq.Items.Add (GenElement("zip",1,1,new System.Xml.XmlQualifiedName("zipType",m_targetNameSpace))); XmlSchemaComplexType addressType = new XmlSchemaComplexType(); addressType.Name = "addressType"; addressType.Particle = seq; return(addressType); }
Rather than typing the code to create single elements, I created an overloaded helper function, GenElement (shown in Listing 2.10 as a shaded line), to facilitate the creation of elements. The overloaded version of this helper function accepts a parameter called qualifiedName : This is the QName that you can use to associate the type name ( zipType ) with your namespace ( urn:schemas- xmlandasp-net:framework ). You also created a local variable in the custom Create AddressType function called schemaString that declares the QName for the built-in string type ( xsd:string ).
After you're done creating each type, append it to the schema.Items collection by using the Add method. After all the types and your root element are appended to the schema, you can then retrieve the XML representation of the schema. I used an XmlTextWriter object to write the schema to a file, although you might want to store the schema as a string or keep it in memory.
Before writing the file, you need to call the Compile method. The Compile method, as shown in Listing 2.11, compiles the contents of the Schema Object Model (SOM) so that the schema can then be read or written. When you write the file, you must set up an event handler to capture errors thrown if a problem with the schema occurs.
A separate code listing for Visual Basic .NET isn't included here because the code is so similar between the two languages. The code is available on the book's website, however, which is located at www.xmlandasp.net. The main differences between the Visual Basic .NET version and the C# version is how the regular expressions look. C# requires a delimiter for \ characters , while Visual Basic .NET does not. So, the following line in C#:
pattern.Value ="\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*";
is changed to the following line in Visual Basic .NET:
pattern.Value = "\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*"
Listing 2.11 contains the complete code listing, available for download as listing2-11.cs , for generating the schema as previously described.
using System; using System.Xml; using System.Xml.Schema; namespace XmlSchemaCS { public class CustomerSchemaFunctions { public const string W3C_SCHEMA_NS ="http://www.w3.org/2001/XMLSchema"; private Boolean m_IsValid = false; ///this is a comment for the boolean variable private string m_targetNameSpace = ""; public CustomerSchemaFunctions(string nameSpace) { m_targetNameSpace = nameSpace; } public void WriteSchemaXml(string xsdFile) /// <summary>function to write the schema as xml</summary> { ///Create the schema root node with the targetNamespace XmlSchema schema = new XmlSchema(); schema.TargetNamespace=m_targetNameSpace; schema.AttributeFormDefault = XmlSchemaForm.Unqualified; schema.ElementFormDefault = XmlSchemaForm.Qualified; ; XmlQualifiedName schemaString = new XmlQualifiedName("string", W3C_SCHEMA_NS); schema.Items.Add(CreateZipType()); schema.Items.Add(CreateEmailType()); schema.Items.Add(CreateAddressType()); schema.Items.Add(CreateEntryType()); XmlSchemaSequence seq = new XmlSchemaSequence(); seq.MinOccurs=0; seq.MaxOccurs=1000; seq.Items.Add(GenElement("entry",1,1000,new XmlQualifiedName("entryType", m_targetNameSpace))); XmlSchemaComplexType complexType = new XmlSchemaComplexType(); complexType.Particle = seq; XmlSchemaElement addressBook = new XmlSchemaElement(); addressBook.Name = "addressBook"; addressBook.SchemaType = complexType; schema.Items.Add (addressBook); XmlTextWriter writer = new XmlTextWriter (xsdFile, System.Text.Encoding.UTF8); schema.Compile(new ValidationEventHandler(ValidationCallback)); schema.Write(writer); writer.Close(); } private XmlSchemaElement GenElement( string name, int minOccurs, int maxOccurs) { XmlSchemaElement returnElement = new XmlSchemaElement(); returnElement.Name = name; returnElement.MinOccurs = minOccurs; returnElement.MaxOccurs = maxOccurs; return (returnElement); } private XmlSchemaElement GenElement(string name, int minOccurs, int maxOccurs, System.Xml.XmlQualifiedName qualifiedName) { XmlSchemaElement element = new XmlSchemaElement(); element.Name = name; element.MinOccurs = minOccurs; element.MaxOccurs = maxOccurs; element.SchemaTypeName = qualifiedName; return (element); } private XmlSchemaSimpleType CreateZipType() { XmlSchemaSimpleTypeRestriction restriction = new XmlSchemaSimpleTypeRestriction (); restriction.BaseTypeName = new System.Xml.XmlQualifiedName("string", W3C_SCHEMA_NS); XmlSchemaPatternFacet pattern = new XmlSchemaPatternFacet(); pattern.Value ="\d{5}(-\d{4})?"; restriction.Facets.Add (pattern); XmlSchemaSimpleType zipType = new XmlSchemaSimpleType(); zipType.Name = "zipType"; zipType.Content = restriction; return(zipType); } private XmlSchemaSimpleType CreateEmailType() { XmlSchemaSimpleTypeRestriction restriction = new XmlSchemaSimpleTypeRestriction (); restriction.BaseTypeName = new System.Xml.XmlQualifiedName("string", W3C_SCHEMA_NS); XmlSchemaPatternFacet pattern = new XmlSchemaPatternFacet(); pattern.Value ="\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*"; restriction.Facets.Add (pattern); XmlSchemaSimpleType emailType = new XmlSchemaSimpleType(); emailType.Name = "emailType"; emailType.Content = restriction; return(emailType); } private XmlSchemaComplexType CreateAddressType() { System.Xml.XmlQualifiedName schemaString = new System.Xml.XmlQualifiedName("string",W3C_SCHEMA_NS); XmlSchemaSequence seq = new XmlSchemaSequence(); seq.MinOccurs=1; seq.MaxOccurs=1; seq.Items.Add (GenElement("street1",1,1,schemaString)); seq.Items.Add (GenElement("street2",0,1,schemaString)); seq.Items.Add (GenElement("city",1,1,schemaString)); seq.Items.Add (GenElement("state",1,1,schemaString)); seq.Items.Add (GenElement("zip",1,1,new System.Xml.XmlQualifiedName("zipType",m_targetNameSpace))); XmlSchemaComplexType addressType = new XmlSchemaComplexType(); addressType.Name = "addressType"; addressType.Particle = seq; return(addressType); } private XmlSchemaComplexType CreateEntryType() { System.Xml.XmlQualifiedName schemaString = new System.Xml.XmlQualifiedName("string",W3C_SCHEMA_NS); XmlSchemaSequence seq = new XmlSchemaSequence(); seq.MinOccurs = 1; seq.MaxOccurs = 1; seq.Items.Add (GenElement("name",1,1,schemaString)); seq.Items.Add (GenElement("address",1,1, new System.Xml.XmlQualifiedName("addressType",m_targetNameSpace))); seq.Items.Add (GenElement("email",0,4, new System.Xml.XmlQualifiedName("emailType",m_targetNameSpace))); XmlSchemaComplexType entryType = new XmlSchemaComplexType(); entryType.Name = "entryType"; entryType.Particle = seq; return(entryType); } } }
Because there is no Read method on an XmlSchema object, how does someone access a schema programmatically using System.Xml.Schema classes? The answer is through the System.Xml.Schema.XmlSchemaCollection object. This object is a collection of schemas that you load from a URL source or an XmlTextReader object. You can add multiple schemas to the collection and then work with all of them programmatically. XmlSchemaCollection is used here to read the schema file created in the previous section and to access it programmatically.
The XmlTextReader accepts a URL for one of its constructors, so the file:// moniker is being used to load the file from the file system. You can add the schema to the XmlSchemaCollection object to populate the SOM. You can retrieve a reference to the populated SOM (the XmlSchema object) by using the collection accessor foreach in C# (the same as for each in Visual Basic). After you have a reference to the XmlSchema object, you can access its properties and methods, as follows:
public void ReadSchemaXml(string xsdFile) { //Read the schema from file using the file:// moniker. System.Xml.XmlTextReader reader = new System.Xml.XmlTextReader(xsdFile); XmlSchemaCollection schemas = new XmlSchemaCollection(); //Add the schema to the schemas collection schemas.Add(m_targetNameSpace,reader); foreach (XmlSchema schema in schemas) { //We now have the schema object try { System.Diagnostics.Debug.WriteLine (schema.TargetNamespace); . . . } catch(System.Exception e) { System.Diagnostics.Debug.WriteLine(e.ToString()); } } }
This is not a complete listing, however; you still have to fill in some gaps. After you have a reference to the schema object, you can access its properties and child nodes the same way that you added them. You can use the for each syntax to cycle through the Items collection to look for child elements of the schema node. Each object in the Items collection has a base type of XmlSchemaObject , so you can test each object to see what types it supports and cast to the appropriate type:
foreach ( XmlSchemaObject element in schema.Items) { if(element is XmlSchemaSimpleType) { XmlSchemaSimpleType simpleType = (XmlSchemaSimpleType)element; XmlSchemaSimpleTypeContent content = simpleType.Content ; if (content is XmlSchemaSimpleTypeRestriction) { XmlSchemaSimpleTypeRestriction restriction = (XmlSchemaSimpleTypeRestriction )simpleType.Content; foreach(XmlSchemaFacet facet in restriction.Facets) { if (facet is XmlSchemaPatternFacet) { . . . } } else if(element is XmlSchemaComplexType) { } else . . . }
By using the Is operator, you can cast each object to a workable type to access the object-specific properties and methods. If you are working with a simpleType object, you need to reference its Content property. If you are working with a complexType object, you need access to its Particle property.
You can expand the simple type logic to display the XML in the debug window. You use the Is operator and a foreach loop to cycle through the facets collection and display only those facets that are pattern facets (facets that use regular expressions for validation):
foreach ( XmlSchemaObject element in schema.Items) { if(element is XmlSchemaSimpleType) { XmlSchemaSimpleType simpleType = (XmlSchemaSimpleType)element; System.Diagnostics.Debug.WriteLine ("<xsd:simpleType name=" + simpleType.Name ); XmlSchemaSimpleTypeContent content = simpleType.Content ; if (content is XmlSchemaSimpleTypeRestriction) { XmlSchemaSimpleTypeRestriction restriction = null; restriction = (XmlSchemaSimpleTypeRestriction) simpletype.Content; System.Diagnostics.Debug.WriteLine("<xsd:restriction base=\"" + restriction.BaseTypeName + "\">"); foreach(XmlSchemaFacet facet in restriction.Facets) { if (facet is XmlSchemaPatternFacet) { System.Diagnostics.Debug.WriteLine("<xsd:pattern value=\"" + facet.Value+ "\"/ >"); } } System.Diagnostics.Debug.WriteLine("<//xsd:restriction>"); } System.Diagnostics.Debug.WriteLine ("<//xsd:simpleType"); } else if(element is XmlSchemaComplexType) { XmlSchemaComplexType complexType = (XmlSchemaComplexType)element; System.Diagnostics.Debug.WriteLine (complexType.Name ); } else System.Diagnostics.Debug.WriteLine(element.ToString()); }
Notice that the Content property of the XmlSchemaSimpleType method returns an object of type XmlSchemaSimpleTypeContent . This object needs to be cast to its appropriate type (restriction, list, or union) to access the type-specific methods. You then cast the Content property's return object to an XmlSchemaSimpleType Restriction object to access the facets collection. Listing 2.12 contains the complete code to give you perspective on the logic that's used to parse out a schema using the framework classes.
public void ReadSchemaXml(string xsdFile) { //Read the schema from file using the file:// moniker. System.Xml.XmlTextReader reader = new System.Xml.XmlTextReader(xsdFile); XmlSchemaCollection schemas = new XmlSchemaCollection(); //Add the schema to the schemas collection schemas.Add(m_targetNameSpace,reader); foreach (XmlSchema schema in schemas) { //We now have the schema object try { foreach ( XmlSchemaObject element in schema.Items) { if(element is XmlSchemaSimpleType) { XmlSchemaSimpleType simpleType = XmlSchemaSimpleType)element; System.Diagnostics.Debug.WriteLine ("<xsd:simpleType name=" + simpleType.Name ); XmlSchemaSimpleTypeContent content = simpleType.Content ; if (content is XmlSchemaSimpleType Restriction) { XmlSchemaSimpleTypeRestriction restriction = (XmlSchemaSimpleTypeRestriction ) simpleType.Content; System.Diagnostics.Debug.WriteLine("<xsd:restriction base=\"" + restriction.BaseTypeName + "\">"); foreach(XmlSchemaFacet facet in restriction.Facets) { if (facet is XmlSchemaPatternFacet) { System.Diagnostics.Debug.WriteLine("<xsd:pattern value=\"" + facet.Value+ "\"/>"); } } System.Diagnostics.Debug.WriteLine("</xsd:restriction>"); } System.Diagnostics.Debug.WriteLine("</xsd:simpleType"); } } } catch(System.Exception e) { System.Diagnostics.Debug.WriteLine(e.ToString()); } } }
The same code in Visual Basic .NET is different regarding type conversion. The Visual Basic .NET version of the code, shown in Listing 2.13, relies on the Ctype function for casting and uses the TypeOf operator to determine if an object supports a given type.
Public Sub ReadSchemaXml(ByVal xsdFile As String) 'Read the schema from file using the file:// moniker. Dim reader As System.Xml.XmlTextReader = New System.Xml.XmlTextReader(xsdFile) Dim schemas As XmlSchemaCollection = New XmlSchemaCollection() 'Add the schema to the schemas collection schemas.Add(m_targetNameSpace, reader) Dim schema As XmlSchema For Each schema In schemas 'We now have the schema object Try Dim element As XmlSchemaObject For Each element In schema.Items If TypeOf element Is XmlSchemaSimpleType Then Dim simpleType As XmlSchemaSimpleType = CType(element, XmlSchemaSimpleType) System.Diagnostics.Debug.WriteLine("<xsd:simple Typename=" + simpleType.Name) Dim content As XmlSchemaSimpleTypeContent = simpleType.Content If TypeOf content Is XmlSchemaSimpleTypeRestriction Then Dim restriction As XmlSchemaSimpleTypeRestriction = CType(simpleType.Content, XmlSchema SimpleTypeRestriction) System.Diagnostics.Debug.WriteLine("<xsd:restriction base=\"" + restriction.BaseTypeName + " \ ">") Dim facet As XmlSchemaFacet For Each facet In restriction.Facets If TypeOf facet Is XmlSchemaPatternFacet Then System.Diagnostics.Debug.WriteLine("<xsd:pattern value=\"" + facet.Value+ " \ "/>") End If Next facet System.Diagnostics.Debug.WriteLine("</xsd:restriction>") End If System.Diagnostics.Debug.WriteLine("</xsd:simpleType") End If Next element Catch errOops As System.Exception System.Diagnostics.Debug.WriteLine(errOops.ToString()) End Try Next schema End Sub
The output from Listing 2.12 and Listing 2.13 yields the following:
urn:schemas-vbdna-net:framework <xsd:simpleType name=zipType <xsd:restriction base="http://www.w3.org/2001/XMLSchema:string"> <xsd:pattern value="\d{5}(-\d{4})?"/> </xsd:restriction> </xsd:simpleType <xsd:simpleType name=emailType <xsd:restriction base="http://www.w3.org/2001/XMLSchema:string"> <xsd:pattern value="\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*"/> </xsd:restriction> </xsd:simpleType
This section looks at programmatically validating XML documents against XML Schemas by using the XmlValidatingReader class. You must understand three main components when you're using the XmlValidatingReader : the reader, the XmlSchemaCollection object, and the ValidationEventHandler .
Chapter 4 explains the concept of readers and writers in the .NET Framework in detail. That concept is introduced in this section by exploring the XmlValidatingReader class. XmlValidatingReader does precisely what its name implies: It reads XML and validates that XML against an XML Schema. XmlValidatingReader reads from a stream, so it does not actually hold the contents of the stream. It uses an instance of the XmlTextReader to read from the stream and extends the functionality of the XmlTextReader by validating the node against the specified schema(s). To associate the XmlTextReader instance with the XmlValidatingReader instance, you can use the constructor that accepts a text reader:
//Instantiate a reader to read the XML file XmlTextReader textReader = new XmlTextReader(xmlFile); //Associate the ValidatingReader with the XMLTextReader object XmlValidatingReader validator = new XmlValidatingReader(textReader);
After the XmlTextReader is associated with the XmlValidatingReader , you need to supply the schema(s) to the XmlValidatingReader instance. The XmlValidating Reader class exposes a Schemas collection that directs the validator to the schemas that are going to be used. The Schemas collection exposes an Add method that accepts four overloads to locate the schema(s) to be validated against. The first accepts an XmlSchemaCollection object, which is useful if you are working with multiple schemas:
XmlSchemaCollection schemas = new XmlSchemaCollection(); schemas.Add ("urn:schemas-vbdna-net:framework", new XmlTextReader(xsdFile)); validatingReader.Schemas.Add(schemas);
The second overload accepts a single XmlSchema object. As you saw earlier, using this object requires that you either build the schema on the fly or retrieve the schema from an XmlSchemaCollection object. The third overload accepts a namespace and an XmlReader abstract class. For this overload, you could specify an XmlTextReader because XmlTextReader is a concrete implementation of the XmlReader abstract class. This is nearly identical to the preceding usage of the XmlSchemaCollection object without a separate object reference:
validatingReader.Schemas.Add("urn:schemas-vbdna-net:customer", new XmlTextReader(xsdFile));
The last overload accepts a namespace for the schema and the URL to the physical file. You'll use this overload for the example, but the third overload can also be used:
validatingReader.Schemas.Add("urn:schemas-xmlandasp net:customer","http://www.xmlandasp.net/schemas/fr.xsd");
As the XmlValidatingReader reads through the stream by using the supplied XmlTextReader , it validates the content in the stream to the schema specified and raises an event if the node violates the schema. This event is specified through its ValidationEvent property. Because the event is fired when the schema is invalid, you need a class member variable to determine, from the function containing the ValidatingReader , if the schema is valid or not.
To wire up the event handler and the XmlValidatingReader , declare a function that accepts two parameters ”an Object type and a ValidationEventArgs type:
private void ValidationCallback ( object sender, ValidationEventArgs args ) { System.Diagnostics.Debug.WriteLine ("Validation error: {0}" + args.Message); isValid = false; }
Inside this function, you have a simple debug output message that lets you know that a validation event occurred and what the message was. You can also set the class member variable to false to indicate that the validation was unsuccessful .
The next step is to create a ValidationEventHandler class that uses your created function as a callback. C# uses a delegate to wire up the event with the object, as shown here:
// Wire up the callback to our ValidationCallBack routine ValidationEventHandler eventHandler = new ValidationEventHandler (ValidationCallback); //Wire up the event handler to the validating reader's event handler validator.ValidationEventHandler += eventHandler;
In Visual Basic .NET, the same result can be accomplished by using the AddHandler method:
'Wire up the callback to our ValidationCallBack routine AddHandler validator.ValidationEventHandler, AddressOf ValidationCallback
The XmlValidatingReader is now associated with your callback function and is fired when you begin the Read process.
Until this point, you have set all the plumbing in place but still have not validated anything. The actual validation occurs when the Read method is called on each element within the XML instance document:
while (validator.Read()) { //Here, you have access to each element. We do nothing with this //because we only want to know if the entire document is valid or not. }
The Read method simply reads the XML instance document and validates the element against the schema. If data is invalid or missing, the event handler is called.
Listing 2.14 contains the entire example in C#.
using System.Xml; using System.Xml.Schema; Public Class XMLFunctions { private Boolean isValid = false; //Private flag to hold validation results from callback public Boolean IsXMLValid(string xsdFile, string xmlFile) { //Instantiate a reader to read the XML file XmlTextReader textReader = new XmlTextReader(xmlFile); //Associate the ValidatingReader with the XMLTextReader object XmlValidatingReader validator = new XmlValidatingReader(textReader); validator.ValidationType = ValidationType.Auto; //Create the schema collection XmlSchemaCollection schemas = new XmlSchemaCollection(); schemas.Add (null, new XmlTextReader(xsdFile)); //Add the list of schemas to validate against to the schemas collection validator.Schemas.Add(schemas); // Wire up the callback to our ValidationCallBack routine ValidationEventHandler eventHandler = new ValidationEventHandler (ValidationCallback); //Wire up the event handler to the validating reader's event handler validator.ValidationEventHandler += eventHandler; while (validator.Read()) { //Here, you have access to each element. We do nothing with this //because we only want to know if the entire document is valid or not. } if (isValid) System.Diagnostics.Debug.WriteLine ("Document is valid."); else System.Diagnostics.Debug.WriteLine ("Document is NOT valid."); validator.Close(); textReader.Close(); Return isValid; } private void ValidationCallback ( object sender, ValidationEventArgs args ) { System.Diagnostics.Debug.WriteLine ("Validation error: {0}" + args.Message); isValid = false; } }
In Listing 2.15, you can see the same code in Visual Basic .NET.
Imports System.Xml Imports System.Xml.Schema Public Class XMLFunctions Private isValid As Boolean = False 'Private flag to hold validation results from callback Public Function IsXMLValid(ByVal xsdFile As String, ByVal xmlFile As String) As Boolean 'Instantiate a reader to read the XML file Dim textReader As XmlTextReader = New XmlTextReader(xmlFile) 'Associate the ValidatingReader with the XMLTextReader object Dim validator As XmlValidatingReader = New XmlValidatingReader(textReader) validator.ValidationType = ValidationType.Auto 'Create the schema collection Dim schemas As XmlSchemaCollection = New XmlSchemaCollection() schemas.Add("urn:schemas-vbdna-net:framework", New XmlTextReader(xsdFile)) 'Add the list of schemas to validate against to the schemas collection validator.Schemas.Add(schemas) 'Wire up the callback to our ValidationCallBack routine AddHandler validator.ValidationEventHandler, AddressOf ValidationCallback While (validator.Read()) 'Here, you have access to each element. We do nothing with this 'because we only want to know if the entire document is valid or not. End While If (isValid) Then System.Diagnostics.Debug.WriteLine("Document is valid.") Else System.Diagnostics.Debug.WriteLine("Document is NOT valid.") End If validator.Close() textReader.Close() Return isValid End Function Private Sub ValidationCallback(ByVal sender As Object, ByVal args As ValidationEventArgs) System.Diagnostics.Debug.WriteLine ("Validation error: {0}" + args.Message); isValid = False End Sub End Class
The example in Listing 2.15 makes validating an XML document against a specified schema easy. Listing 2.16 shows a sample implementation that calls the XMLFunctions class that is defined in Listing 2.15.
Imports System.Xml Imports System.Xml.Schema Public Class WebForm1 Inherits System.Web.UI.Page #Region " Web Form Designer Generated Code " 'This call is required by the Web Form Designer. <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent() End Sub Private Sub Page_Init(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Init 'CODEGEN: This method call is required by the Web Form Designer 'Do not modify it using the code editor. InitializeComponent() End Sub #End Region Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load 'Put user code to initialize the page here Dim test As XMLFunctions = New XMLFunctions() Response.Write(test.IsXMLValid(Server.MapPath("test.xsd"), Server.MapPath("testdoc.xml"))) End Sub End Class
Note the lack of error handling in this example. Both the C# and Visual Basic .NET versions make the assumption that both the XML and XSD files already exist. You might want to expand this example to include a try..catch block to trap errors that can occur when loading the XML or XSD files fails.
The code is fairly straightforward. Again, the concept of readers and writers will be explained in more detail in Chapter 6, "Exploring the System.Xml Namespace".
only for RuBoard |