Search code examples
c#asp.netasynchronousupdatepanel

Multiple Update Panels Prevent Link Click Until They All Finish Loading


I'm working on a ASP.Net page and one of the requirements was to have several widgets on the main page display various metrics. The original problem I ran into was that the widgets were making the page take too long to load after the user logged in. A co-worker suggested I use a user control he created to update the widgets. The user control itself is an update panel with extra bells and whistles. The problem I'm running into now is that users can't click on any links while the widgets are loading. Is there something in the user control that could be blocking the requests? If this can't be done using update panels, please suggest a better approach. The user control code is below.

User Control

namespace test.usercontrols.GenericControls
{
/// <summary>
/// A control that will asyncronously refersh it's content after a delay.
/// </summary>
[ToolboxData("<{0}:DelayedLoadPanel runat=server></{0}:DelayedLoadPanel>")]
[PersistChildren(false)]
[ParseChildren(true)]
public class DelayedLoadPanel : PlaceHolder, INamingContainer
{
    private UpdatePanel contentPanel = new UpdatePanel();
    private PlaceHolder contentHolder = new PlaceHolder();
    private HtmlGenericControl loadScriptControl = new HtmlGenericControl("script");

    private PlaceHolder _unloadedContent = new PlaceHolder();
    private PlaceHolder _loadedContent = new PlaceHolder();

    /// <summary>
    /// The initial content to be shown.
    /// </summary>
    [PersistenceMode(PersistenceMode.InnerProperty)]
    public PlaceHolder UnloadedContent
    {
        get { return _unloadedContent; }
        set { _unloadedContent = value; }
    }

    /// <summary>
    /// The content to be shown after LoadDelay has expired and the panel refreshed.
    /// </summary>
    [PersistenceMode(PersistenceMode.InnerProperty)]
    public PlaceHolder LoadedContent
    {
        get { return _loadedContent; }
        set { _loadedContent = value; }
    }

    /// <summary>
    /// Javascript to be run after LoadDelay has expired and the panel refreshed.
    /// </summary>
    [PersistenceMode(PersistenceMode.InnerProperty)]
    public Literal LoadedScript { get; set; }

    /// <summary>
    /// The delay in milliseconds before the page will reload. Set to a negative value to prevent automatic reload.
    /// </summary>
    public int LoadDelay { get; set; }

    public int MaxAsyncCallCount
    {
        get { return _MaxAsyncCallCount; }
        set { _MaxAsyncCallCount = value; }
    }

    private int _MaxAsyncCallCount = 1;

    /// <summary>
    /// Sets or gets the value for if the LoadedContent is visibile. Setting to true will fire the DelayedLoad events.
    /// </summary>
    public bool IsLoaded
    {
        get
        {
            if (ViewState["IsLoaded"] == null)
                ViewState["IsLoaded"] = false;

            return (bool) ViewState["IsLoaded"];
        }
        set
        {
            ViewState["IsLoaded"] = value;

            if (value)
                OnDelayedLoad(new EventArgs());
        }
    }

    protected override void OnInit(EventArgs e)
    {
        // We want to make sure our child controls are created so they can properly load view state. We need them before OnLoad so we can compare ID's.
        EnsureChildControls();
        base.OnInit(e);
    }

    /// <summary>
    /// Check to see if this is the control's post back after LoadDelay has expired, if so set the value and fire the events.
    /// </summary>
    /// <param name="e"></param>
    protected override void OnLoad(EventArgs e)
    {            
        var scriptManager = ScriptManager.GetCurrent(Page);
        if (scriptManager.IsInAsyncPostBack && (scriptManager.AsyncPostBackSourceElementID == contentPanel.ClientID))
        {
            // This also fire off the DelayedLoad and OnDelayedLoad events.
            IsLoaded = true;
        }

        base.OnLoad(e);
    }

    /// <summary>
    /// Set the visibility of the controls so only the proper ones render.
    /// </summary>
    /// <param name="e"></param>
    protected override void OnPreRender(EventArgs e)
    {
        registerLoadScript();

        // We should really set this in the render function so it's as late in the page life cycle as possible.
        // However render does not seem to get called during partial post backs.
        loadScriptControl.Attributes["type"] = "text/javascript";            
        loadScriptControl.InnerHtml = "$(document).ready(setTimeout(function() { test.doPostBack('" + contentPanel.ClientID + "', null, " + MaxAsyncCallCount + ")}, " + LoadDelay + "));";

        if (IsLoaded)
        {
            if (LoadedScript != null)
            {
                var scriptTag = new HtmlGenericControl("script");
                scriptTag.Attributes["type"] = "text/javascript";
                scriptTag.Attributes["name"] = "test.DelayedLoadPanel.OnLoaded";
                scriptTag.InnerHtml = LoadedScript.Text;
                contentHolder.Controls.Add(scriptTag);
            }

            // We're loaded so we want to show the loaded content items but not the unloaded, and we really don't want to repost the automatic post back scripts.
            LoadedContent.Visible = true;
            loadScriptControl.Visible = false;
            UnloadedContent.Visible = false;
        }
        else
        {
            // Show the unloaded content and add some javascript that will cause the page to post back after the LoadDelay timeout 
            LoadedContent.Visible = false;
            loadScriptControl.Visible = (LoadDelay >= 0); // If LoadDelay is a negative number we're not going to automatically refresh.
            UnloadedContent.Visible = true;
        }

        base.OnPreRender(e);
    }

    /// <summary>
    /// Causes the panel to update it's content to the browser.
    /// </summary>
    public virtual void Update()
    {
        contentPanel.Update();
    }

    public delegate void DelayedLoadHandler(object sender, EventArgs e);

    /// <summary>
    /// Occurs when the control is reloaded after DelayedLoad has expired.
    /// </summary>
    public event DelayedLoadHandler DelayedLoad;

    /// <summary>
    /// Raises the DelayedLoad event.
    /// </summary>
    /// <param name="e"></param>
    protected virtual void OnDelayedLoad(EventArgs e)
    {
        if ((IsLoaded) && (DelayedLoad != null))
            DelayedLoad(this, null);

        Update();
    }

    /// <summary>
    /// Create all the sub controls for this control.
    /// </summary>
    protected override void CreateChildControls()
    {
        Controls.Clear();

        contentPanel.ID = "contentPanel";
        contentPanel.ChildrenAsTriggers = false;
        contentPanel.UpdateMode = UpdatePanelUpdateMode.Conditional;

        contentPanel.ContentTemplateContainer.Controls.Add(contentHolder);
        Controls.Add(contentPanel);

        contentHolder.Controls.Add(LoadedContent);
        contentHolder.Controls.Add(loadScriptControl);
        contentHolder.Controls.Add(UnloadedContent);

        base.CreateChildControls();
    }

    private void registerLoadScript()
    {
        ScriptManager.RegisterClientScriptInclude(this, typeof(DelayedLoadPanel), "DelayedLoadPanelScript", "/javascript/DelayedLoadPanel.js");
    }
}
}

DelayedLoadPanel.js

Test.DelayedLoadPanel = new Object();

Test.DelayedLoadPanel.AddNamespace = function (obj) {
if (obj.Test == undefined)
    obj.Test = new Object();

if (obj.Test.DelayedLoadPanel == undefined)
    obj.Test.DelayedLoadPanel = new Object();
}

Test.DelayedLoadPanel.parseOnLoadScripts = function () {
var scripts = $("script[name='Test.DelayedLoadPanel.OnLoaded']");

for (var i = 0; i < scripts.length; i++) {
    eval(scripts[i].text);
}

scripts.remove();
}

Test.DelayedLoadPanel.saveScrollPosition = function() {
Test.DelayedLoadPanel.currentScrollX = $(window).scrollLeft();
Test.DelayedLoadPanel.currentScrollY = $(window).scrollTop();
}

Test.DelayedLoadPanel.restoreScrollPosition = function() {
$(window).scrollLeft(Test.DelayedLoadPanel.currentScrollX);
$(window).scrollTop(Test.DelayedLoadPanel.currentScrollY);
}

$(document).ready(function() {
var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_pageLoading(Test.DelayedLoadPanel.saveScrollPosition);
prm.add_endRequest(Test.DelayedLoadPanel.restoreScrollPosition);
prm.add_endRequest(Test.DelayedLoadPanel.parseOnLoadScripts);
});

Base.js

var Test = new Object();

Test.postBackQueue = new Array();

Test.doPostBack = function (target, parameter) {
var prm = Sys.WebForms.PageRequestManager.getInstance();

if ((typeof target != "undefined") && (typeof parameter != "undefined"))
    Test.postBackQueue.push({ target: target, parameter: parameter });

// If we're not in the middle of doing a post back then go ahead and fire  the first item in the queue.
if ((!prm.get_isInAsyncPostBack()) && (Test.postBackQueue.length > 0)) {
    var postbackInfo = Test.postBackQueue.shift();

    __doPostBack(postbackInfo.target, postbackInfo.parameter);
}
}

Test.postBackEndRequestHandler = function () {
Test.doPostBack();
};

Sample Usage

    <a href="cat-management.aspx?view=cats&TimeFrame=3" class="metricWidget">
    <div class="widgetHeader">
        <img src="/images/cat.png" class="widgetIcon" />
        <div class="widgetHeaderText">proactive tickets</div>
    </div>
    <Test:DelayedLoadPanel runat="server" ID="catPanel" OnDelayedLoad="catDelayedLoad" MaxAsyncCallCount="2">
        <LoadedContent>
            <div class="widgetSubHeader">Type</div>
            <div class="widgetSubPanelLeft">Fluffy <span class="widgetMetric widgetMetricPadding" runat="server" id="fluffyCats"></span></div>
            <div class="widgetSubPanelRight">Orange <span class="widgetMetric widgetMetricPadding" runat="server" id="orangeCats"></span></div>
        </LoadedContent>
        <UnloadedContent>
            <img class="widgetLoader" />
        </UnloadedContent>
    </Test:DelayedLoadPanel>
</a>

Code Behind

        protected void catDelayedLoad(object sender, EventArgs eventArgs)
    {
        var catCount = Utility.catService.GetCatCount(currentOrg, from,
            to);

        fluffyCats.InnerText = catCount.FluffyCats.ToString();
        orangeCats.InnerText = catCount.OrangeCats.ToString();
    }

Solution

  • The issue was being caused by session locking which prevents two requests from accessing the session state concurrently. I was able to get around this by setting the EnableSessionState attribute of the Page directive to ReadOnly. Hooray!