多个更新面板阻止 Link 单击直到它们全部完成加载

Multiple Update Panels Prevent Link Click Until They All Finish Loading

我正在开发 ASP.Net 页面,其中一项要求是让主页上的多个小部件显示各种指标。我 运行 遇到的最初问题是小部件使页面在用户登录后加载时间太长。一位同事建议我使用他创建的用户控件来更新小部件。用户控件本身是一个带有额外功能的更新面板。我 运行 现在遇到的问题是用户在加载小部件时无法单击任何链接。用户控件中是否有可能阻止请求的内容?如果使用更新面板无法做到这一点,请提出更好的方法。用户控制代码如下。

用户控制

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();
};

示例用法

    <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>

代码隐藏

        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();
    }

问题是由 session locking 引起的,它阻止了两个请求同时访问会话状态。我能够通过将 Page 指令的 EnableSessionState 属性设置为 ReadOnly 来解决这个问题。万岁!