Flickering UI From the ASP.NET AJAX Toolkit TabContainer while in an UpdatePanel?
UPDATE
This issue is officially fixed in .NET 3.5 SP1. If you have applied the workaround in this blog post, you no longer need it. But the story of this bug is still rather interesting.
UPDATE
If you upgrade from Microsoft ASP.NET AJAX Extensions 1.0 to 3.5, and you have a TabContainer inside an update panel, like this:
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<act:TabContainer runat="server">
<act:TabPanel runat="server">
<HeaderTemplate>
Tab
</HeaderTemplate>
<ContentTemplate>
Tab Content
</ContentTemplate>
</act:TabPanel>
</act:TabContainer>
</ContentTemplate>
</asp:UpdatePanel>
...you may notice a change in behavior. When the update panel updates, the UI seems to flicker off and on again, whereas with AJAX 1.0 it seemed to update instantly without any flicker. You may have noticed a similar problem with other components as well, depending on what they are doing.
This is one of those interesting bugs with a convoluted history, driven by browser quirky behavior.
The Tale -- First, some background
Update panels basically work by replacing their content with the new content from the server by setting its innerHTML. However, there may also be components defined within that HTML that register script. Those components need to be recreated now that there is a new set of HTML. UpdatePanel runs those scripts by dynamically creating a new <script> element, setting its content, and then appending the script element to the DOM. It goes something like this:
var script = document.createElement("script");
script.type = "text/javascript";
script.text = "[script that creates a new component]"
document.getElementsByName("head")[0].appendChild(script);
After it has done this for all of the components in the new content, it raises the load event on Sys.Application, among some other page request manager events.
The Browser Quirk
When you append a script element dynamically like this, would you expect the script it contains to execute immediately, or later? Keep in mind we're talking about literal script here, not a reference to another script by url. Remember once all the scripts are appended, page request manager proceeds with raising events. So whether the script executes immediately or later affects whether that component will be created and initialized before, or after those events. That's an important difference.
The intention is for the script to execute immediately. And that was a valid assumption that IE, FireFox, Opera, and Safari all agreed with at the time. That is, until FireFox 2.0.0.2 was released. With that release, FireFox started delaying the execution of the script until later. This caused components to be created after the load events, which broke some AJAX controls that depended on the ordering.
What we did about it
At the time we were in development of Microsoft AJAX Extensions 3.5. We certainly felt that this was a breaking change in FireFox that should be fixed. But we couldn't count on that. We had to fix this problem in AJAX so it wouldn't be a problem whether or not FireFox, or any browser, exhibited this quirk. The fix was to ensure the script executed first, by deferring future operations with a window.setTimeout. It goes something like this:
var script = document.createElement("script");
script.type = "text/javascript";
script.text = "..."
document.getElementsByName("head")[0].appendChild(script);
window.setTimeout(doContinue, 0);
function doContinue() {
alert('definitely after script executes');
}
window.setTimeout is one of those poorly understood JavaScript features. JavaScript is single threaded. All setTimeout does is queue the referenced function for execution at a later time, when the timeout period has expired and there is no other javascript operation occurring (since only one train can be active at a time). Queuing the script element either executed the script, or it queued it for executing. The window.setTimeout just ensures that 'doContinue' is queued up afterwards. I often see people using this trick, but using a timeout value of "1" or something else very small. There's no reason for that, 0 does the trick, which is easily understood if you understand what it's really doing.
Problem Solved
This indeed solved the problem -- scripts would now execute in the order we intended, regardless of whether the browser executes dynamic inline script elements immediately. And so this fix was released with AJAX Extensions 3.5.
Thankfully, FireFox was also quick to respond. Likely this behavioral change broke other frameworks as well (I can only guess), so there was probably enough of a splash with it to get them to fix it in quick order -- because soon after 2.0.0.2 was released, 2.0.0.3 came out and this behavior reverted back to it's pre-2.0.0.2 behavior. But still, it was good that we had the workaround in place in case this issue ever came up again.
Problem Caused
We didn't see any adverse side effects with the setTimeout work-around, other than a small performance hit on initializing components after an asynchronous update from an update panel. Unfortunately, this turned out to affect some components much more than others. The point at which the setTimeout occurs is after the innerHTML has been replaced, but before component initialization occurs. Any component that performs any serious DOM manipulation from its initialize() method would now be doing so later than it used to. The setTimeout not only delays execution, it also gives the browser more opportunity to draw stuff on the screen. So a component that say, creates a bunch of new elements from initialize, would be causing a FOUC ("flash of uninitialized content"), since the user would first see the natural HTML of the component and then a split second later, the manipulated DOM. This is the cause of the 'flicker' you see with the TabContainer!
The Real Solution
First of all, if at all possible, AJAX components should rely as little as possible on DOM manipulation to create their initial UI. Especially if they are server components as well -- they should render out the initial UI and only manipulate it as needed. This is better practice for FOUC purposes, performance, and probably SEO or script disabled browsers too. I don't know for a fact that the TabContainer is doing this, or if it is really necessary for it to do it, I can only infer that it is based on this behavior.
The Actual Solution
Barring that, the framework should do whatever it can to improve the experience of any controls that need to rely on DOM manipulation from initialize.
Since FireFox fixed the quirk, and no other browsers have experienced it, we have decided the workaround is no longer necessary. All it is doing is hurting performance. If any browser makes this kind of change again, or if a new browser is released that exhibits this quirk, it may not be compatible with some components or applications that rely on the ordering of events (but at least it won't flicker). But the number of components and applications that would have a problem should be very small, and such a quirk we hope would be deemed incorrect behavior and fixed in that browser. It may not be specifically called out in any W3C recommendations (provide me a link if you find one), but it should be, and the fact that the 4 major browsers of today are consistent with this really says something about what the correct behavior is. So all in all, the workaround just it isn't worth the cost anymore.
So the workaround is removed in the upcoming 3.5 Extensions release. But if you want to apply the fix to your 3.5 scripts today without waiting, here is how to do it. The goal is to replace the internal method used by PageRequestManager to load scripts, in the ScriptLoader component. The easiest/simplest way to do it would be to put this script in your own JS file, and then reference it on every AJAX page with the ScriptManager. There other ways of patching it, such as by extracting the AJAX scripts and using the ScriptManager.ScriptPath or ScriptReference.Path feature to point to your modified copy of MicrosoftAjax.js and MicrosoftAjax.debug.js (copies of which are in your program files directory).
Sys._ScriptLoader.getInstance()._loadScriptsInternal = function() {
if (this._scriptsToLoad && this._scriptsToLoad.length > 0) {
var nextScript = Array.dequeue(this._scriptsToLoad);
var scriptElement = this._createScriptElement(nextScript);
if (scriptElement.text && Sys.Browser.agent === Sys.Browser.Safari) {
scriptElement.innerHTML = scriptElement.text;
delete scriptElement.text;
}
if (typeof(nextScript.src) === "string") {
this._currentTask = new Sys._ScriptLoaderTask(scriptElement, this._scriptLoadedDelegate);
this._currentTask.execute();
}
else {
document.getElementsByTagName('head')[0].appendChild(scriptElement);
Sys._ScriptLoader._clearScript(scriptElement);
this._loadScriptsInternal();
}
}
else {
var callback = this._allScriptsLoadedCallback;
this._stopLoading();
if(callback) {
callback(this);
}
}
}
Include this script on your page with a TabContainer, and volia, the flickering will be gone.
This has been a bug tale! Happy coding, I wish you all a non-flickering UI.
UPDATE 05/02/08: Removed the bit about 3.5 extensions preview. The fix isn't in the extensions preview as I originally thought.