The problem – I have a lot of event details for a local club in an XML file and I wanted to load them into a C# application which will import them into a Google Calendar (see my earlier article to understand why).
I originally started to write a C# application to maintain the events but Google Calendar provided an easier way to publish them and share event maintenance with other club members. So the application will be hijacked to publish the events to Google Calendar.
C# XML Serialisation is used to load the XML data – I’ve tried it before and it worked OK. This time, though, I’d managed to create a few problems for myself in the schema.
First of all, here is a sample of the XML: –
<AllEvents> <Events month="January 2011"> <Event> <EventDate label="Mon. 3rd">2011-01-03</EventDate> <EventTitle>Re-start after Christmas break</EventTitle> <EventLocation></EventLocation> <EventDescription></EventDescription> </Event> <Event> <EventDate label="Sat. 29th">2011-01-29</EventDate> <EventTime>7:30pm</EventTime> <EventTitle>Burns Night Supper</EventTitle> <EventLocation>Lowton Civic Hall</EventLocation> <EventDescription> <p>7pm for 7:30pm, Tickets £15.50</p> <p><strong><em>Please note the change of venue.</em></strong></p> </EventDescription> </Event> </Events> </AllEvents>
I grouped the events by month to make it easier to display them in month groups – when XSL 2.0 improved the date handling options but I never got around to removing the Month groups – and some elements hold marked up HTML content which turned out to be quite hard to load.
The EventDate element has a text label (which holds a preformatted date – e.g. “Sat. 29th” above). That was one detail I couldn’t get around. Ah, well.
To read (or write) XML data into C# objects I needed some C# objects and something to Read and Write them.
The class diagram above shows the Document and Data classes. I created a framework of classes to handle standard File menu operations (the white classes) which I use in several applications (I’ll publish them eventually). The Event classes are built on top of these to read and write the data.
ThistleEvent holds the event data, the ThistleEventMonth element groups the events into Months matching. The EventDocument class maps to the Events XML file and is managed (Loaded and Saved) by the EventDocumentManager.
XML Serialisation works by mapping C# objects to the equivalent XML elements and attributes. However, sometimes the mapping isn’t exact and you need to indicate how to handle the differences. Have a look at http://msdn.microsoft.com/en-us/library/aa288454(VS.71).aspx for a quick tutorial.
The Code
I’ll walk you through my “Quick and Dirty” code. First, we need to include some references – System classes (for DateTime), Collections (for ArrayList) XML Serialisation classes and my Forms classes (Document, etc.):
using System; using System.Collections; using System.Xml.Serialization; using Kajabity.Tools.Forms;
The EventDocument Class
After the application namespace declaration comes the first of the classes (I’ve grouped them together, for simplicity). It is the EventDocument class:
namespace Event_Manager { [XmlRoot("AllEvents")] public class EventDocument : Document { [XmlElement( typeof( ThistleEventMonth ) )] public ArrayList Events; public EventDocument() { } }
As a document class, this class maps onto the entire contents of the file – which for XML corresponds to the root or “document” element. We indicate it to the XML Serialisation by adding an Annotation to the class: “XmlRootAttribute” (which I have shortened to “XmlRoot”).
This also allows us to change the element name from EventDocument (which would be the default for this class) to “AllEvents” as it is in the XML. Yes, I know I could have saved myself a lot of trouble by keeping the same names between C# and XML – but then there would be no article, would there?
The AllEvents element contains the events grouped into a series of Event Months and these are represented in C# by an ArrayList of ThistleEventMonth classes. The public “Events” attribute of EventDocument has the same name as the XML element so we don’t need to set the name. But we do need to tell the serialisation what type of objects are in the ArrayList – so we add “typeof(ThistleEventMonth)”.
The ThistleEventMonth Class
The ThistleEventMonth class doesn’t need a class level element defining as it has been added as part of the ArrayList declaration in the EventDocument class.
public class ThistleEventMonth { private string month; public DateTime Date; [XmlAttribute("month")] public string Month { get { return month; } set { Date = DateTime.Parse( "1 " + value ); this.month = String.Format( "0:MMMM yyyy", Date ); } } [XmlElement("Event", typeof(ThistleEvent))] public ArrayList ThistleEvents; public ThistleEventMonth() { } public ThistleEventMonth( DateTime date ) { this.Date = date; this.month = String.Format( "0:MMMM yyyy", date ); } }
However, there is a “month” attribute on the element which names the month and year the contained events belong to. I have added a public Property “Month” to wrap the private member variable which is a string. The getter returns the string – in the format “<month> <year>” and the setter expects the same but validates it as well. It also holds the date of the 1st of the Month in the “date” member variable.
The “month” Attribute name is set using the “XmlAttribute” annotation.
Next, the ThistleEvents array is annotated to set the element name and type in the same way as the ThistleEventMonths member in EventDocument.
Finally, we have both a default constructor (for the XML Serialisation) and one which takes a date.
The ThistleEvent Class
This class holds the information I want to upload to the Google Calendar – the Event details. The XML Element name “Event” is set by the enclosing ArrayList in the ThistleEventMonth class, above.
public class ThistleEvent { [XmlAttribute("group")] public string Group; [XmlAttribute("home")] public bool Home; [XmlAttribute("category")] public string Category; [XmlElement(IsNullable=true)] public DateTime? EventDate; [XmlElement(IsNullable=true)] public DateTime? EventDateTo; [XmlElement(IsNullable=true)] public string EventTime; [XmlElement(IsNullable=true)] public string EventTimeTo; [XmlElement("EventTitle")] public object Title; [XmlElement("EventLocation")] public string Location; [XmlElement(IsNullable=true, ElementName="EventDescription")] public object Description; [XmlElement("Program")] public object Program; public ThistleEvent() { } } } // End of namespace
Many of the members of the ThistleElement class follow the same pattern we’ve seen in the classes above: some have the same name as the corresponding XML elements and don’t need the element name in the annotation.
But some elements are Optional and can be missing from the XML. For the string elements, these are read in as “null” but I found that the date elements were being set with a zero date rather than null.
Of course, this is because DateTime is a struct not a class so while the string members are actually a reference to a string which can be replaced with null if it isn’t present, DateTime is held as a value and cannot be set to null.
This makes it harder to identify missing dates so a better approach was needed. I did two things; first, indicate in the Annotation that the element might be missing by adding “IsNullable=true”. Second, I needed to wrap the DateTime with System.Nullable<T>. There is a short cut in C# – which is adding a “?” after the type.
Now the DateTime members are set to null if the corresponding element is absent from the XML data.
For more information about nullable types in C# see http://msdn.microsoft.com/en-us/library/2cf62fcy.aspx.
Elements with HTML Content
You will also notice that the DateTime values – and the bool “Home” value – are automatically converted to or from the text representation in XML to the class data types.
A little more frustrating are the elements which have general marked up content – specifically, HTML. These have an XML Schema type of xs:anyType and contain marked up details of the events – so the Description can include paragraphs and hyperlinks.
A little digging and I found that the element needs to be an “object” type. On loading from XML it is populated with Xml data types. But I wanted it as a simple string .
By debugging I found out a way to extract the content into a string and I created an XmlUtils class with a method to do it for me:
public static string ToString( object o ) { if( o == null ) { return null; } else if( o is XmlNode[] ) { StringBuilder builder = new StringBuilder(); foreach( XmlNode node in (XmlNode[]) o ) { builder.Append( node.OuterXml ); } return builder.ToString(); } else if( o.GetType().Name.Equals( "Object" ) ) { return ""; } else { return o.ToString(); } }
The object can be null (not present in the XML), an XML object hierarchy (plain text or HTML markup), empty (an empty XML element) or possibly something else (?).
I didn’t really need to implement the reverse method but I felt a bit mean not writing it and testing it to complete the article – so here it is:
public static XmlNode FromString( string text ) { XmlTextReader xmlReader = new XmlTextReader( new StringReader( "<X>" + text + "</X>" ) ); XmlDocument xmlDocument = new XmlDocument(); XmlNode node = xmlDocument.ReadNode( xmlReader ); return node; }
Notice that I had to add the “<X></X>” wrapper around the text because the ReadNode method expects a well formed XML document which means a single root element. Without it you will get the exception: “System.Xml.XmlException: Data at the root level is invalid. Line 1, position 1.”
The Description member is optional (marked with “IsNullable=true”) but because it is a nullable type (an Object reference) it doesn’t need the Nullable wrapper unlike DateTime. It also has a different name to the element – “EventDescription”.
Performing the XML Serialisation
Now the classes are properly marked up for XML Serialisation we need to load the data from the XML file.
The EventDocumentManager class extends the DocumentManager class overriding two important methods to read and write the document.
First, we override the Load method:
public override void Load( string filename ) { TextReader reader = new StreamReader( filename ); XmlSerializer serialiser = new XmlSerializer( typeof( EventDocument ) ); document = (EventDocument) serialiser.Deserialize( reader ); reader.Close(); base.Load( filename ); }
The important details to note are the four lines in the middle which create a TextReader to read the file and pass this to the XmlSerialiser which returns an instance of the EventDocument.
Also, to write the XML back to a file override the Save method:
public override void Save( string filename ) { XmlSerializer serialiser = new XmlSerializer( typeof( EventDocument ) ); TextWriter writer = new StreamWriter( filename ); serialiser.Serialize( writer, document ); writer.Close(); base.Save( filename ); }
Again, there are four lines which do the work – create a serialiser for the EventDocument type and serialise it to a TextWriter.
Extra Data In Serialised XML
What a surprise when I examined the XML files created by the application – there are lots of extra XML elements! Here is a sample:
<?xml version="1.0" encoding="utf-8"?> <AllEvents xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Modified>false</Modified> <Name>events.xml</Name> <Events month="0:MMMM yyyy"> <Date>2006-04-01T00:00:00</Date> <Event home="false"> <EventDate>2006-04-07T00:00:00</EventDate> <EventDateTo xsi:nil="true" /> <EventTime>7:30pm</EventTime> <EventTimeTo xsi:nil="true" /> <EventTitle>Spring Ball</EventTitle> <EventLocation>Lowton Civic Hall, Lowton</EventLocation> <EventDescription /> </Event> <Event home="false"> <EventDate>2006-04-28T00:00:00</EventDate> <EventDateTo xsi:nil="true" /> <EventTime xsi:nil="true" />
The “xml” declaration is welcome as are the xmlns:xsi and xmlns:xsd attributes but the Modified and Name elements shouldn’t be there – nor should the Date element in the “Events” element.
I needed to add the attribute “[XmlIgnore]” to the ThistleEventMonth member “Date” – that removed the “Date” element from within the “Events” element.
I can do the same for the members of the Document class – but that then affects all the other applications that use it – most of which don’t use XmlSerialisation. As it doesn’t affect the application for a one off, I’ve ignored it – but I would be interested to hear from anyone who has a decent solution.
Using XSD To Generate Classes
OK, having gone through the agro of manually annotating the classes for XML Serialisation, I found that the XSD utility in the .NET framework SDK does the job for me.
I have the Windows 7/.NET framework installed and I tried it out. It generated some rather complicated classes and I’m not sure they were exactly what I wanted – so I’ll stick with the hand crafted ones after all.
Please, give it a try – it might save you a load of work. You can find some details at http://msdn.microsoft.com/en-us/library/x6c1kb0s(VS.71).aspx.
In the next article in this series, Adding Calendar Events to Google Calendar in C#, I describe how I loaded the data into Google Calendar.
Why don’t you share your entire code. I found its very useful because I was trying to build one Google Calendar application.
As requested I’ve collected the code into a zip file. I should point out that this was going to be an event manager but I hacked it to just upload the events to Google Calendar. It’s not pretty. However, it does have the code to read and write the data to/from XML files.
Also, it’s based on some Document/File handling classes I’m working on with Kajabity Tools and I’m not quite ready to release them yet – so these are missing and the app won’t actually run! Sorry.
I’ve moved the calendar URL, user name and password to the app.config file in the Properties directory.
event-manager-2011-07-21.zip
Note: you use the code at your own risk.
The References used are:
Google.GData.AccessControl
Google.GData.Calendar
Google.GData.Client
Google.GData.Extensions
Kajabity.Tools (not yet released)
System
System.Configuration
System.Core
System.Data
System.Data.DataSetExtensions
System.Drawing
System.Windows.Forms
System.Xml
System.Xml.Linq
Hope this helps…
Great ! Thank you
Kajabity.Tools (not yet released)
Hello
Have you finished your Document and DocumentManager tool ?
I’m really expecting them
Cordially
Davy
Er… kind of. Just never got around to releasing them. I have a few simple apps to illustrate them as well.
I’ll look into it next week and publish what I can.