*** Update: This is NOT fixed in Flash CS4, nor in the 10.0.2 update. I guess Adobe is in denial that stage.invalidate() is broken, or they just don’t have the dough to throw at QA to get 3 lines of code + an updated set of SWC’s. *sigh* ***
…and end the drawNow madness.
I knew something was SERIOUSLY wrong when I read in the official docs (Part 3) from Adobe that if your code doesn’t work correctly, try using “drawNow”, and failing that, “validateNow”. What do you mean “try”? What… the… hell…
I was further curious when I dug in the source code. The Flex 2 & 3 components do not have this problem, why Flash? It turned out to be a bug in the Flash Player. The old way of doing invalidation in the Flex 1.5/1 & Flash 8/MX 2004/MX components was to utilize onEnterFrame. If you don’t know what invalidation is, I’ve written about it here under “Provides Invalidation”. In a nutshell, it allows a component to only redraw once per frame even if you set a property 1000 times.
Now that ActionScript 3 is supposed to be all pure, and have nothing to do with frames and what not, the new way to handle invalidation is to utilize stage.invalidate. This method triggers the Event.RENDER event for all those who are listening.
Problem? You cannot call stage.invalidate() WHILE executing code that was triggered from an Event.RENDER.
Invalidation is powerful in that you can have a Label component that is very efficient in only redrawing it’s text and style changes once per frame. In turn, that Label component is then put into a Button component who does his own invalidation. When he sets the text, or sizes the Label, you can be sure that it’s ok if his methods aren’t written very well, and he accidentally has a situation where the Label has it’s text set more than once per frame, or perhaps he sizes it twice. This process repeats itself as more and more complicated components are built using other components via Composition.
This nested invalidation, however, doesn’t work in the Flash CS3 components because of that very bug. My guess is, the crew building them didn’t know about this bug, and started implementing a convention of calling drawNow when stuff didn’t work right. When they added drawNow, it did, and thus they started to have confidence grow in using drawNow . Since this is AS3, you don’t really notice the performance impact since you’re busy building components and testing really small use cases, not larger applications. The drawNow method basically doesn’t wait a frame; it just draws right now.
Why doesn’t Flex have this problem? It uses both; Event.RENDER, and you guessed it, Event.ENTER_FRAME.
If you’re a Flash Developer, it’s an easy fix. You can replace the existing 2 functions in fl.core.UIComponent with the code below. If you’re doing AS3 Projects utilizing mxmlc, whether Flex Builder or Flash Develop, utilizing the Flash CS3 components because Keith Peter’s minimalist components aren’t thorough enough for your needs… it gets a little trickier. Basically, you do the same thing a Flash Developer would, re-export your SWC’s, and then re-link them into your libs folder of choice.
I haven’t figured out the best way to handle this in a multi-developer environment. You can’t check fl.core.UIComponent into Subversion because if Flex Builder sees a fl class, it’ll use that instead of the SWC’s, thus breaking the Flash components you’ve imported.
Anyway, here’s what you do:
1. Take the following code, and replace the “callLater” and “callLaterDispatcher” methods.
// HACK // Found here: // http://www.kirupa.com/forum/showthread.php?t=287632 // // HACK // Added Event.ENTER_FRAME listener because of // stage.invalidate bug with Event.RENDER // https://jessewarden.com/2008/03/how-to-fix-the-flash-cs3-components.html protected function callLater(fn:Function):void { callLaterMethods[fn] = true; if (stage != null) { stage.addEventListener(Event.ENTER_FRAME,callLaterDispatcher,false,0,true); stage.addEventListener(Event.RENDER,callLaterDispatcher,false,0,true); stage.invalidate(); } else { addEventListener(Event.ADDED_TO_STAGE,callLaterDispatcher,false,0,true); } } private function callLaterDispatcher(event:Event):void { if (event.type == Event.ADDED_TO_STAGE) { removeEventListener(Event.ADDED_TO_STAGE,callLaterDispatcher); stage.addEventListener(Event.ENTER_FRAME,callLaterDispatcher,false,0,true); // now we can listen for render event: stage.addEventListener(Event.RENDER,callLaterDispatcher,false,0,true); stage.invalidate(); return; } else { event.target.removeEventListener(Event.ENTER_FRAME,callLaterDispatcher); event.target.removeEventListener(Event.RENDER,callLaterDispatcher); if (stage == null) { // received render, but the stage is not available, so we will listen for addedToStage again: addEventListener(Event.ADDED_TO_STAGE,callLaterDispatcher,false,0,true); return; } } var methods:Dictionary = new Dictionary(); for (var copyMethod:Object in callLaterMethods) { methods[copyMethod] = callLaterMethods[copyMethod]; delete(callLaterMethods[copyMethod]); } for (var method:Object in methods) { method(); } }
2. Save your file.
3. Reboot Flash.
4. In your FLA, delete the “ComponentShim” out of your Library.
5. Stop frikin’ using drawNow.
Keep in mind, there still are other issues with the CS3 components this won’t fix. Just because you implement ICellRenderer doesn’t mean you’ll work in a TileList. UILoader didn’t get the memo. The rotated vertical Slider makes skinning a nightmare. Transitions… don’t get me started.
On a less sarcastic note, there ARE valid uses of drawNow. For example, you cannot do effective measurement of assets if they aren’t rendered. Therefore, drawNow/validateNow have to be utilized to clear all dirty flags so you can get accurate measurements of width and height.
Hey Jesse,
Actually, we were well aware of the nested invalidation issue, and drawNow was not an uninformed implementation. One of the issues that we tried to solve for the CS3 components was the redraw flicker / delayed redraw that both the AS2 and Flex components suffer from. The only way we could solve this was with drawNow, which is not ideal, but was the best possible solution.
Your modification will certainly address the complexity of using drawNow for component developers, but it does re-introduce the delayed redraw.
That definitely doesn’t make it wrong. It just demonstrates the tradeoffs we had to make. One of our other major goals was to make the architecture easy to “hack”, so it’s great to see you doing exactly that.
Much love.
Grant.
And here we are again having problems with Adobe’s components… I gave up on them back on Flash 6 and I’ve never ever used them again.
Every new version claims is *super* easy to skin. Every new version claims is *super* easy to extend. Every new version claims is bug free. Every new version claims last version was rubbish (heard from Adobe guys at FOTB) but not this one.
Please Adobe HOW COME?????? Why components are SUCH a big deal that not even after 3-4 versions you can get them right?
Flash developers are *still* wasting time dealing with such basic parts of applications instead of using their time to build cool features…
That’s just unacceptable, really.
Cheers
I think this design choices in the fl components are ok – the code is generally very straighforward and simple to hack, and they are not really expected to be used to build really complex components.
I however agree the ICellRenderer is weird – it doesn’t seem to be used as a way to create custom renderers, but only to manipulate instances.
Jesse,
Be careful with the code you have there. I’d never seen it on kirupa, but oddly it’s the exact same code I had written in my own component set. The problem I encountered was when I integrated my code into a much larger codebase (Disney). At that point, the Event.RENDER/stage.invalidation was causing issues with their stuff since they used stage invalidates randomly. I ended up just moving back towards Event.ENTER_FRAME because I knew I was safe
@Kenny: Thanks for the tip. If you comment out stage.invalidate, it should still work. I’ll do so next week on some projects at work and hopefully never look back.
Did you try creating your own class that extends fl.core.UIComponent and overrides the shown methods? Then you can change the bound class in the linkage dialog.
“Problem? You cannot call stage.invalidate() WHILE executing code that was triggered from an Event.RENDER…. It turned out to be a bug in the Flash Player”
They really need to fix this. I’ve been trying to build components that can live side by side with Adobe’s from Flash CS3 or anyone else’s. I can’t use Event.RENDER at all because it would mean I’d have to write my code to account for drawNow(), when I want it to be generic as possible. What happens when another component set comes along that uses Event.RENDER. That’s right, I need to learn what they do instead of drawNow() and throw that into my code too. It becomes an obvious mess:
if(FlashCS3)
{
component.drawNow();
}
else if(SuperCoolComponents)
{
component.superCoolDrawNow();
}
Ugh.
Jesse, great post. I’m working on my first foray into Flash CS3 and AS3, and you provided a lot of insight into some potential problems I might encounter moving forward.
For simplicity’s sake as I’m still getting used to AS3, I’m not extending UIComponent. So, to use what you’ve cooked up here I created an Invalidator class which extends shape (can’t extend DisplayObject).
The constructor looks like this: Invalidator(dispatcher:DisplayObject = null)
So, to use with Display Objects:
new Invalidator(this)
To use with DisplayObjectContiners:
var i:Invalidator = new Invalidator();
addChild(i);
Have any thoughts about the pros / cons with this approach?
Thanks again!
Jesse, thanks for the info on this. Knowing that there was an issue helped me to find a solution before it became a problem. I’m writing some components from scratch (well, porting some AS2 actually) so I wrote an “Invalidator” class that handles it nicely without ENTERFRAME at all. Basically my UIComponent class has an invalidate method which in turn calls Invalidator.invalidate( this ) which adds it to a queue of components to update. If it’s currently listening to Event.RENDER it adds the component and that’s it (so it will be updated with the rest in the current frame), if not it calls stage.invalidate and registers the listener. Seems to work like a charm so far, obviously if there was/is third party code that manually calls stage.invalidate that could be an issue, but it’ll just an easy find/replace away.