Aki la
Aki la

Reputation: 339

How to change the value of an attribute in a new XML document?

I have a database where is a ton of XML file who's waiting to get update. The update would wipe ALL files in the database. Each new file would be the same as his previous except for the attribute value.

But I want to keep each attributes type that have session as value. This attribute is in each file multiple time. But, it's doesn't work for the moment: attributes doesn't update at all).

How can I change the value of an specific attribute in an new XML file identical depending of another one?

This is what I did so far...

So I decided to find a way to get the path to these attributes using this code for each file before the wipe:

List<XmlList> dBPathTypeSession = new List<XmlList>();
/*reader*/
XmlDocument doc = new XmlDocument();
doc.LoadXml(sessionType.Parameters);

XmlNode root = doc.DocumentElement;

//I think my problem come from this line below
XmlNodeList nodes = root.SelectNodes("//node()[@type='session']");

dBPathTypeSession.Add(new XmlList("b_session_type", i, nodes));//table,row,paths

The code below this line is probably not needed to answer my question, this is just more detail.

And then correct the attributes value after the update with this:

XmlDocument doc = new XmlDocument();
doc.LoadXml(sessionType.Parameters);

foreach (XmlNode node in file.Paths)//XmlList file = new XmlList();
{
    if (node.Attributes["type"].Value == "system")
    {
        node.Attributes["type"].Value = "session";
    }
}
//push to DB

The informations for every path in each file is contain in this:

//But I think this is pointless for my question
public class XmlList
{
    public XmlList(string tablePath, int filePath, XmlNodeList paths)
    {
        this.TablePath = tablePath;
        this.FilePath = filePath;
        this.Paths = paths;
    }
    public string TablePath {get; private set;}
    public int FilePath {get; private set;}
    public XmlNodeList Paths {get;set;}
}

I'm using C# 3.0(.NET framework 3.5), I MUST use XMLDocument to make it fit with everything else in the code.

Here's an example of what the XML look like before update(short version)

<Session is_hidden="false">
    <ID is_static="true">1</ID>
    <SESSIONIDX is_static="true">0</SESSIONIDX>
    <Timing>
        <FirstPresentation display_name="FirstPresentation" type="system"/>
        <Pause display_name="Pause" type="system" datatype="float"/>
        <Feedback display_name="Feedback" type="session" datatype="float"/>
        <Answer display_name="Answer" type="system" datatype="float"/>
    </Timing>
    <Balls>
        <Indexed display_name="Indexed" type="session" datatype="pos_int"/>
        <IndexingColor1 display_name="IndexingColor1" type="system" datatype="list">
            <list>
                <ListItem>RED</ListItem>
                <ListItem>BLUE</ListItem>
            </list>
        </IndexingColor1>
    </Balls>
</Session>

Here's an example of what the XML look like after update(short version)

<Session is_hidden="false">
    <ID is_static="true">1</ID>
    <SESSIONIDX is_static="true">0</SESSIONIDX>
    <Timing>
        <FirstPresentation display_name="FirstPresentation" type="session"/>//system change for session
        <Pause display_name="Pause" type="system" datatype="float"/>
        <Feedback display_name="Feedback" type="system" datatype="float"/>//session change for system
        <Answer display_name="Answer" type="system" datatype="float"/>//system stay system
    </Timing>
    <Balls>
        <Indexed display_name="Indexed" type="session" datatype="pos_int"/>//session stay session
        <IndexingColor1 display_name="IndexingColor1" type="system" datatype="list">
            <list>
                <ListItem>RED</ListItem>
                <ListItem>BLUE</ListItem>
            </list>
        </IndexingColor1>
    </Balls>
</Session>

Here's an example of what the XML of what I'm looking for:

<Session is_hidden="false">
    <ID is_static="true">1</ID>
    <SESSIONIDX is_static="true">0</SESSIONIDX>
    <Timing>
        <FirstPresentation display_name="FirstPresentation" type="session"/>//system stay session
        <Pause display_name="Pause" type="system" datatype="float"/>
        <Feedback display_name="Feedback" type="session" datatype="float"/>//session return session
        <Answer display_name="Answer" type="system" datatype="float"/>//system stay system
    </Timing>
    <Balls>
        <Indexed display_name="Indexed" type="session" datatype="pos_int"/>//session stay session
        <IndexingColor1 display_name="IndexingColor1" type="system" datatype="list">
            <list>
                <ListItem>RED</ListItem>
                <ListItem>BLUE</ListItem>
            </list>
        </IndexingColor1>
    </Balls>
</Session>

If we compare this to boolean algebra, we have this :

For x = session & y = system

before update = after update -> What we want

x = x -> x

x = y -> x

y = x -> x

y = y -> y

If you need more information just ask in comment and I will update the post.

Upvotes: 3

Views: 6717

Answers (2)

Aki la
Aki la

Reputation: 339

The problem with the code in the question is the XmlNodeList:it creates a list of object that is link to the XML files. So when the files get wipe, XmlNodeList means nothing. So, changing this list of XmlNode for a list of string to these XPath would work.

XmlNodeList nodes = root.SelectNodes("//node()[@type='session']");
List<string> xPathList = new List<string>();
foreach (XmlNode node in nodes)
{
    xPathList.Add(getXPath(node));
}

dBPathTypeSession.Add(new XmlList("b_session_type", i, xPathList));//table,row,paths

Where the getXPath function is something like this:

static public string getXPath(XmlNode _xmlNode)
{
    Stack<string> xpath = new Stack<string>();

    while (_xmlNode != null)
    {
        if (_xmlNode as XmlElement != null)
            xpath.Push(_xmlNode.Name);

        _xmlNode = _xmlNode.ParentNode as XmlElement;
    }
    return string.Join("/", xpath.ToArray());
}

The writer would modify the string to XMLNode that will have the link to the good files now.

foreach (string path in file.Paths)
{
    XmlNode node = doc.SelectSingleNode(path);
    if (node.Attributes["type"].Value == "system" && node.Attributes["type"].Value != null)
    {
        node.Attributes["type"].Value = "session";
    }
}
//push to DB

The XmlList class would need to be change to take List instead of XmlNodeList

Upvotes: 1

dbc
dbc

Reputation: 117026

The problem is that you are searching for nodes with an attribute named type with value session -- then replacing that value if the current value is system. That's not going to work, since the value can't be both.

You must want either:

    foreach (XmlNode node in root.SelectNodes("//node()[@type='session']"))
        node.Attributes["type"].Value = "system";

or

    foreach (XmlNode node in root.SelectNodes("//node()[@type='system']"))
        node.Attributes["type"].Value = "session";

Update

If you have two XmlDocuments that have identical element hierarchies but different sets of attributes for each element, and wish to propagate some attribute information from the first to the second, you need to walk the element hierarchies and create temporary mapping tables between them. The following does that, assuming the elements are corresponded by name, and then by order if duplicate names exist (e.g. in a list):

    static void WalkMatchingElements(XmlElement root1, XmlElement root2, Action<XmlElement, XmlElement> action)
    {
        WalkMatchingElements(root1, root2, (element) => (element.Name), action);
    }

    static void WalkMatchingElements<TKey>(XmlElement root1, XmlElement root2, Func<XmlElement, TKey> getKey, Action<XmlElement, XmlElement> action)
    {
        if (EqualityComparer<TKey>.Default.Equals(getKey(root1), getKey(root2)))
            action(root1, root2);
        var children1GroupedByName = root1.ChildNodes.OfType<XmlElement>().GroupBy(getKey);
        var children2LookupByName = root2.ChildNodes.OfType<XmlElement>().ToLookup(getKey);
        foreach (var child1group in children1GroupedByName)
        {
            var child2group = children2LookupByName[child1group.Key];
            foreach (var pair in child1group.Zip(child2group, (el1, el2) => new KeyValuePair<XmlElement, XmlElement>(el1, el2)))
                WalkMatchingElements(pair.Key, pair.Value, getKey, action);
        }
    }

And then call it like:

        var oldDoc = new XmlDocument();
        oldDoc.LoadXml(oldXml);

        var newDoc = new XmlDocument();
        newDoc.LoadXml(newXml);

        WalkMatchingElements(oldDoc.DocumentElement, newDoc.DocumentElement, (elOld, elNew) =>
            {
                var attrOld = elOld.Attributes["type"];
                if (attrOld != null && attrOld.Value == "session")
                {
                    elNew.SetAttribute("type", "system");
                }
            });

Update2 If you don't want the entire old XmlDocument in memory at once (though I don't see why not), you can built a lookup table of elements with a type attribute, indexed by path, then use that later:

    const string AttributeName = "type";

        var lookup = oldDoc.DocumentElement.DescendantsAndSelf().OfType<XmlElement>().Where(el => el.HasAttribute(AttributeName)).ToLookup(el => el.Path(), el => el.Attributes[AttributeName].Value);

        // And then later

        WalkMatchingElements(new XmlElement[] { newDoc.DocumentElement }, lookup, (el, oldValue) =>
            {
                if (oldValue != null && oldValue == "session")
                    el.SetAttribute(AttributeName, "session");
            });


    private static void WalkMatchingElements<TValue>(IEnumerable<XmlElement> elements, ILookup<string, TValue> pathLookup, Action<XmlElement, TValue> action)
    {
        var elementsByPath = elements.GroupBy(el => el.Path());
        foreach (var elementsGroup in elementsByPath)
        {
            foreach (var pair in elementsGroup.Zip(pathLookup[elementsGroup.Key], (el, value) => new KeyValuePair<XmlElement, TValue>(el, value)))
                action(pair.Key, pair.Value);
            foreach (var element in elementsGroup)
                WalkMatchingElements(element.ChildNodes.OfType<XmlElement>(), pathLookup, action);
        }
    }

You'll need the following extension methods:

public static class XmlNodeExtensions
{
    public static string Path(this XmlElement element)
    {
        if (element == null)
            throw new ArgumentNullException();
        return element.AncestorsAndSelf().OfType<XmlElement>().Reverse().Aggregate(new StringBuilder(), (sb, el) => sb.Append("/").Append(el.Name)).ToString();
    }

    public static IEnumerable<XmlNode> AncestorsAndSelf(this XmlNode node)
    {
        for (; node != null; node = node.ParentNode)
            yield return node;
    }

    public static IEnumerable<XmlNode> DescendantsAndSelf(this XmlNode root)
    {
        if (root == null)
            yield break;
        yield return root;
        foreach (var child in root.ChildNodes.Cast<XmlNode>())
            foreach (var subChild in child.DescendantsAndSelf())
                yield return subChild;
    }
}

public static class EnumerableExtensions
{
    // Back ported from .Net 4.0
    public static IEnumerable<TResult> Zip<TFirst, TSecond, TResult>(this IEnumerable<TFirst> first, IEnumerable<TSecond> second, Func<TFirst, TSecond, TResult> resultSelector)
    {
        if (first == null) throw new ArgumentNullException("first");
        if (second == null) throw new ArgumentNullException("second");
        if (resultSelector == null) throw new ArgumentNullException("resultSelector");
        return ZipIterator(first, second, resultSelector);
    }

    static IEnumerable<TResult> ZipIterator<TFirst, TSecond, TResult>(IEnumerable<TFirst> first, IEnumerable<TSecond> second, Func<TFirst, TSecond, TResult> resultSelector)
    {
        using (IEnumerator<TFirst> e1 = first.GetEnumerator())
        using (IEnumerator<TSecond> e2 = second.GetEnumerator())
            while (e1.MoveNext() && e2.MoveNext())
                yield return resultSelector(e1.Current, e2.Current);
    }
}

(I had forgotten that Zip is not in .Net 3.5.)

Upvotes: 2

Related Questions