讓UpdatePanel支援檔案上傳(2):伺服器端元件

weixin_34262482發表於2007-04-05

我們現在來關注伺服器端的元件。目前的主要問題是,我們如何讓頁面(事實上是ScriptManager控制元件)認為它接收到的是一個非同步的回送?ScriptManager控制元件會在HTTP請求的Header中查詢特定的項,但是我們在向IFrame中POST資料時無法修改Header。所以我們必須使用一個方法來“欺騙”ScriptManager。

目前使用的解決方案是,我們在POST資料之前在頁面中隱藏的輸入元素(<input type="hidden" />)中放入一個特定的標記,然後我們開發的伺服器端元件(我把它叫做AjaxFileUplaodHelper)會在它的Init階段(OnInit方法)中在Request Body中檢查這個標記,然後使用反射來告訴ScriptManager目前的請求為一個非同步請求。

但是事情並不像我們想象的那麼簡單,讓我們在寫程式碼之前來看一個方法:

internal sealed class PageRequestManager
{
    // ...

    internal void OnInit()
    {
        if (_owner.EnablePartialRendering && !_owner._supportsPartialRenderingSetByUser)
        {
            IHttpBrowserCapabilities browser = _owner.IPage.Request.Browser;
            bool supportsPartialRendering =
                (browser.W3CDomVersion >= MinimumW3CDomVersion) &&
                (browser.EcmaScriptVersion >= MinimumEcmaScriptVersion) &&
                browser.SupportsCallback;

            if (supportsPartialRendering)
            {
                supportsPartialRendering = !EnableLegacyRendering;
            }
            _owner.SupportsPartialRendering = supportsPartialRendering;
        }

        
    }

    // ...
}

上面這段程式碼會在ScriptManager的OnInit方法中被呼叫。請注意紅色部分的程式碼,“_owner”變數是當前頁面上的ScriptManager。在頁面受到一個真正的非同步會送之後,PageRequestManager會響應頁面的Error事件,並且將錯誤資訊用它定義的格式輸出。如果我們只是修改了ScriptManager的私有field,那麼如果在非同步回送時出現了一個未捕獲的異常,那麼頁面就會輸出客戶端未知的內容,導致在客戶端解析失敗。所以我們必須保證這種情況下的輸出和真正的非同步回送是相同的,所以我們就可以使用以下的做法來解決錯誤處理的問題。

internal static class AjaxFileUploadUtility
{
    internal static bool IsInIFrameAsyncPostBack(NameValueCollection requestBody)
    { 
        string[] values = requestBody.GetValues("__AjaxFileUploading__");

        if (values == null) return false;

        foreach (string value in values)
        {
            if (value == "__IsInAjaxFileUploading__")
            {
                return true;
            }
        }

        return false;
    }

    // ...
}

[PersistChildren(false)]
[ParseChildren(true)]
[NonVisualControl]
public class AjaxFileUploadHelper : Control
{
    // ScriptManager members;
    private static FieldInfo isInAsyncPostBackFieldInfo;
    private static PropertyInfo pageRequestManagerPropertyInfo;

    // PageRequestManager members;
    private static MethodInfo onPageErrorMethodInfo;
    private static MethodInfo renderPageCallbackMethodInfo;


    static AjaxFileUploadHelper()
    {
        Type scriptManagerType = typeof(ScriptManager);
        isInAsyncPostBackFieldInfo = scriptManagerType.GetField(
            "_isInAsyncPostBack",
            BindingFlags.Instance | BindingFlags.NonPublic);
        pageRequestManagerPropertyInfo = scriptManagerType.GetProperty(
            "PageRequestManager",
            BindingFlags.Instance | BindingFlags.NonPublic);

        Assembly assembly = scriptManagerType.Assembly;
        Type pageRequestManagerType = assembly.GetType("System.Web.UI.PageRequestManager");
        onPageErrorMethodInfo = pageRequestManagerType.GetMethod(
            "OnPageError", BindingFlags.Instance | BindingFlags.NonPublic);
        renderPageCallbackMethodInfo = pageRequestManagerType.GetMethod(
            "RenderPageCallback", BindingFlags.Instance | BindingFlags.NonPublic);
    }

    public static AjaxFileUploadHelper GetCurrent(Page page)
    {
        return page.Items[typeof(AjaxFileUploadHelper)] as AjaxFileUploadHelper;
    }

    private bool isInAjaxUploading = false;

    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);

        if (this.Page.Items.Contains(typeof(AjaxFileUploadHelper)))
        {
            throw new InvalidOperationException("One AjaxFileUploadHelper per page.");
        }

        this.Page.Items[typeof(AjaxFileUploadHelper)] = this;

        this.EnsureIsInAjaxFileUploading();
    }

    private void EnsureIsInAjaxFileUploading()
    {
        this.isInAjaxUploading = 
AjaxFileUploadUtility.IsInIFrameAsyncPostBack(this.Page.Request.Params); if (this.isInAjaxUploading) { isInAsyncPostBackFieldInfo.SetValue( ScriptManager.GetCurrent(this.Page), true); this.Page.Error += new EventHandler(Page_Error); } } private void Page_Error(object sender, EventArgs e) { // ... } private object _PageRequestManager; private object PageRequestManager { get { if (this._PageRequestManager == null) { this._PageRequestManager = pageRequestManagerPropertyInfo.GetValue( ScriptManager.GetCurrent(this.Page), null); } return this._PageRequestManager; } } // ... }

這段實現並不複雜。如果Request Body中的“__AjaxFileUploading__”的值為“__IsInAjaxFileUploading__”,我們就會使用反射修改ScirptManager控制元件中的私有變數“_isInAsyncPostBack”。此後,我們使用了自己定義的Page_Error方法來監聽頁面的Error事件,當頁面的Error事件被觸發時,我們定義的新方法就會將能夠正確解析的內容傳送給客戶端端。

自然,AjaxFileUploadHelper也需要將程式集中內嵌的指令碼檔案註冊到頁面中。我為元件新增了一個開關,可以讓使用者開發人員使用程式設計的方式來開啟/關閉對於AJAX檔案上傳的支援。這部分實現更為簡單:

public bool SupportAjaxUpload
{
    get { return _SupportAjaxUpload; }
    set { _SupportAjaxUpload = value; }
}

protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);

    if (this.isInAjaxUploading)
    {
        this.Page.SetRenderMethodDelegate(new RenderMethod(this.RenderPageCallback));
    }

    if (this.Page.IsPostBack || !this.SupportAjaxUpload) return;

    if (!ScriptManager.GetCurrent(this.Page).IsInAsyncPostBack)
    {
        ScriptReference script = new ScriptReference(
            "Jeffz.Web.AjaxFileUploadHelper.js", this.GetType().Assembly.FullName);
        ScriptManager.GetCurrent(this.Page).Scripts.Add(script);
    }
}

如果使用者希望關閉對於AJAX檔案上傳的支援,他可以使用下面的程式碼將頁面上AjaxFileUploadHelper控制元件的SupportAjaxUpload屬性關閉:

AjaxFileUploadHelper.GetCurrent(this.Page).SupportAjaxUpload = false;

等一下,這是什麼?我是指在“OnPreRender”方法中的程式碼:

if (this.isInAjaxUploading)
{
    this.Page.SetRenderMethodDelegate(new RenderMethod(this.RenderPageCallback));
}

解釋如下:在ScirptManager的“OnPreRender”方法執行時,頁面的Render方法會被伺服器端PageRequestManager類的RenderPageCallback方法替代。上面程式碼的作用是在“我們的”非同步回送時,再次使用我們定義的方法來替換頁面的Render方法。請注意之前的Page_Error方法也是我們重新定義的方法,當非同步回送時遇到了未捕獲的異常時會使用它來輸出,請注意下面的程式碼:

private void RenderPageCallback(HtmlTextWriter writer, Control pageControl)
{
    AjaxFileUploadUtility.WriteScriptBlock(this.Page.Response, true);

    StringBuilder sb = new StringBuilder();
    HtmlTextWriter innerWriter = new HtmlTextWriter(new StringWriter(sb));
    renderPageCallbackMethodInfo.Invoke(
this.PageRequestManager, new object[] { innerWriter, pageControl }); writer.Write(sb.Replace("*/", "*//*").ToString()); AjaxFileUploadUtility.WriteScriptBlock(this.Page.Response, false); } private void Page_Error(object sender, EventArgs e) { AjaxFileUploadUtility.WriteScriptBlock(this.Page.Response, true); onPageErrorMethodInfo.Invoke(this.PageRequestManager, new object[] { sender, e }); AjaxFileUploadUtility.WriteScriptBlock(this.Page.Response, false); }

究竟什麼是“AjaxFileUploadUtility.WriteScriptBlock”方法呢?我們為什麼要這樣寫?其實這麼做的目的是為了相容各種瀏覽器,使它們都能夠正確通過iframe正確收到伺服器端獲得的資訊。這可以說是整個專案中最有技巧的部分了,我將會使用一個部分來單獨講一下這部分的機制。

 

點選這裡下載整個專案

English Version

相關文章