為WebForms說幾句話,以及一些ASP.NET開發上的經驗(3)
2007-12-23 18:44 Jeffrey Zhao 閱讀(14174) 評論(68) 收藏 舉報四、生成復雜的ID難以使用JavaScript操作
我在上一篇文章的最后提到了,雖然使用WebForms我們能夠對于頁面上的HTML屬性和樣式等進行自由的定制和控制,但是有一點是毋庸置疑的,我們沒有辦法(正常的辦法吧,Hack不算)讓服務器端控件在客戶端生成一個簡單的ID。例如,一個TextBox控件,在服務器端的ID是txtUserName,但是最終在客戶端生成的ID可能是LoginForm_txtUserName,因為它被放在一個ID為LoginForm的NamingContainer中。
有了組件模型,就出現了大量控件。控件最主要的目的之一就是復用,而復用的一個特點就是應該高度內聚,而不依賴于外部環境。因此,為了使組件內部的服務器控件最終生成的客戶端ID能夠在頁面上唯一,WebForms引入了NamingContainer這個概念。在NamingContainer中的服務器端控件最終在客戶端生成的ID,會使用NamingContainer的“客戶端ID”作為前綴。如此“遞歸”的做法保證了服務器控件在客戶端的ID唯一。
Web 2.0在業界風卷殘云般的勢頭至今還未停歇,與其有密切相關的AJAX技術也被廣泛使用。AJAX技術從根本上講,是一種在瀏覽器中使用JavaScript實現的技術,因此使用JavaScript操作DOM元素的情況非常多見。在非WebForms的頁面中我們可以編寫如下的代碼:
<input type="text" id="textBox" />
<script language="javascript" type="text/javascript">
document.getElementById("textBox").value = "Hello World!";
</script>
但是由于NamingContainer的緣故,我們在使用WebForms的服務器端的控件時就可能無法通過textBox在客戶端獲得文本框(生成的<input />元素)。為了解決這個問題,服務器端的控件模型提供了一個ClientID屬性,通過這個屬性,我們就可以在服務器端得到控件最終在客戶端的ID。例如,如果上面的代碼放在一個用戶控件里的話,就一定必須寫成如下形式:
<%@ Control Language="C#" AutoEventWireup="true" %>
<asp:TextBox runat="server" ID="textBox" />
<script language="javascript" type="text/javascript">
document.getElementById("<%= this.textBox.ClientID %>").value = "Hello World!";
</script>
此時,當控件被放到頁面上之后,它在客戶端生成的代碼則會是:
<input name="DemoControl1$textBox" type="text" id="DemoControl1_textBox" />
<script language="javascript" type="text/javascript">
document.getElementById("DemoControl1_textBox").value = "Hello World"!;
</script>
請注意<input />元素的name和id,它們都留下了NamingContainer的痕跡。由于我們在頁面上使用了<%= %>標記直接輸出了服務器控件的ID,這樣在客戶端的JavaScript代碼也就可以正確訪問到服務器端<asp:TextBox />對應的客戶端<input />元素了。
這種在設計器很難預測的客戶端ID,就是使用WebForms時所謂的“客戶端ID污染”。
接下來我們不妨來看一個略為復雜點的例子:
<%@ Control Language="C#" AutoEventWireup="true" %>
<asp:TextBox runat="server" ID="textBox" />
<script language="javascript" type="text/javascript">
var counter = 0;
function increase()
{
document.getElementById("<%= this.textBox.ClientID %>").value = (counter++);
window.setTimeout(increase, 500);
}
increase();
</script>
上面這段JavaScript代碼的作用是每500為一個計數器加1,并且顯示在文本框上。隨著項目的發展,頁面上復雜的JavaScript代碼會越來越多,于是我們就會想辦法將其轉移到js文件中并且在頁面上引用它們。使用js文件的好處很多,便于進行代碼管理是一方面,但是最重要的好處之一還是對于性能的提高。如果JavaScript代碼完全寫在頁面上,這樣每次加載頁面都需要下載這些JavaScript代碼,而js文件可以緩存,這樣客戶端只需要在第一次加載時下載這個文件就可以了。減少了客戶端與服務器之間數據通信的大小,也就加快頁面加載的速度,提高了性能。
不過問題就此出來了:為了能夠正確引用到頁面上的某個服務器控件生成的DOM元素,我們就必須在頁面中使用<%= %>標記來輸出控件的ClientID,但是<%= %>無法寫在js文件中,這可怎么辦?于是很多人著急了起來,我也不時會收到此類問題,似乎很難找到合適的解決辦法。于是“客戶端ID污染”似乎也就成了一個使用WebForms時非常嚴重的問題。
有些朋友會說:“這個沒有問題啊,仔細觀察ClientID的組成方式能夠很容易找到規律的。”服務器控件的ClientID是由自身ID和它所在的NamingContainer“樹”來共同決定的,因此在理論上我們也完全可以在設計器得到“已經放置在頁面中”的某個服務器控件的客戶端ID,并將其寫進JavaScript代碼中。話雖如此,的確沒錯,但是這個解決方案實在不好,因為它違背了控件的重要特性:“復用”。作為一個控件來說,它可能會被放在任意的NamingContainer樹下,也就是說,它的客戶端ID在不同的環境中并不固定。另外,如果控件上層NamingContainer樹中有任何一個的服務器端ID被修改的話,js文件中使用的ID就需要進行改變,這樣實在不利于的維護,隨著項目增大,此類問題會愈發明顯。
那么我們究竟該怎么做呢?
在設法解決這個問題之前,我們先來思考一下這個問題。如果我們沒有使用WebForms進行開發,就在普通的頁面上編寫代碼,那么我們對于上面的功能會如何將其提取到js文件中呢?嗯,就直接在代碼中通過textBox這個ID來獲得DOM元素吧。那么好,請您先回答我以下幾個疑問:
- 為什么要寫textBox而不是其他ID呢?
- 如果其他頁面上有個同樣需要實現的功能,而那個文本框的id是txtCounter,那么該怎么作呢?
- 如果一張頁面上有兩個文本框需要顯示這樣的計數器,那么又該怎么做呢?
上面的幾個疑問其實只反應了一件事情,那就是這個計數器的復用性實在太差。什么叫做好的復用性呢?那么我們來看一下一個典型的示例,MaskedEditExtender。我們來看看它是怎么做的:
<ajaxToolkit:MaskedEditExtender
TargetControlID="TextBox1"
Mask="9,999,999.99"
MessageValidatorTip="true"
OnFocusCssClass="MaskedEditFocus"
OnInvalidCssClass="MaskedEditError"
MaskType="Number"
InputDirection="RightToLeft"
AcceptNegative="Left"
DisplayMoney="Left"
ErrorTooltipEnabled="True" />
MaskedEditExtender的第一個屬性TargetControlID,就可以決定了究竟是為哪個文本框添加效果,然后效果的樣式可以由MaskType和Mask決定,獲得焦點的樣式和輸入錯誤的樣式可以由OnFocusCssClass和OnInvalidCssClass屬性決定,連字符輸入的順序都可以定制。
這就是復用:愛怎么用,就怎么用。愛給誰用,就給誰用。想什么時候用,就什么時候用。
要復用,一般總需要組件化或模塊化,內部實現通用的功能,而具體的信息應該由外部傳入。例如我們上面的計數器就應該進行改造(用到了MS AJAX Lib里的Function.createDelegate方法):
function Counter(textBoxId, interval)
{
this._counter = 0;
this._textBox = document.getElementById(textBoxId);
this._interval = interval;
}
Counter.prototype =
{
run : function()
{
this._textBox.value = (this._counter ++);
window.setTimeout(
Function.createDelegate(this, this.run), this._interval);
}
};
現在這個技術器的復用性已經有質的飛躍了,因為我們可以隨意指定一個客戶端的文本框進行顯示,并且可以自由地設置計數器增長的間隔時間。于是我們在WebForms頁面中就可以寫如下的代碼了:
<asp:TextBox runat="server" ID="textBox1" />
<asp:TextBox runat="server" ID="textBox2" />
<script language="javascript" type="text/javascript">
new Counter("<%= this.textBox1.ClientID %>", 500).run();
new Counter("<%= this.textBox2.ClientID %>", 1000).run();
</script>
現在WebForms客戶端ID污染已經不構成問題了吧!
其實解決客戶端ID污染的做法用一句話就能說清:“將不變的部分提取至js文件,將變化的部分(例如服務器控件的客戶端ID)留在頁面中”。但是我在這里將它上升到組件化的高度,因為它能讓我們開發出更優秀的客戶端程序。組件化的客戶端編程方式較之傳統的零散function的做法,更有利于代碼的管理,并且增強了復用性和可維護性。有人說,客戶端ID污染問題使腳本代碼很難做到“內聚”——可能他的意思是將腳本代碼提取到js文件中吧——但是我認為,這種污染“迫使”我們使用組件化的方式進行客戶端開發,而這種組件化或者模塊化的做法恰恰提高了代碼的內聚性。
不過,似乎組件化的編程方式會寫更多的代碼,不是嗎?從理論上來說,可能的確是。不過需要注意的是,我上面提出的例子非常簡單,簡單到了其中的一半代碼是用于“組件化”編程的“骨架”上。而對于一個略為復雜的功能來說,例如一個通用的表單驗證組件,或者客戶端級聯組件,增加的這點“骨架”還算得了什么呢?
這也算是一種因禍得福吧。
相關文章:
為WebForms說幾句話,以及一些ASP.NET開發上的經驗(1):ViewState、性能
為WebForms說幾句話,以及一些ASP.NET開發上的經驗(2): 生成丑陋的HTML,難以進行樣式控制
未完待續:
五、MVC
六、單元測試
浙公網安備 33010602011771號