Advanced E4X – Assignment to XMLLists

I was playing around with E4X last night working on an upcoming blog post about the capabilities of E4X. While playing, I was thoroughly reading the ECMA-357 standard, and found that there are some special rules about assignment and compound assignment, that produce truely unexpected results.

I will try to summarize common pitfalls and provide valid work-arounds.

Setup

This post will for most examples work on the following document:

var e:XML = <employees>
	<person>
		<name>John</name>
		<age>35</age>
	</person>
	<person>
		<name>Joe</name>
		<age>25</age>
		<hobby>Swimming</hobby>
	</person>
</employees>

Sometimes other documents will be used explicitly.

Direct children

How would you go about changing the hobby of Joe to running? Well, we could traverse all the way to the node:

e.person[1].hobby = "Running";

This of course works as expected. Actually, we ought to be able to skip the index [1], as only the second person has a hobby and thus only this node ought to be the target for the update:

e.person.hobby = "Running";

But no, this throws a runtime-error:

TypeError: Error #1089: Assignment to lists with more than one item is not supported.

But it is not a list with more than one item, is it?

trace(e.person.hobby.length()); // traces 1

The issue here (I expect, I had a hard time figuring out what the spec actually said about it) is, that the second-to-last list in the assignment target has more than one item:

trace(e.person.length()); // traces 2

This can be tested by introducing some extra level of nodes and try to update one of these:

var more_nodes:XML = <employees>
	<person>
		<name>John</name>
		<age>35</age>
	</person>
	<person>
		<name>Joe</name>
		<age>25</age>
		<hobby>
			<primary>Swimming</primary>
			<secondary>Sewing</secondary>
		</hobby>
	</person>
</employees>
trace(more_nodes.person.length()); // traces 2
trace(more_nodes.person.hobby.length()); // traces 1
trace(more_nodes.person.hobby.primary.length()); // traces 1
more_nodes.person.hobby.primary = "Running"; // no error, updated as expected

Voila, it works. What this means is, that the error is not related to the actual XMLList, that you are assigning to – but to the XMLList, that is the parent of this in the statement. We could actually have reached the desired result by adding an extra level to reach the text node inside the (single) hobby node:

e.person.hobby.* = "Running"; // no error, updated as expected

In this case again, the second-to-last XMLList (the target object for the assignment) has a length of 1, and the update can be accomplished.

Descendants

But if we really just wanted to update the single hobby, that we know only one of the employees have, then we would normally try:

e..hobby = "Running";

Now things start to get really interesting. Because, what happened? No error was thrown, but the document after this update looks like:

<employees>
  <person>
    <name>John</name>
    <age>35</age>
  </person>
  <person>
    <name>Joe</name>
    <age>25</age>
    <hobby>Swimming</hobby>
  </person>
  <hobby>Running</hobby>
</employees>

That is, the existing hobby was unchanged, but a new hobby node has been appended to the end of the outermost node? The statement works the same, as if I had just written:

e.hobby = "Running";

Which would do exactly that – create and append a new hobby node to the outermost node and set its text value to “Running”. But the two lists aren’t identical in the original document:

trace(e..hobby.length()); // traces 1
trace(e.hobby.length()); // traces 0

But again, assignment to xml (and xmllists) does not really have to do with the last part of the assignment target but the penultimate. And again, adding an extra level to reach the text node(s) works:

e..hobby.* = "Running"; // no error, and this time updated as expected

Why the previous statement seemed to ignore the descendant-part of the statement, I’m not really sure. Could be a bug, could be a loop-hole, but it definitely does not make sense.

There are many other ways to reach the same list of the single hobby node when simply consuming, but they cannot all be used in the same way when updating:

e..*.hobby = "Running"; // throws the same Error #1089 about more-than-one element lists
e..*.hobby.* = "Running"; // and it works again
 
e.descendants().hobby = "Running"; // throws the same Error #1089 about more-than-one element lists
e.descendants().hobby.* = "Running"; // and it works again
 
e.descendants('hobby') = "Running"; // compile-time error
// the parser simply does not allow assigning to a function invocation
e.descendants('hobby').* = "Running"; // and it works again
 
e..*.(localName()=='hobby') = "Running"; // compile-time error
// the parser simply does not allow assigning to (what looks like) a function invocation
e..*.(localName()=='hobby').* = "Running"; // and it works again

Conclusion

Be aware that assignments to XMLLists does not care about the last list – it is the second-to-last list, that must contain only one value. Internally, a statement like:

expression.property = value;

Is not a new thing at all, but it is handled in a new, special way, when expression has a value of either XML or XMLList. Thus, if you either get compile-time errors, runtime-errors or nodes appended in completely wrong places, just add extra level of element traversal with the wildcard-identifier .* to reach the text node of the single node you want to update.

No related posts.

Category: AS3, E4X Comment »


Leave a Reply



Back to top

     

Get Adobe Flash playerPlugin by wpburn.com wordpress themes