Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

HttpContext.Items and Server.Transfer/Execute


:P
On this page:

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

Posted in ASP.NET  

The Voices of Reason


 

Ben Jones
January 20, 2010

# re: HttpContext.Items and Server.Transfer/Execute

Rick - Thanks for looking into this. I definately agree about avoiding server transfers, but as I mentioned earlier, we're incorporating your toolkit into a complex app that actually started life as an asp application in 2001 and has evolved into asp.net and changed with new technologies. I was once using "remote server" (aka remote execution) techniques, way before AJAX (which is really about the same thing with a fancier name). Anyway I recall when server transfers were considered a good thing. And this old code is literally riddled with them, in many cases using the Context.Items to pass state from page to page. Some of this is in controls that frankly I'd rather not touch right now. So it's very helpful that the ScriptContainer control can handle server transfer because, well you know ScriptContainer is a good thing. I had also made a change to allow multiple ScriptContainers. Anyway I'll take at look at your new bits and shake then down when I get a chance. Aloha - Ben

photo jewelry
October 06, 2010

# re: HttpContext.Items and Server.Transfer/Execute

"I was once using "remote server" (aka remote execution) techniques, way before AJAX (which is really about the same thing with a fancier name)."

Ive encountered some AJAX Feed API that are still a JavaScript tool that you can use to handle your AJAX requests. But they are particularly useful for reading in feed data from external websites.

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024