tags:

views:

357

answers:

4

I have two XML files that are generated by another application I have no control over. The first is a settings file, and the second is a list of changes that should be applied to the first.

Main settings file:

<?xml version="1.0"?>
<preset>
  <var id="9" opt="0" val="6666666"/>
  <var id="9" opt="1" val="10000000"/>
  <var id="9" opt="2" val="10000000"/>
  <var id="9" opt="3" val="10000000"/>
  <var id="9" opt="4" val="0"/>
  <var id="10" opt="0" val="4"/>
  <var id="11" opt="0" val="0"/>
  <var id="15" opt="0" val="75"/>
  <var id="22" opt="0" val="0,0,127,516" type="rect(l,t,r,b)"/>
  <var id="23" opt="0" val="27,18,92,66" type="rect(l,t,r,b)"/>
  <var id="24" opt="0" val="320"/>
  ... Skip 300 lines ...
</preset>

And here is an example of the changes:

<?xml version="1.0"?>
<preset>
  <var id="15" opt="0" val="425"/>
  <var id="22" opt="0" val="0,0,127,776" type="rect(l,t,r,b)"/>
  <var id="26" opt="0" val="147"/>
  <var id="27" opt="0" val="147"/>
  <var id="109" opt="1" val="7"/>
  <var id="126" opt="0" val="6,85,85,59" type="crv(t,m,b,vm)"/>
  <var id="157" opt="0" val="1"/>
  ... Skip 10 lines ...
</preset>

Each variable has an ID and an Optimization that ID applies to. Basically, I'm looking to replace the lines where the id="#" and opt="#" are the same with the version from the "change" file. In the example above, the value for id="15" opt="0" would change from 75 to 425.

Would there be any clean way in doing this in C#? At first thought, reading as text and stepping through the changes using a find-replace type of method seems the cleanest. An approach handling this as an XmlDocument seems like much more work.

+2  A: 

This would be terribly inefficient if the files get very big, but this is how you can do it with XmlDocuments:

XmlDocument main = new XmlDocument();
main.Load( "main.xml" );

XmlDocument changes = new XmlDocument();
changes.Load( "changes.xml" );

foreach ( XmlNode mainNode in main.SelectNodes( "preset/var" ) )
{
    string mainId = mainNode.Attributes[ "id" ].Value;
    string mainOpt = mainNode.Attributes[ "opt" ].Value;

    foreach ( XmlNode changeNode in changes.SelectNodes( "preset/var" ) )
    {
        if ( mainId == changeNode.Attributes[ "id" ].Value &&
            mainOpt == changeNode.Attributes[ "opt" ].Value )
        {
            mainNode.Attributes[ "val" ].Value = changeNode.Attributes[ "val" ].Value;
        }
    }
}

// save the updated main document
main.Save( "updated_main.xml" );
Jeff Hillman
This worked perfectly as-is. As a console application, the total execution time was 21ms for a 584-line XML file with 20 changes, which is great for how it'll be used. Thanks!
Will Eddins
A: 

XmlDocument would be ideal for this process and is far less work than the other method you suggested. You might want to look at other methods to approach this if your using LARGE Xml files as XmlDocument loads the whole document into memory!

Anyways the XmlDocument approach would be something like:

  1. Load both files into their respective XmlDocument objects.
  2. Iterate through the list of var nodes in the changes file XmlDocument object and foreach one run a xpath query on the original file XmlDocument object to search for the node with the matching id (using the SelectSingleNode() method on the original file XmlDocument object) when found edit the attribute you need edited in the node.
  3. Save the file after all the edits.

I know what i'm telling you to do works but i might not be explaining it clearly. I can complete a very rough version of this program in under 30mins and i've only got about a years experience in c#.

Guesty
+2  A: 

Not sure about the efficiency, but this is straightforward with Linq to XML - the follwing was a bit rough - but having remembered that the very wonderful LinqPAD will let you run programs... herewith a complete lump of code that would do the job:

void Main()
{
    XDocument settingsXML = XDocument.Load(@"c:\temp\settings.xml");
    XDocument updateXML = XDocument.Load(@"c:\temp\updates.xml");

    Console.WriteLine("Processing");

    // Loop through the updates
    foreach(XElement update in updateXML.Element("preset").Elements("var"))
    {    
        // Find the element to update    
        XElement settingsElement = 
            (from s in settingsXML.Element("preset").Elements("var")
          where s.Attribute("id").Value == update.Attribute("id").Value &&
                   s.Attribute("opt").Value == update.Attribute("opt").Value
             select s).FirstOrDefault();    
        if (settingsElement != null)    
        {      
            settingsElement.Attribute("val").Value = update.Attribute("val").Value;    
            // Handling for additional attributes here
        }    
        else    
        {   
            // not found handling    
            Console.WriteLine("Not found {0},{1}", update.Attribute("id").Value,
                                                   update.Attribute("opt").Value);
        }
    }
    Console.WriteLine("Saving");
    settingsXML.Save(@"c:\temp\updatedSettings.xml");
    Console.WriteLine("Finis!");
}

Addition of using clauses is left as an exercise :)

There's another example here but its in VB which has more capabilities in terms of XML.

I also think that it might be possible to do something seriously elegant by way of a query with a join of the two sets of XML data generating a list of dynamic types containing an XElement and the value (or values) that it needs to be updated with. But I've had enough fun (spent enough time) with this one already for one evening

Murph
+2  A: 

Example of doing it in Linq to XML using joins to relate the documents together. First I select the elements that match on the two attributes and then I update those to the new value from the change file.

XDocument main = XDocument.Load("XMLFile1.xml");
XDocument changes = XDocument.Load("XMLFile2.xml");

var merge = from entry in main.Descendants("preset").Descendants("var")
            join change in changes.Descendants("preset").Descendants("var")
            on 
               new {a=entry.Attribute("id").Value, b=entry.Attribute("opt").Value}
            equals 
               new {a=change.Attribute("id").Value, b=change.Attribute("opt").Value}
            select new
            {
               Element = entry,
               newValue = change.Attribute("val").Value                       
            };

            merge.ToList().ForEach(i => i.Element.Attribute("val").Value = i.newValue);

            main.Save("XMLFile3.xml");
Brian ONeil
I'd be interested in knowing how this solution compares in performance to the brute-force looping Accepted Answer above.
Joseph DeCarlo