LINQ for XML – the basics

LINQ provides a library of classes and methods that allow XML to be generated and imported quite easily (certainly more easily than with previous .NET libraries).

We’ll assume the reader is familiar with the basics of XML syntax and dive in with a simple little program that allows the user to enter some details for books in a library, then store this data to a disk file as XML (and of course to read in data from an XML file and display it).

The GUI is a WPF DataGrid and a menu for handling file operations, as shown:

We’ll represent the data internally using a Book class to represent each book, and an ObservableCollection to represent the collection of books. The data structures are similar to those that we used in discussing data binding to lists and combo boxes. The Book class is a bit simpler than it was there:

using System.ComponentModel;
namespace LinqXml02
{
  public class Book : INotifyPropertyChanged
  {
    public event PropertyChangedEventHandler PropertyChanged;
    protected void Notify(string propName)
    {
      if (this.PropertyChanged != null)
      {
        PropertyChanged(this, new PropertyChangedEventArgs(propName));
      }
    }

    string author;

    public string Author
    {
      get { return author; }
      set
      {
        author = value;
        Notify("Author");
      }
    }

    string title;

    public string Title
    {
      get { return title; }
      set
      {
        title = value;
        Notify("Title");
      }
    }

    decimal price;

    public decimal Price
    {
      get { return price; }
      set
      {
        price = value;
        Notify("Price");
      }
    }

    public Book() { }
    public Book(string author, string title, decimal price)
    {
      this.author = author;
      this.title = title;
      this.price = price;
    }
  }
}

The ObservableCollection is created in a special class called Library:

using System;
using System.Collections.ObjectModel;

namespace LinqXml02
{
  public class Library
  {
    Random rand = new Random();
    private decimal BookPrice()
    {
      decimal price = rand.Next(0, 5000) / 100m;
      return price;
    }

    public ObservableCollection<Book> GetLibrary()
    {
      ObservableCollection<Book> library = new ObservableCollection<Book>();
      return library;
    }
  }
}

This class serves as a resource in the XAML file:

<Window x:Class="LinqXml02.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:LinqXml02"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <ObjectDataProvider x:Key="LibraryGrid"
                            ObjectType="{x:Type local:Library}"
                            MethodName="GetLibrary"/>
    </Window.Resources>
    <Grid DataContext="{StaticResource LibraryGrid}" HorizontalAlignment="Stretch">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Menu VerticalAlignment="Top">
            <MenuItem Header="_File">
                <MenuItem x:Name="saveMenuItem" Header="_Save" HorizontalAlignment="Left" Width="145" Click="saveMenuItem_Click"/>
                <MenuItem x:Name="saveAsMenuItem" Header="Save _as" HorizontalAlignment="Left" Width="145" Click="saveAsMenuItem_Click"/>
                <MenuItem x:Name="openMenuItem" Header="_Open" HorizontalAlignment="Left" Width="145" Click="openMenuItem_Click"/>
                <Separator HorizontalAlignment="Left" Width="145"/>
                <MenuItem x:Name="exitMenuItem" Header="E_xit" HorizontalAlignment="Left" Width="145" Click="exitMenuItem_Click"/>
            </MenuItem>
        </Menu>
        <DataGrid x:Name="bookGrid" Grid.Row="1" ItemsSource="{Binding}" AutoGenerateColumns="False"  HorizontalAlignment="Stretch">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Author" Binding="{Binding Author}" Width="45*"/>
                <DataGridTextColumn Header="Title" Binding="{Binding Title}"  Width="45*"/>
                <DataGridTextColumn Header="Price" Binding="{Binding Price}"  Width="10*"/>
            </DataGrid.Columns>
        </DataGrid>

    </Grid>
</Window>

On lines 7 to 9 we create the resource, then use it as the data context for the Grid on line 11. The DataGrid defined on line 25 uses this data context as the binding for its ItemsSource property, and then we define the three columns, each bound to a property in the Book class. We could have used the auto-generate column feature of a DataGrid, but that doesn’t allow us to customize the widths of the columns, which we’ve done here by assigning each of the Author and Title columns 45% of the horizontal width, with Price getting the remaining 10%.

With the data structures set up and the binding in place, we could run the program and enter some book data, and the data binding will automatically update the ObservableCollection as we enter data into the DataGrid. However, at this stage we have no way of saving the data thus entered. For that we introduce the XML.

First, we’ll have a look at the event handlers for the Save and Save As menu items.

    string saveFilename = "";
    private void saveAsMenuItem_Click(object sender, RoutedEventArgs e)
    {
      SaveFileDialog saveDialog = new SaveFileDialog();
      saveDialog.Filter = "XML file|*.xml";
      saveDialog.Title = "Save library";
      if (saveDialog.ShowDialog() == true)
      {
        saveFilename = saveDialog.FileName;
        saveMenuItem_Click(sender, e);
        Title = "Library - " + saveDialog.FileName;
      }
    }

    private void saveMenuItem_Click(object sender, RoutedEventArgs e)
    {
      if (saveFilename.Equals(""))
      {
        saveAsMenuItem_Click(sender, e);
      }
      else
      {
        XElement saveLibraryXml = XmlFromLibrary();
        saveLibraryXml.Save(saveFilename);
      }
    }

The SaveFileDialog (and OpenFileDialog) classes are in the old Microsoft.Win32 namespace, but they still seem to work well enough. In order to allow us to save changes to a currently open file, we have an auxiliary string called saveFilename. If this string has zero length, then we open the SaveFileDialog to get the user to select a filename. The dialog has a filter that displays only .xml files.

Once a file has been chosen, the saveMenuItem_Click() handler is called, and the method XmlFromLibrary() is called. We’ll consider this in a moment, but first we need to describe the XElement class.

In LINQ’s handling of XML, all XML tags are represented by XElement objects. There is no need for a separate, top-level document object in which to place the XElements; XElement itself can serve as the top level, and all lower levels.

Nested tags in the XML are represented simply as nested XElement objects. This gives the C# code a structure that is easy to understand for the human reader.

Now we can have a look at XmlFromLibrary():

    private XElement XmlFromLibrary()
    {
      ObservableCollection<Book> library = (ObservableCollection<Book>)((ObjectDataProvider)FindResource("LibraryGrid")).Data;
      XElement libraryElement =
        new XElement("LIBRARY",
          library.Select(book =>
            new XElement("BOOK",
              new XElement("AUTHOR", book.Author),
              new XElement("TITLE", book.Title),
              new XElement("PRICE", book.Price))));
      return libraryElement;
    }

After retrieving ‘library’ from the Windows resources, we create the XML representation of the library with a single C# statement. The top level object is libraryElement, which is given the tag LIBRARY. The second argument to its contructor is built using a LINQ Select() call on library. Remember that library consists of a list of Book objects, so we simply iterate through each Book in the list, and construct a new XElement for each Book. Within the Book’s XElement, we add 3 more XElements for the Author, Title and Price fields.

And that’s it. The code is very clean. Back in saveMenuItem_Click(), we simply call the Save() method from the XElement object to save the file to disk. The resulting file for the books shown in the picture above is:

<?xml version="1.0" encoding="utf-8"?>
<LIBRARY>
  <BOOK>
    <AUTHOR>Asimov, Isaac</AUTHOR>
    <TITLE>I, Robot</TITLE>
    <PRICE>3.50</PRICE>
  </BOOK>
  <BOOK>
    <AUTHOR>Niven, Larry</AUTHOR>
    <TITLE>Ringworld</TITLE>
    <PRICE>4.95</PRICE>
  </BOOK>
  <BOOK>
    <AUTHOR>Asimov, Isaac</AUTHOR>
    <TITLE>Foundation</TITLE>
    <PRICE>2.25</PRICE>
  </BOOK>
  <BOOK>
    <AUTHOR>Simak, Clifford D.</AUTHOR>
    <TITLE>Buckets of Diamonds</TITLE>
    <PRICE>5.00</PRICE>
  </BOOK>
</LIBRARY>

The Save() method produces the usual first line of an XML file, and then writes out the XML itself, all neatly indented.

To read the XML file back into the program, we need to construct the internal ObservableCollection from the XML. This is almost as easy as producing the XML in the first place. Here’s the code for the Open menu item, and the associated LibraryFromXml() method that reads the XML:

    private void openMenuItem_Click(object sender, RoutedEventArgs e)
    {
      OpenFileDialog openDialog = new OpenFileDialog();
      openDialog.DefaultExt = ".xml";
      openDialog.Filter = "XML documents (.xml)|*.xml";
      bool? result = openDialog.ShowDialog();
      if (result == true)
      {
        XElement libraryXml = XElement.Load(openDialog.FileName);
        Title = "Library - " + openDialog.FileName;
        LibraryFromXml(libraryXml);
        saveFilename = openDialog.FileName;
      }
    }

    private void LibraryFromXml(XElement libraryXml)
    {
      ObservableCollection<Book> library = (ObservableCollection<Book>)((ObjectDataProvider)FindResource("LibraryGrid")).Data;
      library.Clear();
      var bookElements = libraryXml.Elements("BOOK");
      foreach (XElement book in bookElements)
      {
        Book addBook = new Book(
          (string)book.Element("AUTHOR"),
          (string)book.Element("TITLE"),
          (decimal)book.Element("PRICE"));
        library.Add(addBook);
      }
    }

In the openMenuItem_Click() handler, we use the static XElement.Load() method to read the XML from the file into an XElement.

In LibraryFromXml() we again retrieve the library resource and clear it of existing data. Then we call the Elements() method on the XElement to retrieve a list of BOOK tags. This produces an IEnumerable list of XElements for the BOOK objects in the original XML. For each of these, we simply create a Book object by extracting the AUTHOR, TITLE and PRICE XElements for each BOOK, and then add this Book object to the library. The data binding takes care of the rest, so the DataGrid is automatically updated to display the list of books we read in.

There’s a lot more that can be done with LINQ and XML, but this little example should show you that for saving and reading basic XML, LINQ is easy to use.

Code for this post available here.

Advertisements
Post a comment or leave a trackback: Trackback URL.

Trackbacks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: