December 30, 2005
|
Updating an XML file
|
5426 hit(s)
As I blogged before, I've been playing with keeping my collection of quotations in an XML file. I noted then that I created the XML file by saving a dataset as XML, which made each column into an element. I used an XmlDataSource control and a DataList control to display the quotes on a page. But the XmlDataSource control prefers to read attributes as columns, so I had to apply a transformation. Still, it worked.
Bashing on, I'm interested in making the XML file updatable -- either by editing existing quotations (not that I ever make any typing mistakes, no) or by inserting a new quote. Everyone knows, I think, that the XmlDataSource control is effectively read-only; it doesn't support any update methods, unlike, say, the SqlDataSource control. So for sure my original goal, which was to see if I could this without coding, was not attainable. The question then became how much code would be required to create an ASP.NET page that allowed me to edit and add quotations to the XML file.
The first task was to set up editing. I figured that I'd use a GridView control with paging to show the quotes and a DetailsView control to edit or insert individual quotes -- in other words, a master-detail relationship. As it happens, there's a walkthrough that illustrates this.
The walkthrough uses two XmlDataSource controls. The first (with transform) gets all the records for the GridView control. The second (also with transform) gets a single record for the DetailsView control, using an XPath expression (created dynamically) to find the record currently selected in the GridView control.[1] My first problem was that the quotations don't have a unique ID, so I couldn't use a lookup like that illustrated in the walkthrough.
I initially thought I could use the selected index value from the GridView control to create an XPath expression for the DetailsView control, like this:XmlDataSource2.XPath = String.Format("//Quotation[{0}]" , GridView1.SelectedIndex) And it worked -- for the first page of the GridView control. If I paged to the second or any other page, the index value was zero-based for that page. So no good. (Unless I've overlooked some way to get the absolute index of a record --?)
I tinkered with the dataset I originally used to export the quotations to the XML file, adding an element with a consecutive number named quoteId , so now I had a unique ID. I had to update the transformation, too. And add the quoteId to the DataKeys property of the GridView control. But after all that I could use the following expression to read an individual record (the "@" indicates that I'm searching an attribute, thanks to the transformation):Dim currentIndex As Integer = GridView1.SelectedIndex Dim quoteId As String = CStr(GridView1.DataKeys(currentIndex).Value) XmlDataSource2.XPath = String.Format("//Quotation[@quoteId={0}]" , quoteId) A little wordy, but workable. Did I mention that I'm doing all this in the GridView1_SelectedIndexChanged handler? Probably you figured that out.
Since I knew I was going to edit the quote -- that's the purpose of selecting it -- I also set edit mode for the DetailsView control:DetailsView1.ChangeMode(DetailsViewMode.Edit) That's a bit of a change from how we did things in 1.x, if I recall correctly.
Here's the complete handler:Protected Sub GridView1_SelectedIndexChanged(ByVal sender As Object, _ ByVal e As EventArgs) Dim currentIndex As Integer currentIndex = GridView1.SelectedIndex Dim quoteID As String quoteID = CStr(GridView1.DataKeys(currentIndex).Value) XmlDataSource2.XPath = _ String.Format("//Quotation[@quoteId='{0}']", quoteID) DetailsView1.ChangeMode(DetailsViewMode.Edit) End Sub Ok, now I have a single editable record in the DetailsView control. I edit it and now I want to write it back to the XML file. I already know that this means I have to write the whole XML file out; there's no way to update just a single element in the file. (I should mention here that this would not work well for any scenario involving concurrency, and probably for anything that needs to scale. But I'm the only one updating and I'm unlikely to scale myself out of a simple quotations file, I am thinking.)
To update, I handle the DetailsView control's ItemUpdating event. There I can get the edited values like this:Dim oldKey As Integer = e.Keys.Item(0) Dim newAuthor As String = e.NewValues.Item(0) Dim newQuote As String = e.NewValues.Item(1) Dim newSource As String = e.NewValues.Item(2) But what XML file should I update? Well, the XmlDataSource control for the GridView control has already read the entire XML file, and makes it available via its GetXmlDocument method. The problem, though, is that it returns a version of the document after the transformation has been applied. I really need the original version so I can update that and write that back.
In the end, I manually get the XML file, find the element to update, update it, and write the file back. It isn't a lot of code, but it's basically doing almost everything by hand (with the help of an XPath expression, of course). Here's the complete handler.Protected Sub DetailsView1_ItemUpdating(ByVal sender As Object, _ ByVal e As DetailsViewUpdateEventArgs) Dim oldKey As Integer = e.Keys(0) Dim newQuote As String = e.NewValues(0) Dim newAuthor As String = e.NewValues(1) Dim newSource As String = e.NewValues(2)
' Get XML file/document Dim quotesXmlDoc As New System.Xml.XmlDocument Dim xmlFileName As String = Server.MapPath("~") & "\quotes\quotations.xml" quotesXmlDoc.Load(xmlFileName)
' Locate node Dim quoteId As String = e.Keys().Item(0).ToString() Dim quoteNode As XmlElement quoteNode = quotesXmlDoc.DocumentElement.SelectSingleNode( _ String.Format("//Quotation[quoteId='{0}']", quoteId) )
' Replace quoteNode("author").InnerText = newAuthor quoteNode("quote").InnerText = newQuote quoteNode("source").InnerText = newSource
' Write out updated file quotesXmlDoc.Save(Server.MapPath("~") & "\quotes\quotations.xml")
e.Cancel = True End Sub Notice that I create the path to the XML file manually. I thought for sure that I could get the path and file name from the XmlDataSource2.DataFile property. The property returns the string ~/quotes/quotations.xml , which sure looks right. But! Aha! The ~ operator isn't recognized by types in the System.Xml namespace, basically -- that's strictly an ASP.NET operator. If I thought it was important, I could do some string stuff to fix up the path returned by XmlDataSource2.DataFile, but it isn't an issue at the moment.[2]
Finally, the last thing I do is to set the Cancel property to true to stop processing. I've already updated the file; I don't actually want the update information to be passed to the XmlDataSource control, where it would be, er, unwelcome.
Coming soon: inserting a new record.
|