views:

668

answers:

3

I'm rendering display objects to the stage depending on the given XML elements, as you can see here:

PageRenderer.as

private static var curElements:Dictionary = new Dictionary();

//renders the current page
        private static function renderCode(pageCode:XML):void
        {

            if (pageCode)
            {
                //loop all elements from top to bottom
                for each (var child:XML in pageCode.descendants())
                {
                    var render:DisplayObject = ElementRenderer.render(child);
                    if (render)
                    {
                        WebBuilder.mainDisplay.addChild(render);
                        curElements[child] = render;    
                    }
                }   
            }
        }

So, each XML element has an associative rendered shape. If you have the XML element, you can access the shape like this: var shape:DisplayObject = curElements[xmlElement];

This works fine within the same class.

However, now I also have the ElementSelector class which deals with selection of shapes and reflects the actions done with the shape to the xml element. To do this, one needs to get the XML element when the shape has been clicked:

ElementSelector.as

private static var currentSelection:XML;    

//fired when the stage has been clicked
            private static function stageClicked(event:MouseEvent):void
            {
                //if the element selector has been enabled
                if (enabled)
                {
                    var allPages:Array = CodeHandler.getPageCodes();
                    var mainElement:XML = allPages[CodeHandler.getCurPageId()];
                    var targetElement:XML = CodeHandler.getDeepestElementAtPos(mainElement, 0, event.stageX, event.stageY)["xml"];
                    if ((targetElement.localName() != "page") && (targetElement != currentSelection))
                    { //we have a new element selected
                        Utils.log("ElementSelector now selecting: " + targetElement.localName());
                        select(targetElement);
                    }
                    else if ((targetElement.localName() == "page") && (currentSelection))
                    { //the selection has been lost
                        Utils.log("ElementSelector now dropping selection.");
                        deselect();
                    }
                }
            }

            //sets the new selection
            private static function select(element:XML):void
            {
                if (currentSelection) deselect();
                currentSelection = element;

                var curElements:Dictionary = PageRenderer.getElements();
                var render:DisplayObject = curElements[element];
                trace(render);
            }

    //drops the current selection
            private static function deselect():void
            {
                currentSelection = null;
            }

I added the StageClicked event function only so you have an idea of how my procedure works. That function itself works fine. The problem seems to lie within the select() method.

Now, the strange thing is, that curElements[element] returns undefined and render returns null.

I tried to debug it like this (at bottom of select method):

for (var key:Object in curElements) 
                {
                    if (key === element)
                    {
                        trace("key === element");
                    }
                                trace(curElements[key] === curElements[element]);
                    trace(curElements[key]);
                    trace(curElements[element]);
                }

It returns:

key === element
false
[object Shape]
undefined

Why is this happening? If a === b, then dic[a] === dic[b], right? Well, apparently not.

So, the key really is there... and it's the same as the key it was set with.

Why isn't it returning the associative display object?

A: 

Dictionary uses strict equality (===) for key comparison on non-primitive object keys. So key == elements might return true but key === elemets will be returning false in your case. As element and key object are different.
Try using associative array instead of Dictionary.

bhups
Associative arrays only allow strings as keys while I want to use objects as keys, which is what a dictionary is meant for. Thanks for clearing up why my key == elements evaluation is returning true. Still, why is the dictionary with this key undefined yet I've set it earlier in a different class?
Tom
Actually, while I assumed that your == comment would mean that === would return false, it actually still returns true. So the key === element. I've updated the original code to reflect this.
Tom
it seems that either your pageCode XML object and mainElement XML object are not same (by same I mean ===) or targetElement object in stageClicked function is different from child object on renderCode function.(again ===)
bhups
curElements[key] does return the display object. key === element, according to my debug trace - and yet curElements[element] !== curElements[key]. What you're saying would make sense but I've checked it and the keys and values are the same (===).
Tom
See my updated code in the question to see what I'm talking about.
Tom
+2  A: 

I'm sorry if this doesn't help, but I think that the way you've constructed your application, using XML objects as dictionary keys, is flawed in the first place. I would greatly suggest having some other way to index the elements, such as having an id attribute in the XML that is used for the key. Or you could simply use an incremented integer index.

Another option would be to create a class (or interface) for the objects returned by ElementRenderer.render(), and have your ID as a property in that interface:

public interface IRenderedElement
{
  function get id() : String;
}

You could then store your elements in a flat array or Vector.

If the reason you are using the XML nodes is that you need to store the data that is declared in the XML, then I would strongly suggest that you parse the XML and store the data in a custom Element class instead of inside the weakly-typed, dynamic XML object. The above interface would be a good way to place properties for such data.

If all you need to do is find which element was clicked, then I would suggest adding event listeners to all of the elements, and using the Event.currentTarget property to identify the element.

var element:DisplayObject = ElementRenderer.render(child);
render.addEventListener(MouseEvent.CLICK, handleElementClick);

/* ... */

function handleElementClick(ev : MouseEvent) : void
{
  var element : IRenderedElement = ev.currentTarget as IRenderedElement;

  // do something here
}

You could also simply listen on the parent of all the elements, i.e. WebBuilder.mainDisplay, and then use the Event.target property to find the exact object that was clicked.

richardolsson
I don't understand why it is a flaw to use XML objects as keys. These XML elements are the code-representations of the associated screen-representations. The code elements are what form the rendered elements, and thus they are linked with each other. As far as I know, that is the whole point of a dictionary - to associate objects with each other. The way you want to do it seems like an unneeded amount of overhead, aswell as code to me.
Tom
With your identifier system, you'd have to loop through all elements to find the corresponding ID everytime you need to access the screen representation of the code associative. I also don't understand how curElements[key] === curElements[element can be false while key === element is true. Do you know anything about that? Also please don't think that I am stubborn and don't want to do what you suggest, but with the limited elaboration you gave - it doesn't seem like a better option than a dictionary. So if you can convince me further, please do.
Tom
If you need a lot of random access, you are totally right that it's a good idea to keep a hash table of some sort, e.g. a dictionary or Object with string keys. You could still do that without using the XML nodes, though, e.g. with numerical keys in a Vector (fast!.)Besides, XML is a "proxy" class, where all properties are evaluated dynamically. That might be causing your troubles. I strongly encourage you to parse XML only once, on init, and then store data in data models/value objects. That will likely solve all problems! Hope this helps, or I could edit my post to elaborate.
richardolsson
I see why you encourage me to use the identifier instead now. I'll upvote and accept your answer if no direct answer is given to the "curElements[key] !== curElements[element]" problem. I do not really understand why you would copy all data from a xml node to an other data object. At the moment I can change the data of a node by accessing any reference to it and the complete tree will contain the updated node. If I would create a separate handler, I would have to manually update the tree and each node all the time. I think the XML object is made to work with XML objects, which this is. Thanks.
Tom
Two reasons why I would prefer storing the data as properties on your actual models instead of keeping the XML around: 1) performance; XML is weakly typed and dynamic == relatively slow, and 2) documentation; using strongly typed data, the models will in large be self-documenting, and code-completion will give you easy access to it. I'm sorry I can't help you with the dictionary key issue, but hopefully this is a viable workaround. Cheers!
richardolsson
Accepted your answer. Personally I'll still use XML for data handling simply because I work with XML data, and that's exactly what the XML object is made for. The performance is not really an issue in this application (it is not a real-time game or something) and the documentation is actually pretty good for the XML document and everything related. I am now assigning a unique identifier to each element though so I can call the associative render with curElements[element.@id]. Thanks!
Tom
A: 

I've run across this problem, and couldn't find a solution. But I think I at least understand what's happening.

If you trace the value of your 'child' node in your code above, like this:

for each (var child:XML in pageCode.descendants()) { trace(child); }

you'll get the VALUE of that node. So if the node is content, 'content' will trace out.

This is maybe annoying, probably surprising. Most other actionscript objects trace their object type by default when traced. But numbers and strings will trace their value, so maybe it's not so surprising...

I've done some tests, and it seems like xml nodes can be added to a dictionary just fine. If you do so and trace the contents of the dictionary, they'll be there. And you can even verify that the keys of the dictionary are strictly equivalent to the nodes in your original xml.

But when you try to retrieve the value from the dictionary using the node as a key, it fails. My theory is that when try to get the value you want by saying:

curElements[child]

the value between the brackets is being evaluated and returning its node value, not the object itself. So, if this was the node you added: content, the compiler thinks you want to retrieve curElements["content"] from the dictionary. And that was never added to the dictionary, so it returns null or undefined.

Well, if I'm right, and I think I am because the same problem drove me crazy for several hours this morning, then this is a drag! Despite the other posters comments, it would sometimes be very convenient to use xml nodes as dictionary keys. There are certainly cases where it's the simplest path.

Shmimpie
Alas, I entered some tags in my answer and forgot that they wouldn't make it to the page. "So, if this was the node you added: content" should read "So, if this was the node you added: <tag>content</tag>"
Shmimpie
Similarly, "So if the node is content, 'content' will trace out." should be"So if the node is <tag>content</tag>, 'content' will trace out."
Shmimpie