Introduction
Recently, after submitting a Beginner's tip
here at CodeProject on rewriting XML files, someone who follows my
work suggested I use XLINQ
(now in System.Xml.Linq
).
I decided to take him up on that offer. I would classify
this tip as Intermediate and suggest checking the earlier
tip as needed.
Thank You, Microsoft
Microsoft has indeed made wonderful LINQ extensions to handle
XML. The trick is knowing whether these extensions really buy you
that much, given your requirements and objectives. I'd like
to elaborate somewhat on these from my previous
tip:
- Suppose you have Windows machines with existing XML files set
for a particular customer environment, that is, the settings in
the XML files are not just some set of standard or default
values. Suppose you want to change a few settings to run
a test. When you are through, you want to restore the
existing XML files to what they were before the test.
- An objective is to leave no or as few changes as possible,
down to even the whitespace, after the 'restore'. Another
objective is to avoid making any temporary files on the test
system.
- Some means of specifying values to be changed and restored is
needed. It has to be general enough to specify any element
value in your XML files. This is where
XLINQ
should have helped, but I found building the queries to find this
element Name, then from there find that attribute
value, then from there set the next element's value to "X
" to be
less intuitive and efficient than the Dictionary
's
and reader/writer code used in my previous tip. More about
this later.
- For CodeProject, another objective was to show a tested,
simple solution using easy to follow code.
Let's get into XLINQ
then. There are two
basic approaches to modifying an XML file. One uses streams
where you parse each XML node and handle state and value changes
from beginning to end. This is the world of XmlReader
and XmlWriter
that were used in the helper classes
in the previous tip.
The other approach is a document object model (little d.o.m.)
where the entire XML file is represented in memory. This
in-memory object can be queried something like a database to get
and set values.
XLINQ
uses both, but to get an XDocument
into memory from a file, you pretty much have to use XDocument.Load
. Here is a blurb from MSDN regarding XDocument.Load(String)
used in this tip:
This method uses an underlying XmlReader
to read the XML into an XML tree.
The Load(Stream)
method has this blurb (Yes, we
have to use XmlReaderSettings
since one of our goals is to PreserveWhitespace):
If you have to modify XmlReaderSettings,
follow these steps:
-
Create an XmlReader
by calling one of the Create
overloads that take XmlReaderSettings
as a parameter.
-
Pass the XmlReader
to one of the Load
overloads of XDocument
that takes XmlReader
as a parameter.
The Load(TextReader)
method has this blurb:
LINQ to XML's loading functionality is built upon XmlReader.
Therefore, you might catch any exceptions that are thrown by the
XmlReader.Create overload methods and
the XmlReader
methods that read and parse the document.
The Load(XmlReader)
method has, well, my point is
this: Since we need to create readers and writers to get and
save XDocument
s, we should be convinced XLINQ
provides enough incentive for its use when our goal is to simply
replace and restore some XML values.
Some of the Rewrites
I'd like to point out some of the differences used in this
tip. For the wpml-config.xml sample, I have used
'full-blown' XLINQ
. Keep in mind that I am still
learning the true power and glory of System.Xml.Linq
,
but I think this is what my follower had in mind. Instead of
using Dictionary
s and since we are dealing with a
'database', we use a 'Stored Procedure' as if it were part of the
database:
private string SaveValue3;
private string ReplValue3 = "#fabdec";
private bool StoredProcedure3(XDocument xdoc, int caseNum)
{
IEnumerable<xelement> els =
(from el in xdoc.Root.Elements("language-switcher-settings")
select el);
var xx = els.First();
IEnumerable<xelement> keys =
(from key in xx.Descendants()
where key.Attribute("name").Value == "background-other-normal"
select key).Skip(1).Take(1);
switch (caseNum)
{
case 1:
SaveValue3 = keys.First().Value; break;
case 2:
keys.First().Value = ReplValue3; break;
case 3:
keys.First().Value = SaveValue3; break;
}
return true;
}>
Note this is for just one element value change. The
previous tip replaced and restored two different elements starting
from two named elements ("wpml-config
" - one occurrence, and "key
"
- two occurrences) using helper classes. These helper
classes are retained in this tip's solution for your inspection.
This stored procedure does however address a point made in the previous
tip. Given:
="1.0"="UTF-8"
<root>
<element>One</element>
<element>Two</element>
</root>
"Without any attributes to hang its hat on, it's not possibly[e]
with this current code to change just the second element's text
value of Two
. You would need to
change the code to replace only the Nth
matching element."
If you look at StoredProcedure3
above, you will see
the second query is qualified with ".Skip(1).Take(1)
". We
only replace the value of the second occurrence of the
"background-other-normal
" "name
"-attributed "key
" element.
Which is kind'a nice I have to admit. Grudgingly.
For the other samples, in place of the XmlSaveReader
and XmlReWriter
helper classes, we have these two
methods in the MainWindow
class:
private bool Save(XDocument xdoc, Dictionary<string, Tuple<string, string, int>> saveElemVals)
{
bool inElem = false, getNextText = false;
int nesting = 0;
...
return success;
}
private bool ReWrite(string fname, Dictionary<string, Tuple<string, string, int>> replElemVals)
{
try
{
...
}
catch (Exception ex)
{
throw ex;
}
return true;
}
These handle XML files as XDocument
s. You will
notice ReWrite
(and the full-blown stored procedure)
handle conformance level on their own. Each uses this method:
private void SaveDocOrFrag(XDocument xdoc, string fname)
{
if (xdoc.Declaration == null)
{
System.Xml.XmlWriterSettings xws =
new System.Xml.XmlWriterSettings() { Indent = true, OmitXmlDeclaration = true };
using (var fs = new FileStream(fname, FileMode.Create))
using (System.Xml.XmlWriter xw = System.Xml.XmlWriter.Create(fs, xws))
{
xdoc.Save(xw);
}
}
else
new XDocument(xdoc.Declaration, xdoc.Root).Save(fname);
}
Old Wrinkles and New Wrinkles
Both the old solution and this one have common wrinkles I haven't
mentioned before. They both make these changes:
- ANSI line endings (single line feed) are converted to the PC's
carriage return / line feed pairs.
- Byte Order Marks (BOM) are added to saved files (probably a
good thing).
I made some effort in the previous solution to retain XML file
contents down to the original whitespace. Using XLINQ
is a tradeoff that increases file differences. The tradeoff is
managing less state but more dependence on the faithfulness of XDocument
's
Load
and Save
methods. Using XDocument
s
creates the following changes:
- Partial declarations are replaced with declarations giving
both version and encoding.
- Uppercase encoding values "UTF-8" are set to lower case
"utf-8".
- Root element indentations are removed (Actually improves
Config2.xml's style, but it's still an unwanted change).
7 Or 8 Years Ago
The link to Microsoft
Visual Studio Code Name “Orcas†Language-Integrated Query, May
2006 Community Technology Preview still works; however, when
you try to apply it today, you get:
When you resurrect Visual Studio 2005 on your machine, this preview
installs OK. When this is done, building and running
this CodeProject
article's demo application1 works. I would say that this
app is susceptible to extraneous button pushing crashes, but only if
I were mean spirited.
Bottom Line
My previous tip is small and adequate for the task I had at
hand. I shared it here at CodeProject in hopes this simple
approach could help beginners. If you are comfortable with Linq
you can use this tip's solution as a starting point. Or, I
notice searching for "XLINQ" on CodeProject there are about 50
articles from 24 May 2007 (yes, the very first one. big
deal. but I'm not being personal) to 1 Jan 2011. Searching for "Xml.Linq
" matches two articles (a Tip and a
Technical Blog) dated 23 Aug 2011 and 20 Mar 2013, so I don't feel
too bad showing up here in 2014.
I want to thank Sacha Barber for pointing out XLINQ
. He's good reading and I used his class diagram in making this
rewrite.
History
- Submitted to CodeProject on Sunday, 30th March,
2014