微軟的NET Framework 自帶了Ajax框架將以往傳統的ASPNET開發帶入了一個全新的AjaxNET開發時代我們除了在頁面上引入ScriptManager控件用以在客戶端注冊功能豐富的Ajax框架腳本外這個龐大的框架還提供了諸多功能強大的Ajax控件例如著名的UpdatePanelModalPopupExtenderRating等控件Ajax框架和控件的引入大大簡化了開發人員的開發任務同時也給用戶帶來了全新的Web體驗但是我們在使用復雜的框架提供的腳本時也常常會遇到這樣或那樣的問題有很多問題相信不少開發人員都能獨立解決不過有些復雜的問題還真是很傷腦筋
本來在MOSS中使用Ajax開發就已經不是一件輕易的事情或許高手們覺得這沒有什麼是的!我們在Google上會搜到很多介紹這方面的文章而且配置步驟都寫得非常詳細按照前輩們的經驗只要認真按照步驟將環境配置好一般都是沒有什麼問題的在MOSS中開發Ajax應用程序就如同簡單的Ajax網頁一樣只是部署的時候稍微要麻煩一些這裡我不想詳細講解在MOSS中如何進行Ajax開發只是想說一說前段時間在MOSS開發中因為Ajax框架所引起的一個非常怪異的問題一直困擾了我好幾天不過最終算是委曲求全得找到了一個替代的解決辦法至於會不會引起其它的什麼問題讀者也可以幫我分析一下
前不久我寫了一篇有關在FireFox中通過腳本獲取客戶端本地所選文件路徑的文章裡面介紹了通過客戶端上傳文件時如果通過javascript得到文件的本地路徑事實上在真正的文件上傳過程中得到文件的客戶端路徑意義是不大的除非我們需要實現如圖片本地預覽的功能否則我們一般都可以通過Form的Post方法得到要上傳的文件在C#一般都是這樣的
<body>
<form id=form runat=server method=post enctype=multipart/formdata>
<input id=File name=mtfile type=file />
<asp:Button ID=Button runat=server Text=Button OnClick=Button_Click />
</form>
</body>
protected void Button_Click(object sender EventArgs e)
{
HttpFileCollection files = RequestFiles;
if (files != null && filesCount > )
{
for (int i = ; i < filesCount; i++)
{
// TODO something
}
}
}
設置Form的method屬性為post並設置enctype為mulipart/formdata當頁面提交時在服務端通過RequestFiles方法即可得到上傳文件的對象集合非常簡單我們根本不需要在客戶端通過javascript得到文件的路徑不過這裡有一個限制那就是頁面必須post到服務端才能得到要上傳的文件也就是說我們不能通過javascript方式在頁面無刷新的情況下將文件上傳到服務器這也是Ajax唯一不能做到的一件事情不過我們通過一個比較老舊的技術可以避開這個問題那就是在頁面上使用隱藏的iFrame在頁面提交前將Form的target指向這個隱藏的iFrame頁面提交時iFrame會被刷新提交從而避免了整個頁面被刷新
事實上在Ajax興起前很多無刷新的頁面幾乎都是通過這種方式來實現的iFrame可以提交數據而且還避免了網頁的整體刷新在Ajax興起後iFrame似乎很少再被人們提起但是有一個例外
那就是文件上傳!我們可以去當今比較流行的網站考察一下像郵箱Gmail等都無一例外地使用了iFrame上傳文件我們可以將上面代碼中的HTML部分稍作修改就可以實現使用iFrame上傳文件的功能
<body>
<form id=form runat=server target=ifu method=post enctype=multipart/formdata>
<iframe frameborder= id=ifu name=ifu ></iframe>
<input id=File name=mtfile type=file />
<asp:Button ID=Button runat=server Text=Button OnClick=Button_Click />
</form>
</body>
後台代碼不變只是在Form上加了一個target屬性用來指向iFrame當頁面提交時會自動提交iFrame對象而不會將Form本身提交當遇到頁面上還有其它表單需要提交時我們可以這樣做先在提交按鈕的客戶端事件上將Form的target指向隱藏的iFrame然後返回True提交表單這時iFrame會被提交在服務端處理完數據保存後注冊一段腳本用來將iFrame的父頁面中Form的target改回自身這樣就可以模擬一次iFrame提交而不會影響到頁面上其它的功能我們只是在頁面需要被提交時才去修改Form的target屬性提交完後再改回來
這看起來似乎是一個很不錯的主意看看代碼吧!
<body>
<form id=form runat=server method=post enctype=multipart/formdata>
<iframe frameborder= id=ifu name=ifu ></iframe>
<input id=File name=mtfile type=file />
<asp:Button ID=Button onclientclick=documentforms[form]target = ifu;return true; runat=server Text=Button OnClick=Button_Click />
</form>
</body>
protected void Button_Click(object sender EventArgs e)
{
HttpFileCollection files = RequestFiles;
if (files != null && filesCount > )
{
for (int i = ; i < filesCount; i++)
{
//TODO something
}
}
string script = alert({});windowparentdocumentforms[form]target = _self;;
ClientScriptRegisterClientScriptBlock(thisPage thisGetType() stringEmpty stringFormat(script Save Successfully!) true);
}
盡管我們在頁面上使用Ajax控件該方法仍然會奏效需要說明一點就是上傳文件的功能是不能在UploadPanel控件中使用的否則功能會失效因為文件上傳必須刷新頁面除非我們使用iFrame提交表單如果非要在UpdatePanel控件中完成文件上傳功能那必須設置UpdatePanel控件的PostBackTrigger屬性將觸發事件的控件添加到PostBackTrigger中如
<asp:UpdatePanel ID=update runat=server UpdateMode=Conditional>
<ContentTemplate>
<input id=File name=mtfile type=file />
<asp:Button ID=btSave runat=server Text=Save onclick=btSave_Click />
</ContentTemplate>
<Triggers>
<asp:PostBackTrigger ControlID=btSave />
</Triggers>
</asp:UpdatePanel>
這將導致頁面回傳UpdatePanel控件的意義也就失去了在頁面上放置隱藏的iFrame按照前面介紹的方法通過javascript動態去修改Form的target屬性提交iFrame可以實現類似於Ajax方式的文件上傳功能其實頁面同樣被刷新了只是刷新的是隱藏的iFrame用戶不會有什麼感覺
前面說了這麼多只是想說說我所遇到的問題的背景現在步入正題!
在MOSS中開發頁面和普通的ASPNET頁面基本沒有什麼不同主要就是部署的時候會有一些麻煩那麼按照前面介紹的方法將編寫好的頁面部署到站點上運行時我發現了一個奇怪的問題那就是第一次按鈕觸發事件的時候服務端可以正確響應並且是通過iFrame提交過來的但是從第二次開始就需要等待十幾秒的時間按鈕才能再次被觸發一開始我以為是iFrame在被提交後沒有響應完畢來不及處理第二次請求後來通過設置斷點和插入調試腳本進行測試發現iFrame已經完全響應完畢按鈕還是不能被點擊(這裡說的按鈕不能被點擊是指Button不能響應服務端事件)
究竟發生了什麼問題?
在NET 時代我們通常會遇到按鈕的事件丟失等問題但這是在NET 的環境下根本不存在這種問題況且按鈕在第一次的時候是可以被點擊的程序一直處於運行狀態沒有人修改過代碼讓我非常奇怪!這個問題我反復調試並采用了很多不同的方法去嘗試但是問題依舊如查看頁面上其它部分可能導致的腳本干擾setInterval方法的使用是否會導致程序處於等待狀態(事實上這個根本不可能)
去掉所有可能導致此問題的控件和代碼等等天啊!我幾乎嘗盡了所有能夠想到的辦法但是這塊大石頭依然紋絲不動我崩潰了!!
過了一個周末在家睡了兩天腦海中一直想的就是究竟是什麼原因導致了按鈕的事件不能被觸發我也嘗試過在FireFox下利用FireBug跟蹤按鈕的客戶端代碼執行情況沒有什麼結果周一上班的時候突然想到用排除法來驗證一下看看究竟是哪部分代碼出現了問題因為之前我在本地創建的工程中使用了iFrame提交表單並且利用javascript在頁面往返服務器的過程中動態修改了Form的target屬性並沒有發現按鈕事件不能被觸發的問題說明問題不是出在我所寫的代碼中我在MOSS站點中創建了一個功能一樣的頁面上面只有非常簡單的幾行代碼然後編譯部署激活特定的Feature訪問頁面簡單看了一下功能很正常說明這種方法在MOSS下是可以正常使用的並沒有之前假象的會受到MOSS本身機制的影響
生產環境中的頁面要稍微復雜一些裡面除了一些必須的功能和UserControl外整個頁面是繼承自一個公共的模板頁難道問題出在模板頁上?我又仔細看了看模板頁中的代碼幾乎嘗試著將模板頁中所有的控件都刪除了但是問題依然沒有解決一身冷汗啊一上午的時間就這麼讓我浪費了做過MOSS項目的朋友可能會比較清楚在MOSS上開發項目復雜的並不是如何去寫代碼而是部署和調試經常大把的時間都浪費在這個上面更何況我為了測試這個問題產生的原因還要新建頁面重新部署站點然後調試代碼光這個過程就比較繁瑣了
反正已經開始做了午飯過後我打算徹底搞定它問題既然不是出在頁面本身那一定是出在模板頁上因為之前沒加模板頁的時候是可以的後來將頁面繼承自模板頁後問題就來了在FireFox中查看頁面的源代碼仔細查看生成的HTML和腳本發現在Body和Form標簽上有兩個腳本事件不知道是干什麼用的很好奇問了一下老大他說這是MOSS在新建模板頁時自動加上的沒有誰刻意去加它代碼片段如下
<body onload=javascript:_spBodyOnLoadWrapper();>
<form id=Form runat=server onsubmit=return _spFormOnSubmitWrapper(); method=post enctype=multipart/formdata>
我嘗試著將這兩個事件取消掉然後重新部署運行程序哈哈!終於可以了那個按鈕的事件再也沒有丟失過可以一直被點擊而不會出現不響應的情況其實罪魁禍首的就是form的onsubmit事件中的_spFormOnSubmitWrapper方法取消它就可以解決問題
但是問題馬上又來了既然這個事件是MOSS自動加上的那肯定有它的用途我們不能隨意就將它刪掉說不定以後哪裡就會出問題(雖然我到後來也不太清楚這個函數究竟是用來干什麼的)那麼只能曲線救國了用FireBug看看它的具體代碼吧順便跟了一下
var _spSuppressFormOnSubmitWrapper=false;
function _spFormOnSubmitWrapper()
{
if (_spSuppressFormOnSubmitWrapper)
{
return true;
}
if (_spFormOnSubmitCalled)
{
return false;
}
if (typeof(_spFormOnSubmit)==function)
{
var retval=_spFormOnSubmit();
var testval=false;
if (typeof(retval)==typeof(testval) && retval==testval)
{
return false;
}
}
RestoreToOriginalFormAction();
_spFormOnSubmitCalled=true;
return true;
}
這個方法只要返回true就會觸發服務器端時間如果返回false則不會觸發我反復看了一下導致函數返回false的原因是因為_spFormOnSubmitCalled的值為true那麼我們只需要將這個變量的值設為false即可重新觸發服務器端事件了這個好辦我馬上修改代碼在button按鈕的客戶端事件代碼中這樣寫
//aspnetForm為Form的客戶端nameiframeHidden為隱藏的iFrame的name
documentforms[aspnetForm]target = iframeHidden;
_spFormOnSubmitCalled = false;
return true;
然後服務端返回的時候再將form的target改回_self這樣就可以了!
我不知道MOSS自動加上的那個Form事件是用來干什麼的但至少我讓_spFormOnSubmitCalled變量的值為false可以導致按鈕的事件被觸發並且可以實現我預期的效果
因為我在頁面提交成功後會整個刷新頁面所以也不用擔心修改這個值後會帶來什麼樣的後果最後來看一下服務器端要注冊的腳本
private const string scriptOK = @alert({});
windowparentlocationhref += #;
windowparentlocationreload();;
private const string scriptFailed = @alert({});
windowparentdocumentforms[aspnetForm]target = _self;;
分為兩種如果成功則重新刷新整個頁面如果失敗則修改父頁面Form的target屬性的值為_self你可能會問我為什麼要將父頁面的locationhref加上一個#這主要是為了解決在FireFox下通過iFrame提交表單並重新刷新整個頁面時出現是否重新提交數據的提示(這個問題在IE下不會出現)浏覽器只認URL我們稍微修改一下URL的內容只要地址不變重新刷新頁面時就不會出現是否重新提交數據的提示了
到目前為止我將我的代碼做了這樣的修改不知道會不會遇到什麼問題寫這篇文章的目的有兩個一是記錄一下自己解決這個問題的過程二是想告訴各位正在做MOSS開發的朋友如果遇到通過Ajax方式無法觸發服務器端事件的問題時不妨認真檢查檢查客戶端生成的HTML和腳本找找原因在哪裡
另外如果有哪位朋友能詳細講解一下我所遇到的問題的具體原因以及_spFormOnSubmitWrapper函數的具體含義也歡迎指正!
From:http://tw.wingwit.com/Article/program/net/201311/12554.html