HttpContext.Items and Server.Transfer/Execute
- by Rick Strahl
A few days ago my buddy Ben Jones pointed out that he ran into a bug in the ScriptContainer control in the West Wind Web and Ajax Toolkit. The problem was basically that when a Server.Transfer call was applied the script container (and also various ClientScriptProxy script embedding routines) would potentially fail to load up the specified scripts. It turns out the problem is due to the fact that the various components in the toolkit use request specific singletons via a Current property. I use a static Current property tied to a Context.Items[] entry to handle this type of operation which looks something like this: /// <summary>
/// Current instance of this class which should always be used to
/// access this object. There are no public constructors to
/// ensure the reference is used as a Singleton to further
/// ensure that all scripts are written to the same clientscript
/// manager.
/// </summary>
public static ClientScriptProxy Current
{
get
{
if (HttpContext.Current == null)
return new ClientScriptProxy();
ClientScriptProxy proxy = null;
if (HttpContext.Current.Items.Contains(STR_CONTEXTID))
proxy = HttpContext.Current.Items[STR_CONTEXTID] as ClientScriptProxy;
else
{
proxy = new ClientScriptProxy();
HttpContext.Current.Items[STR_CONTEXTID] = proxy;
}
return proxy;
}
}
The proxy is attached to a Context.Items[] item which makes the instance Request specific. This works perfectly fine in most situations EXCEPT when you’re dealing with Server.Transfer/Execute requests. Server.Transfer doesn’t cause Context.Items to be cleared so both the current transferred request and the original request’s Context.Items collection apply.
For the ClientScriptProxy this causes a problem because script references are tracked on a per request basis in Context.Items to check for script duplication. Once a script is rendered an ID is written into the Context collection and so considered ‘rendered’:
// No dupes - ref script include only once
if (HttpContext.Current.Items.Contains( STR_SCRIPTITEM_IDENTITIFIER + fileId ) )
return;
HttpContext.Current.Items.Add(STR_SCRIPTITEM_IDENTITIFIER + fileId, string.Empty);
where the fileId is the script name or unique identifier. The problem is on the Transferred page the item will already exist in Context and so fail to render because it thinks the script has already rendered based on the Context item. Bummer.
The workaround for this is simple once you know what’s going on, but in this case it was a bitch to track down because the context items are used in many places throughout this class. The trick is to determine when a request is transferred and then removing the specific keys.
The first issue is to determine if a script is in a Trransfer or Execute call:
if (HttpContext.Current.CurrentHandler != HttpContext.Current.Handler)
Context.Handler is the original handler and CurrentHandler is the actual currently executing handler that is running when a Transfer/Execute is active. You can also use Context.PreviousHandler to get the last handler and chain through the whole list of handlers applied if Transfer calls are nested (dog help us all for the person debugging that).
For the ClientScriptProxy the full logic to check for a transfer and remove the code looks like this:
/// <summary>
/// Clears all the request specific context items which are script references
/// and the script placement index.
/// </summary>
public void ClearContextItemsOnTransfer()
{
if (HttpContext.Current != null)
{
// Check for Server.Transfer/Execute calls - we need to clear out Context.Items
if (HttpContext.Current.CurrentHandler != HttpContext.Current.Handler)
{
List<string> Keys = HttpContext.Current.Items.Keys.Cast<string>().Where(s => s.StartsWith(STR_SCRIPTITEM_IDENTITIFIER) || s == STR_ScriptResourceIndex).ToList();
foreach (string key in Keys)
{
HttpContext.Current.Items.Remove(key);
}
}
}
}
along with a small update to the Current property getter that sets a global flag to indicate whether the request was transferred:
if (!proxy.IsTransferred && HttpContext.Current.Handler != HttpContext.Current.CurrentHandler)
{
proxy.ClearContextItemsOnTransfer();
proxy.IsTransferred = true;
}
return proxy;
I know this is pretty ugly, but it works and it’s actually minimal fuss without affecting the behavior of the rest of the class. Ben had a different solution that involved explicitly clearing out the Context items and replacing the collection with a manually maintained list of items which also works, but required changes through the code to make this work.
In hindsight, it would have been better to use a single object that encapsulates all the ‘persisted’ values and store that object in Context instead of all these individual small morsels. Hindsight is always 20/20 though :-}.
If possible use Page.Items
ClientScriptProxy is a generic component that can be used from anywhere in ASP.NET, so there are various methods that are not Page specific on this component which is why I used Context.Items, rather than the Page.Items collection.Page.Items would be a better choice since it will sidestep the above Server.Transfer nightmares as the Page is reloaded completely and so any new Page gets a new Items collection. No fuss there.
So for the ScriptContainer control, which has to live on the page the behavior is a little different. It is attached to Page.Items (since it’s a control):
/// <summary>
/// Returns a current instance of this control if an instance
/// is already loaded on the page. Otherwise a new instance is
/// created, added to the Form and returned.
///
/// It's important this function is not called too early in the
/// page cycle - it should not be called before Page.OnInit().
///
/// This property is the preferred way to get a reference to a
/// ScriptContainer control that is either already on a page
/// or needs to be created. Controls in particular should always
/// use this property.
/// </summary>
public static ScriptContainer Current
{
get
{
// We need a context for this to work!
if (HttpContext.Current == null)
return null;
Page page = HttpContext.Current.CurrentHandler as Page;
if (page == null)
throw new InvalidOperationException(Resources.ERROR_ScriptContainer_OnlyWorks_With_PageBasedHandlers);
ScriptContainer ctl = null;
// Retrieve the current instance
ctl = page.Items[STR_CONTEXTID] as ScriptContainer;
if (ctl != null)
return ctl;
ctl = new ScriptContainer();
page.Form.Controls.Add(ctl);
return ctl;
}
}
The biggest issue with this approach is that you have to explicitly retrieve the page in the static Current property. Notice again the use of CurrentHandler (rather than Handler which was my original implementation) to ensure you get the latest page including the one that Server.Transfer fired.
Server.Transfer and Server.Execute are Evil
All that said – this fix is probably for the 2 people who are crazy enough to rely on Server.Transfer/Execute. :-} There are so many weird behavior problems with these commands that I avoid them at all costs. I don’t think I have a single application that uses either of these commands…
Related Resources
Full source of ClientScriptProxy.cs (repository)
Part of the West Wind Web Toolkit
Static Singletons for ASP.NET Controls Post
© Rick Strahl, West Wind Technologies, 2005-2010Posted in ASP.NET