20. Internet Information Server 應用程式

在 第19章 中,您已經學習到如何建立透過Internet或Intranet與Web伺服器連線的用戶端應用程式,現在則要學習如何使用Visual Basic建立在Microsoft Internet Information Server (IIS)和Active Server Pages (ASP)內執行的應用程式和元件。在我們詳細說明Web伺服器程式設計之前,您至少要對什麼是IIS及如何不使用Visual Basic而執行ASP程式設計有基本的了解。

INTERNET INFORMATION SERVER 4簡介
 

在市面上有許多由不同廠商所推出的Web伺服器應用程式,有的很昂貴,有的則免費。Internet Information Server 4是Microsoft所推出的Web伺服器應用程式,而它屬於"免費"一族。事實上它是Microsoft Windows NT 4 Option Pack的一部份,它與其他如Component Services (正式名稱為Transaction Server,或MTS)、Microsoft Message Queue Server (MSMQ)、和Microsoft Index Server等重要的程式一起放在Option Pack中。您可以由Visual Studio CD安裝Windows NT 4 Option Pack,或者您也可以從Microsoft網站下載。所有這些產品,加上其他策略產品-如Microsoft SQL Server、Microsoft Exchange Server、Microsoft Systems Management Server (SMS)、Microsoft Cluster Server、和Microsoft SNA Server-組成了

Microsoft BackOffice平台,在此平台上您可以建立有效率、有彈性和強固的企業解決方案。

主要功能
 

即使您的主要工作是設計Web站台而非管理它,您還是需要對IIS的功能有最基本的了解。簡單而言,當您執行IIS時,您便將Windows NT電腦轉換為Web伺服器,因此這台電腦便可以接受來自Intranet和Internet用戶端的要求。

IIS 4完全支援HTTP 1.1通訊協定,但它只能接受較舊且較無效能的HTTP 1.0通訊協定的要求。此外,它也支援其他普遍接受的Internet標準,如用來下載檔案的File Transfer Protocol (FTP)和用來在Web應用程式中傳送E-Mail郵件的Simple Mail Transport Protocol (SMPT)。

IIS 4與前一版不同,它可以像MTS元件一樣的執行;這對它的效能有實質上的衝擊。事實上,一個在ASP網頁上執行的指令檔可以起始一個如同MTS元件般執行的ActiveX DLL,並仍將DLL視為同處理序(In-Process)元件。在比較上,在IIS 3下執行的指令檔必須跨越它的處理序範圍以存取MTS內的元件,而且您也會知道跨處理序(Out-Of-Process)元件有多慢。您需要MTS元件來建立可靠的元件基礎交易應用程式。如果您比較關心的是強固性而非效能,您可以在不同的處理序內執行Web應用程式。如此一來,如果應用程式因為錯誤或其他不良的功能而中斷時,其他的應用程式並不會受到影響。

IIS 4也支援多個Web站台,它甚至支援不同的系統管理員-一個站台一位系統管理員。各個Web系統管理員可以完整控制他們所負責的站台-他們可以允許權限、指定內容分級和逾期、啟動記錄檔等等。但他們無法變更會影響IIS內其他站台的通用設定,如Web站台的名稱或指定給每一個Web站台的頻寬。

儘管IIS的功能很強大,但只要使用即簡單又親切的Microsoft Management Console主控台介面就可以管理IIS。您也可以設定讓IIS接受來自Web為基礎的Internet Service Manager (它可以讓系統管理員使用一般的瀏覽器進行遠端管理)的管理命令,您甚至可以撰寫屬於您自己的應用程式-一個可以透過COM物件模型操控IIS的應用程式。由於IIS和Windows NT緊密的整合,系統管理員可以使用他們已熟悉的系統工具來管理使用者和群組,也可以使用標準的偵錯公用程式,如事件檢視器和效能監視器。

The Microsoft Management Console
 

就像剛剛所提及的,您可以透過Microsoft Management Console (MMC) 管理IIS(圖20-1)-就像BackOffice平台上其它大多數的元件一樣。這個工具本身並沒有作用,它比較類似其它snap-in應用程式的收納器。您可以使用Console功能表中的Add/Remove Snap-in命令來安裝或是移除給其它程式使用的snap-in應用程式。


 

圖20-1 Microsoft Management Console

電腦及目錄
 

MMC工具可以用來管理網路上多台電腦。在左邊的窗格中的電腦名稱下,您會發現這台電腦所有的Web及FTP站台。您可以在電腦的節點上按下滑鼠右鍵,在New功能表中選擇Web站台,來新增一個站台。此時會開啟一個精靈詢問您站台的說明,IP位址,連接埠,站台主目錄的路徑及站台主目錄的存取權限。為了發展程式的需要,您可以選擇在IP位址使用 全未指定 的預設值,但必須為每個Web站台指定不同的連接埠。

當使用Web站台時,您必須管理幾個不同型態的目錄。主目錄是當Internet存取這個Web站台時的進入點,為一個本機路徑(或是網路上其它電腦上的目錄)。例如,在筆者的機器上,URL http://www.vb2themax.com 是對應到C:\inetpub\vb2themax目錄。

在主目錄下所有的子目錄都可以在URL中以子目錄方式來存取。例如: http://www.vb2themax.com/tips 就是對應到C:\inetpub\vb2themax\tips目錄。

虛擬目錄並不真正存在於主目錄樹中,但卻會顯示在主目錄樹中。例如筆者可以讓www.vb2themax.com/buglist URL子目錄對應到D:\KnowledgeBase\VbBugs實體目錄。您可以在電腦的節點上按下滑鼠右鍵,在New功能表中選取虛擬目錄,來新增虛擬目錄。在MMC的左窗格中,實體目錄與虛擬目錄會以不同的圖示(icon)來標示。

Web站台、目錄及檔案屬性
 

您可以在Web站台節點上,按下右鍵並選擇Properties功能表來更改Web站台的內容(或是按下工具列上的Properties按鈕)。內容對話方塊有九個頁籤。


 

圖20-3 IIS Web 「內容」對話方塊中的「主目錄」頁籤。

Web application的定義是標示為應用程式起點的目錄中所有的目錄和檔案。按下設定按鈕可將非標準副檔名對應到處理這些檔案的ISAPI應用程式(如處理ASP檔案用的Asp.dll)。Web應用程式可以被設定為在不同的記憶體空間 (獨立的程序中) 執行,意指應用程式可在萬一執行失敗時,還能保護其它應用程式 (包括Web伺服器本身) 不受影響。最後,您可以設定檔案及目錄的使用權限。這些選項有 無  指令檔 (只有指令檔被允許執行)以及 執行 (可執行目錄中的指令檔,DLLs及EXEs)。

 IP位址及網域名稱限制 第二對話方塊中可設定是否讓特定電腦存取此Web站台。當您出版Web站台時,很明顯地您必須給予everyone存取權,但您可強迫Web站台中某些選取的部分需要存取權限。在這頁籤中所有的設定均會繼承所屬電腦節點的內容設定。

若您需要更改實際或是虛擬目錄的內容,您可以在MMC中相對應的節點按下滑鼠右鍵並且選擇功能表中的 內容 (Properties)命令。這些內容對話方塊包含一組頁籤,它們是Web站台 內容 對話方塊頁籤的子集合。因此,筆者不會再一一講解。每個文件檔案的內容對話方塊作用都是一樣的。

提醒您,IIS讓您定義在computer/site/directory/file階層中每個元素的行為及屬性。同時,因為IIS自動將每個物件屬性設定為繼承上一層的屬性,也替您節省很多時間。在這些元素中,內容對話方塊中的頁籤都是完全相同的,且使用者介面符合邏輯及一致性。


小秘訣

請確定NTFS中檔案或目錄的安全設定是否與它在內容對話方塊的安全設定相同。假使兩者間有不同,則IIS會使用兩者中權限較小者。


瀏覽Web站台
 

要瀏覽Web站台上的網頁,必須先啟動該站台,您可在該站台上按下滑鼠右鍵,選擇 開始 ,也可以直接按下工具列上的 開始項目 來啟動。同樣地,要停止或是暫停站台,也可使用類似上述的方法來達成。

要觀看在瀏覽器中網頁如何呈現,您可在右半邊方格中的HTM或是ASP文件按下滑鼠右鍵,選擇功能表中 瀏覽 命令。在MMC中瀏覽網頁可能會與直接從Windows Explorer瀏覽有不同的結果,因為假使網頁中含有伺服器端的scripts,在MMC中會被正確的執行。這樣的特性可以讓您測試在同機器上所發展的ASP程式。


小秘訣

若您將Microsoft Internet Explorer4.0設定為 透過數據機連線到Internet ,則您要瀏覽本機IIS中的網頁時會發生錯誤,此時請選取 Internet 選項中的 透過區域網路連線到Internet 


當您在檔案上按下滑鼠右鍵時,顯示的本文功能表中,有 開啟舊檔 的命令,它會將該檔案以註冊為HTML編輯器的應用程式開啟。例如,若您安裝過Microsoft InterDev, 開啟舊檔 會將HTM或是ASP類型的檔案以InterDev開啟。

Active Server Pages
 

概括地說,ASP網頁是在Web server上的文件,它混合了HTML程式碼及伺服器端(server-side) scripts。這種scripts可以處理從客戶端瀏覽器傳來的要求,並可對特定的客戶端傳回回應頁─例如透過ADO對資料庫作查詢。這個能力非常重要,因為它可讓您建立「動態的」HTML網頁,來讓所有支援純HTML的瀏覽器下載。因此,ASP在Internet應用程式上扮演舉足輕重的角色,而DHTML應該只用在比較受控制的環境中─如公司中的Intranet (可限定都使用Internet Explorer)。

不要讓「動態的」這個形容詞困擾您,這裡我們不是指DHTML的意思。ASP技術不會是送出含有動畫及特殊效果的網頁。更確切地說,您可以使用它在線上為每個客戶端自訂出網頁。例如您可以讓伺服器接受客戶端的請求,執行資料庫的查詢,並且將結果用標準的HTML表格來傳回給特定的客戶端。


說明

在Windows 95及Windows 98中,您可以執行Personal Web Server 4來發展您的ASP應用程式。然而在正式的Web發展環境中,您絕對需要「真正」的IIS在Windows NT或是Windows 2000伺服器家族上執行。在本書中所有的範例程式都是在Windows NT伺服器上發展的。


ASP概論
 

一個HTML網頁可以有兩種型態的scripts: 伺服器端scripts會在伺服器上執行,並將建立的HTML文件傳回給瀏覽器,而客戶端(client-side) scripts如VBScript或是JScript程序會在客戶端瀏覽器上執行。這兩種型態的scripts在ASP網頁中需要不同的標記,因為ASP篩選器機制需要執行伺服器端scripts而不是將它們送出到瀏覽器,相反地,對於客戶端scripts是直接送出給瀏覽器而不用將它們解譯。

有兩種方法可以在ASP網頁中插入伺服器端scripts。第一個方式是使用<SCRIPT>標記搭配RUNAT屬性來使用,如:

<SCRIPT LANGUAGE="VBScriptl. RUNAT="Server">
' Add server-side VBScript code here.
</SCRIPT>

在LANGUAGE屬性中,您也可指定VBScript或是JScript。不像客戶端scripts,在ASP中,預設的script語言反而是VBScript,所以您可以放心的省略這個設定。第二種方式是使用<%及%>符號來插入伺服器端scripts。例如下面的陳述式將server時間指定給currTime變數:

<% currTime = Now() %>

在ASP範例中筆者不會使用JScript,但為了作完整的介紹,在這裡告訴您如何使用<%及%>符號來改變所有伺服器端scripts的預設語言:

<%@ LANGUAGE = JScript %>

有兩種陳述式可以用script符號圍起來:執行一個命令或是傳回一個值。

要讓陳述式傳回值,您必須在開始的符號後立刻插入一個等於(= 字元)符號,如:

<% = Now() %>

(請注意,您可以在執行命令的陳述式中插入註解,但在傳回值的陳述式不可)。這個VBScript陳述式傳回的值,會被插入在該程式片斷出現在HTML網頁中的地方。這代表您可以(且時常) 在同一列中混合純HTML文字及伺服器端script的程式碼。例如底下是用來顯示server目前日期及時間的ASP文件完整原始碼:

<HTML>
<HEAD><TITLE>Your first ASP document</TITLE></HEAD>
<BODY>
<H1>Welcome to the XYZ Web server</H1>
Today is <% = FormatDateTime(Now, 1) %>. <P>
Current time on this server is <% = FormatDateTime(Now, 3) %>.
</BODY>
</HTML>

您可以用<SCRIPT>標記來將單獨的陳述式或整個常式圍住:

<SCRIPT RUNAT="Server">
Function RunTheDice()
    RunTheDice = Int(Rnd * 6) + 1
End Function
</SCRIPT>

剛才定義的程序可在scipt別處來呼叫:

<% Randomize Timer %>
First die shown <% = RunTheDice %> <P>
Second die shown <% = RunTheDice %>

您也可以使用<% 及%>符號來嵌入VBScript陳述式(不用=符號)。底下的範例比之前的複雜,它交替的使用純HTML及伺服器端陳述式:

<% h = Hour(Now)
If h <= 6 Or h >= 22 Then %>
Good Night
<% ElseIf h <= 12 Then %>
Good Morning
<% ElseIf h <= 18 Then %>
Good Afternoon
<% Else %>
Good Evening
<% End If %>

設計伺服器端VBScript程式
 

撰寫伺服器端script並不會比撰寫客戶端script困難很多(至少在在語句構造上)。撰寫ASP程式最困難處是嘗試要預先考慮到當IIS執行它時要產生什麼script。

一般的VBScipt程式與伺服器端陳述式唯一較不同的是,在後者,有些陳述式是被禁止的,尤其是那些在螢幕顯示對話方塊的陳述式。畢竟,伺服器端script是在無人照顧的server上執行,不會有人在那裡按下訊息對話方塊中的OK按鈕。所以當撰寫伺服器端的VBScript程式時,請勿使用MsgBox及InputBox陳述式。

伺服器端scripts支援包含檔(include files)─也就是在server端的檔案可以在HTML網頁產生時被包含進來。插入包含檔的語法如下:

<!-- #include file="Routines.inc " -->

檔案名稱可以是實際路徑(如C:\Vbs\Routines.inc),此時可以是目前檔案的絕對或是相對檔名,檔案名稱也可以是虛擬的,此時必須稍微地更改一下語法:

<!-- #include virtual="/Includes/Routines.inc" -->

包含檔的副檔名並沒有限制,但通常使用.inc副檔名來跟Web站台上其它的檔案作區分。包含檔的內容實際上可以是任何型態: 純文字,HTML碼,伺服器端sciprts等。唯一的限制是不能為「不完全」的scripts,例如有<SCRIPT>標記,卻沒有對應的</SCRIPT>標記。

典型使用包含檔的時機是讓您的ASP scripts可以引用一些常數值。但當這些常數是來自型態程式庫時,如所有ADO的常數,有更好的解決方法:只要在網頁或是Global.asa檔案最前面加入下面的指令。(關於 Global.asa檔案 ,請參閱本章稍後的小節。)

<!--METADATA TYPE="typelib"
    FILE="C:\Program Files\Common Files\system\ado\msado15.dlll_ -->

伺服器端ActiveX元件
 

假使ASP網頁只能執行用VBScript或是JScript撰寫的伺服器端scripts,將會很困難去發展複雜的Internet應用程式。很幸運地,您可以使用外部ActiveX元件(標準或是自訂)來提高純VBScript的能力。舉例來說,伺服器端script

可以透過ADO Recordset物件所提供的屬性及方法來查詢資料庫。要建立ActiveX元件,您必須使用Server.CreateObject方法來取代較簡單的CreateObject指令,除了細節,您可以像在純VBScript(或是Visual Basic,就此而言)中,來處理所傳回的物件引用。底下的ASP程式碼片段示範如何使用這項能力,對server機器上的Biblio.mdb資料庫中Authors資料表(table)作查詢,並將結果以動態建立的表格傳回:

<%
Dim rs, conn, sql
Set rs = Server.CreateObject("ADODB.Recordset")
' Modify the next lines to match your directory structure.
conn = " Provider=Microsoft.Jet.OLEDB.3.51;" 
conn = conn & " Data Source=C:\Microsoft Visual Studio\Vb98\Biblio.mdb"
' Return all the authors whose birth year is known.
sql = " SELECT * FROM Authors WHERE NOT ISNULL([Year Born])"
rs.Open sql, conn  
%>
<H1>A query on the Authors Table</H1>
<TABLE WIDTH=75% BGCOLOR=LightGoldenrodYellow BORDER=1 
CELLSPACING=1 CELLPADDING=1>
    <TR>
        <TH ALIGN=center>Author ID</TH>
        <TH>Name</TH>
        <TH ALIGN=Center>Year Born</TH>
    </TR>
<%  Do Until rs.EOF %>     
    <TR>
        <TD ALIGN=center> <%= rs("Au_Id")%>      </TD>
        <TD>              <%= rs(" Author" )%>     </TD>
        <TD ALIGN=center> <%= rs("Year Born") %> </TD>
    </TR>
<%  rs.MoveNext
    Loop 
    rs.Close %>
</TABLE>

這個ASP程式傳回的結果如圖20-4。這裡有個重點是,client端的瀏覽器接收到的是純HTML的表格,沒有任何一行伺服器端script程式。不像那些客戶端script,其它人無法偷窺您的程式碼。


 

圖20-4 您可以處理對server上資料庫的查詢,並且傳回純的HTML表格

給client端瀏覽器。

ASP物件模型
 

正如您所看到的,撰寫ASP程式的基本概念是很容易的,特別是當您對script及ADO的程式撰寫已經很熟悉時。要建立完整且具效益的ASP應用程式,您只需要去學習如何使用ASP物件模型,相較於其它您所精通物件階層,那並不複雜。

圖20-5就是ASP物件模型的概要,它是由六個主要物件組成的,筆者將在下面的幾節作深度的介紹。如圖所示,這個模型並不是階層,因為六個物件彼此間並沒有直接的關係。

Request物件
 

Request物件代表從client端瀏覽器所傳回來的資料。它提供六個屬性(QueryString、Form、ServerVariables、Cookies、ClientCertificate以及TotalBytes),前五個屬性實際上是集合(collections),另外包含一個方法(BinaryRead)。所有的屬性都是唯讀的,這很容易理解,因為ASP是在server端執行,它沒有辦法影響從客戶端傳回的資料。


 

圖20-5 ASP物件模型

傳送資料到伺服器
 

要完全瞭解Request物件的特性,就必須先瞭解資料是如何由客戶端傳回的。HTML表單可以使用兩種不同的方法來傳回資料:使用GET方法,或是POST方法。而選擇的方法是依據<FORM>標記中的METHOD特性來設定。例如下面的表單會使用GET方法來把值傳給server。(圖20-6表單的一部份。)

<H1>Send data through the GET method</H1>

<FORM ACTION=http://www.yourserver.com/Get.asp  METHOD=get NAME=FORM1><P>



Your Name: <INPUT name=txtUserName >
Your Address: <INPUT name=txtAddress >
Your City: <INPUT name=txtCity >
<INPUT NAME=reset1 TYPE=reset VALUE=Reset>
<INPUT NAME=submit1 TYPE=submit VALUE=Submit> 
</FORM>


 

圖20-6 表單使用GET方法透過Internet傳送值

ACTION屬性的值代表某個網頁的URL,它將會被執行,且會接收目前網頁三個TextBox控制項的值。當使用者按下表單上的Submit按鈕,並以GET方法來傳送資料時,控制項的值會被加到ACTION參數所指定的URL後面:

http://www.yourserver.com/Get.asp?txtUserName=Francesco+Balena
&txtCity=Bari&txtCountry=Italy&submit1=Submit

請注意,在URL後面會立刻加上一個問號(?),而傳送給server的每一組controlname=value都會使用&符號來區隔。當使用者按下Submit按鈕且瀏覽器完成下載目標網頁時,上述的字串會出現在瀏覽器的「位址」下拉式功能表(combo box)中。一旦您瞭解URL字串是怎麼建立的,您就可以自己來建立它,舉例來說,使用客戶端的VBScript函式,讓按鈕被按下時呼叫這個函式。若您選擇這麼做,就必須自己加上那些符號,並且必須把空白字元使用其它合法的符號來替代:

<SCRIPT LANGUAGE=VBScript>
' This is a client-side script routine.
Sub btnSendData_onclick()
    url = "http://www.yourserver.com/get.asp?"
    url = url & "txtUserName=" & Replace(Form1.txtUserName.Value,"","+")
    url = url & "&txtCity=" & Replace(Form1.txtCity.Value,"","+")
    url = url & "&txtCountry=" & Replace(Form1.txtCountry.Value,"","+")
    Window.Navigate url
End Sub
</SCRIPT>

正如您所看到的,這樣的做法需要更多的程式碼,但是相對地,它也比使用GET方法傳送資料提供更大的彈性, 因為客戶端script可以對資料作驗證及預先的處理。有時,您不需要使用script。例如,您可以使用兩個以上的超鏈結來指向同一個網頁,而儘管您在它們的URL後面加上不同的值,它們還是會指向同一個URL:

<A HREF="http://www.yourserver.com/Get.asp?Request=Titles">
Show me the titles</A>
<A HREF="http://www.yourserver.com/Get.asp?Request=Authors">
Show me the authors</A>

然而使用程式來建立URL也有缺點:有些字元在URL中有特殊的意義。例如,您必須將所有的空白字元以+符號來替換,而使用%字元來替代所有脫逸(escaape)字元。需要瞭解更多關於特殊意義字元的資訊,請參考本章稍後 〈對HTML文字及URL字串編碼〉 小節。

使用GET方法來傳送資料有兩個缺點。第一,因為HTTP協定的限制,URL只能傳送大約1000個字元,所以資料有可能被截斷。其次,在Internet上使用文字來傳輸,會使資料更容易被攔截。(假使您是自己建立URL,第二個問題就比較不嚴重,因為您可以先將資料編碼。)

若您想解決第一個問題,且希望讓資料比較難拿得到,可以使用POST方法來傳送資料。這個方法將資料放入HTTP header來傳送,所以使用者無法在「位址」下拉式功能表中看到這些資料。要使用POST方法取代GET方法傳送資料,只需更改<FORM>標記的METHOD屬性:

<FORM ACTION="http://www.yourserver.com/Get.asp" METHOD=post NAME=FORM1><P>

從客戶端接收資料
 

在客戶端,使用這兩種方法傳送資料,唯一的不同處就是METHOD屬性的值,但在ASP網頁的程式中,對傳入的資料是兩種完全不同的處理方式。當資料是由GET方法傳入時(或是手動在URL後增加資料),您可使用Request物件的QueryString屬性來取得。這個屬性有雙重性質,您可以當作一般屬性來使用,也可以當作集合來使用。

當把它當作集合來使用時,您可以傳入一個表單上的控制項名稱,就可取得該參數的值。底下的Get.asp網頁會取得從表單所傳回的資料:

<H1>This is what the ASP script has received:</H1> 
<B>The entire Request.QueryString: </B> <% = Request.QueryString %>
<P><I>The string can be broken as follows:</I><P>
<B>UserName:</B>  <% = Request.QueryString("txtUserName") %> </BR>
<B>City:</B>  <% = Request.QueryString("txtCity") %> </BR>
<B>Country:</B>  <% = Request.QueryString("txtCountry") %> </BR>

若傳給QueryString的參數,在URL中並找不到對應的名稱,則它會傳回空字串,而不會產生錯誤。您可以善加利用QueryString屬性的集合本質,來將它所包含的值利用For Each ...Next迴圈列舉出來:

<% For Each item In Request.QueryString %>
<B><% = item %></B> = <% Request.QueryString(item) %><BR>
<% Next %>

當client使用POST方法來傳送資料時,QueryString屬性回傳回空字串,而您必須使用Form集合來取得資料:

<B>UserName:</B>  <% = Request.Form("txtUserName") %> </BR>

您也可以使用For Each ...Next來取得在Form集合中所有控制項的值:

<% For Each item In Request.Form %>
<B><% = item %></B> = <% Request.Form(item) %><BR>
<% Next %>

當使用表單上的控制項時,您必須負責處理具有相同名稱的控制項。您必須考慮到兩種情況:當相同名稱的控制項是選擇鈕(radio button)時,或是其它控制項時。當控制項為選擇鈕時,規則很簡單:QueryString或是Form屬性只會傳回使用者所選擇的單一選擇鈕控制項。例如您可以在表單上有下面的控制項:

<INPUT TYPE=radio NAME=optLevel VALUE=1>Beginner
<INPUT TYPE=radio NAME=optLevel VALUE=2>Expert

scipt陳述式request.QueryString("optLevel")-或是Request.Form("optLevel") (當使用POST方法傳送資料時)-將會依據所選擇的控制項傳回1或是2。

當有多個控制項具有相同的名稱,卻不是選擇鈕控制項時,QueryString或Form集合會傳回一個子集合,包含所有值不是Empty的控制項。請記得這個重要的細節:當表單包含兩個名為chkSend的控制項時,Request.QueryString("chkSend") 或是Request.Form("chkSend") 會依據所選擇的核取方塊(check box)而有可能傳回零,1或是兩個元素:

<INPUT TYPE=checkbox NAME=chkSend VALUE="Catalog">Send me your catalog
<INPUT TYPE=checkbox NAME=chkSend VALUE="News">Send me your newsletter

若有兩個以上同名稱的核取方塊,您可以使用count屬性來作區分,如:

<% If Request.QueryString("chkSend").Count = 1 Then %>
    <B>Send:</B> <% = Request.QueryString("chkSend") %><BR>
<% Else
    For i = 1 To Request.QueryString("chkSend").Count %>
        <B>Send:</B> <% = Request.QueryString("chkSend")(i) %><BR>
<% Next
End If %>

前面的原始碼是應用在使用GET方法的情況。而讀取使用POST方法傳送的資料,在伺服器端script原始碼中,與GET方法是相似的,除了使用Form集合來取代QueryString集合。隨書光碟中包含兩個示範由HTM網頁傳送資料到ASP網頁的程式,一個使用GET方法,而另一個則使用POST方法。圖20-7是ASP網頁產生的結果範例。


 

圖20-7 這個網頁是由ASP伺服器端script動態產生並傳回給客戶端,請注意在「位址」下拉式功能表中的URL。

ServerVariables集合
 

每一個來自客戶端瀏覽器的請求,都會在HTTP標頭(header)存放很多資訊,包含關於使用者,客戶端瀏覽器以及文件本身的重要的資訊。您可以利用Request物件的ServerVariables集合來取得這些資訊。要測試這項功能,可以寫一小段伺服器端script來列出這個集合的內容。底下的程式碼摘錄自隨書光碟中的ServerVa.asp檔案:

<H1>The ServerVariables collection</H1>
<TABLE BORDER=1 WIDTH = 90%>
<TR>
    <TH>Variable</TH>
    <TH>Value</TH>
</TR>
<% For Each item In Request.ServerVariables  %>
<TR>
    <TD><B>  <% = item %>                         </B></TD>
    <TD>     <% = Request.ServerVariables(item) %>    </TD>
</TR>
<% Next %>
</TABLE>

這個集合中有些項目有特殊的用途。例如,您可以用下面的程式來判斷網頁是用什麼方法來傳送資料:

<% Select Case UCase(Request.ServerVariables("REQUEST_METHOD"))
    Case "GET"
        'Data is being sent through the GET method.
    Case "POST"
        'Data is being sent through the POST method.
    Case ""
        'No data is being sent from the client.
End Select %>

在這個集合中另外一個重要的項目是HTTP_USER_AGENT,它擁有客戶端瀏覽器的名稱,從而讓您過濾不被支援的HTML陳述式。利如,對於Internet Explorer 4或之後版本的瀏覽器,您可以傳回DHTML程式碼,而其它的瀏覽器則只傳回標準的HTML程式碼:

<%  Supports_DHTML = 0     ' Assume that the browser doesn't support DHTML.
    info = Request.ServerVariables("HTTP_USER_AGENT")
    If InStr(info, "Mozilla") > 0 Then
        ' This is a Microsoft Internet Explorer browser.
        If InStr(info, "4.") > 0 Or InStr(info, "5.") > 0 Then
            ' You can safely send DHTML code.
            Supports_DHTML = True
        End If
    End If
%>

ServerVariables集合還有幾個讀者可能會感興趣的項目,如APPL_PHYSICAL_PATH (應用程式的實體路徑),SERVER_NAME (server的名稱或是IP位址),SERVER_PORT (Server連接埠),SERVER_SOFTWARE (Web server軟體的名稱,如Microsoft IIS 4.0),REMOTE_USER (client的user name),URL (目前網頁的URL,在需要參考server上其它檔案時很有用),HTTP_REFFRER (記載使用者是由哪一個URL鏈結進來的),HTTPS (若使用編碼協定,會傳回on),以及HTTP_ACCEPT_LANGUAGE(客戶端瀏覽器內定支援的語言)。

Cookies集合
 

Cookies是一些片段的資訊,它們儲存在客戶端機器上某個特定檔案中。瀏覽器在每次對伺服器的請求中,都會傳送資訊,而在伺服器執行的應用程式就可以使用cookies來儲存特定客戶端的資訊。這種儲存資料的方法是很重要的,因為HTTP是屬於非狀態(stateless)的協定,伺服器無法將變數與客戶端結合在一起。實際上,伺服器甚至無法判斷一個客戶端是否是第一次瀏覽這個網頁。要解決這個問題,伺服器可以傳送給客戶端一個cookie,客戶端會儲存這個cookie,並於下次對伺服器發出請求時,一併傳回cookie。Cookie會依據在建立時所設定的到期方式來決定何時該失效,範圍可以是目前的Session,指定的日期,或是永遠有效。例如,Web server通常使用永遠有效的cookie來讓使用者自訂他們的網頁。

在ASP程式中,有兩個方法來存取cookies: 當作Request或是Response物件的集合。您必須瞭解這兩種模式的差異。Request.Cookies集合是唯讀的,因為server只是單純的接受client的請求,而您也只能對於隨之而來的cookies作檢查。相反地,您可以透過Response.Cookies集合(稍後〈 Response物件 〉小節中將會講解)來建立並且傳送新的cookies到client。在ASP script中,您可以使用下面的語法來取得cookie的內容。

User Preference: <% = Request.Cookies("UserPref") %>

另外,您也可以使用For Each...Next列舉出所有由客戶端傳回的cookies

但您必須考慮到一點:事實上,一個cookie可能會是由多個值組成的,此時會用副集合來儲存它們。我們可以檢查HasKeys布林屬性,來得知cookie是否是多個值組成的。下面的程式會列出cookies集合及它們的副集合的內容:

<% 
For Each item In Request.Cookies
    If Request.Cookies(item).HasKeys = 0 Then %>
        <% = item %> = <% Request.Cookies(item) %>
<% Else
        For Each subItem In Request.Cookies(item) %>
            <% = item & "(" & subItem & ")" %> = 
            <% Request.Cookies(item)(subItem> %>
<%     Next
    End If
Next %>

其他的屬性及方法
 

Request物件還提供另外兩個屬性,及一個方法。ClientCertficate集合讓您使用更安全的HTTPS協定來傳送資料,而不是較簡單的HTTP協定。安全在Internet上的是很複雜的主題,然而,這已經超出了本書的範圍。

最後剩下的屬性TotalByte,以及Request物件的唯一方法BinaryRead幾乎都是一塊搭配使用。TotalBytes代表client使用POST方法傳送資料到server,而server接受到的資料位元組;而BinaryRead方法會對client所傳回的最原始資料執行低階的讀取:

<%  bytes = Request.TotalBytes
rowData = Request.BinaryRead(bytes)   %>

BinaryRead不能與Form集合一起使用,所以您必須兩者擇一使用

實際上,我們很少會用到BinaryRead方法。

Response物件
 

Response物件代表從Web server送到瀏覽器的資料。它提供五個屬性(Expires、ContentType、CharSet、Status及Pics ),四個方法 ( Write、BinaryWrite、IsClientConnected及AppendToLog),以及一個集合 (Cookies).。

傳送資料到瀏覽器
 

使用Response物件的Write方法,可以傳送一個字串或是符號到客戶端的瀏覽器中。這個方法並不是必要的,因為您可以使用<%=符號來取代它。例如,底下的三個陳述式,結果都是一樣的:

<B>Current Time is <% = Time %> </B>
<% = "<B>Current Time is " & Time & "</B>" %>
<% Response.Write "<B>Current Time is " & Time & "</B>" %>

使用哪一種方式通常取決於您寫程式的風格。然而,若目前正在伺服器端script的區塊中,至少對VB程式設計人員而言,使用Response.Write方法將會使得程式比較容易閱讀。

Buffer屬性可以讓您控制瀏覽器該什麼時候接收網頁。預設值是False,表示當一有網頁內容產生時,瀏覽器就會接收到資料。若讀者將屬性設為True,由ASP網頁所產生的資料會暫存在緩衝區中,直到您呼叫Response物件的Flush方法,才會將一段資料傳送給客戶端。使用緩衝區有兩個好處:第一,某些情況下,使用緩衝區,ASP網頁可能會顯示的較快。其次,也是比較重要的,您可以在任何時候使用Response.Clear方法來放棄之前所產生出來的資料。例如您可以將資料庫查詢的結果先存放到緩衝區,當程序處理完畢時,可以檢查是否有錯誤發生,再決定是否要將既有的資料傳給客戶端:

<%  ' Here you execute the query.
    ...
    If Err Then
        Response.Clear
        Response.Write "An error has occurred"
    Else
        ' Send the result of the query to the client.
        Response.Flush
    End If
%>

您也可以使用Response物件的End方法來停止ASP網頁的執行,並將之前所建立的網頁傳給瀏覽器。

Response物件也可以使用另一種方法傳送資料到客戶端─使用BinaryWrite方法。但是這個方法很少會被使用,一種情況是當您想要傳送非字串的資料到自訂的應用程式時。

Cookies
 

Response物件的Cookies屬性(不像Request物件)可以讓您建立及更改Cookies的值。Cookies集合的特性與Dictionary物件類似,所以您可以簡單地將值指定給這個屬性來建立新的cookie:

<%  ' Remember the user's login name. 
Response.Cookies("LoginName") = Request.Form("txtLoginName")  %>

就如同筆者稍早在 〈Cookies集合〉 一小節所提及的,cookie可以有多個的值。

例如Internet上的購物網站可能會有一個購物袋來讓您存放多個的資訊:

<%  ' Add a new item to the Bag cookie.
    product = Request.Form("txtProduct")
    quantity = Request.Form("txtQty")
    Response.Cookies("Bag")(product) = quantity      %>

讀者必須記住的是,cookies是藉由HTTP header來傳回給客戶端,因為這個原因,他們必須在server傳給客戶端第一行文字之前指定好,否則就會有錯誤發生。若對您來說要在傳送第一行文字之前,就先指定好cookie是不切實際的,您可以透過緩衝區來傳送文字到客戶端。預設中,cookies只有在目前的session中會儲存在客戶端的機器中,而當客戶端的瀏覽器一關閉,cookies就會被刪除。您可以使用下面的程式片斷指定給cookie的Expires屬性一個值,來改變更改這個預設功能:

<%  ' This cookie is valid until December 31, 1999.
    Response.Cookies("LoginName").Expires = #12/31/1999#  %>

Cookie物件還有幾個同樣重要的屬性。您可以在Domain屬性設定一個網域,讓只有在這個網域上的網頁才會接收到這個cookie。Path屬性可以讓您有更多的選擇:您可以讓在網域上只有您指定路徑上的網頁才會接收到這個cookie。最後,Secure布林屬性可以讓您決定是否要透過Secure Sockets Layer (SSL)連接來傳送cookie值。底下的範例使用了所有Cookie物件的屬性:

<%  ' Create a secure cookie that expires after one year and that
    ' is valid only on the /Members path of the vb2themax.com site.
    Response.Cookies("Password") = Request.Form("txtPassword")
    Response.Cookies("Password").Expires = Now() + 365
    Response.Cookies("Password").Domain = "/vb2themax.com"
    Response.Cookies("Password").Path = "/members"
    Response.Cookies("Password").Secure = True   %>

最後一個範例將示範網頁如何判斷使用者是否曾經到訪過,並且展示ASP網頁如何要求使用者輸入該網頁所需要的資料:

<%    ' We're going to create cookies, so we need to turn on buffering.
    Response.Buffer = True  %>
<HTML>
<HEAD></HEAD>
<BODY>

<H1>Using Cookies to manage login forms</H1>
<%    If Request.Cookies("LoginName") <> "" Then 
    ' This isn't the first time the user has visited this site. %>
    It's nice to hear from you again, <% = Request.Cookies("LoginName") %>.
<%    Elseif Request.Form("txtLoginName") <> "" Then
    ' This is the user's first visit to this site, and
    ' she has just filled out the login form.
    ' Save the user's login name as a cookie for subsequent sessions.
    Response.Cookies("LoginName") = Request.Form("txtLoginName")
    Response.Cookies("LoginName").expires = Now() + 365 %> 
    
    Welcome to this site, <% = Request.Form("txtLoginName") %> 
<% Else
    ' This is the user's first visit to this site, ' so prepare a login form.   %>
    This is the first time you've logged in. Please enter your name:<P>
<% url = Request.ServerVariables("URL")%>  
    <FORM ACTION="<%= url %>" METHOD=POST NAME=form1>
    <INPUT TYPE="text" NAME=txtLoginName>
    <INPUT TYPE="submit" VALUE="Submit" NAME=submit1>
    </FORM>
<%  End If %>
</BODY>
</HTML>

上面的網頁可以以三種不同的方式來運作。當這個網頁第一次被存取,會產生一個表單來要求使用者的姓名,如下面圖20-8部分。這個資訊將會從新的被輸出到網頁上,請注意<FORM>標記中如何使用ACTION屬性,來取得txtLoginName控制項的內容,將它存在一個期限為一年的cookie中。(請參閱圖20-8的中間部分)。最後,當使用者再度瀏覽這個網頁時,server可以識別出使用者,並且顯示出"Welcome back"的問候語。(圖20-8的下半部。)


 


 


 

圖20-8 一個ASP script可以根據存放在客戶端的cookie而有不同的行為。

網頁屬性
 

使用Response物件的某些屬性可以讓您控制網頁應該如何被傳回給客戶端。這些屬性都是在HTTP header來傳給客戶端的,因此必須在網頁內容傳給客戶端前先被設定好(或是啟用網頁緩衝區)。

Expire屬性可以讓您設定server傳回的網頁可以在client瀏覽器的快取中保留多少分鐘。例如,若您的網頁內容為股票交易的資料,相對地,您可能希望使用較短的期限:

<%  ' This page expires after 5 minutes.
    Response.Expires = 5    %>

在某些情況下,您可能會需要設定到期的日期及時間,此時您可以透過ExpireAbsoulte屬性:

<%  ' This page expires 1 minute before the year 2000.
    Response.expiresabsolute = #12/31/1999  23:59:00#  %>

您可以將CacheControl屬性設為public來決定是否網頁可以被proxy server快取起來,private代表不被快取。

通常我們會使用ASP程式產生HTML文字傳回給客戶端,除此之外,我們也可使用ASP產生任何由瀏覽器所支援的MIME(Multipurpose Internet Mail Extensions)格式內容給客戶端,此時必須告訴客戶端是什麼格式。要通知瀏覽器傳送的內容格式,我們必須指定給ContentType屬性一個適當值,例如text/plain代表純文字,而text/richtxt代表MIME Rich Text格式。您也可以傳送二進位的資料,例如我們可將資料庫取出的圖片,以image/tiff或是image/gif格式的設定來傳給客戶端。(若您使用二進位格式,必須使用BinaryWrite方法來傳送資料。)

CharSet屬性可讓ASP程式通知客戶端瀏覽器需要使用哪個字元集。這個屬性的值指示瀏覽器使用正確的code page,並確保在客戶端螢幕上顯示出正確的字元。例如要使用Greek code page,在網頁開頭要執行下面的陳述式:

<% Response.CharSet ="windows-1253" %>

到目前為止所列出的屬性,允許您在HTTP標頭中設定一些資料來傳回給客戶端。若您有需要,您也可以使用AddHeader方法來更改標準標頭(header)的資訊,甚至建立自訂的標頭資料。

重新導向
 

您可以使用redirect方法將瀏覽器導向另一個網頁。一個典型的用法是在讀取ServerVariables集合中的資料後,依情況將瀏覽器導向特定的網頁。例如,一個擁有全球性客戶的公司,可以準備不同語言的網頁,並自動將客戶導向適當的網頁去:

<%  If InStr(Request.ServerVariables("HTTP_ACCEPT_LANGUAGE"), "it") Then
        'If the browser supports Italian, go to a specific page.
        Response.Redirect "Italy.asp"
    Else
        ' If the browser doesn't support Italian, use the standard page.
        Response.Redirect "English.asp"
    End If    
%>

Redirect方法必須在傳送任何內容之前使用,因為重新導向的動作是透過傳送HTTP標頭到瀏覽器來達成。

其他的屬性及方法
 

Response物件還提供兩個屬性:Status與Pics以及兩種方法:IsClientConnected與AppendToLog。Status屬性可以設定或傳回HTTP狀態列。例如,若沒有錯誤發生,它回傳回200 OK,而當找不到網頁時,它會傳回404 Page not found。Pics屬性讓您在HTTP標頭中的PICS-Label欄位加入一個值,透過這個值可以讓瀏覽器使用Content Advisor對話方塊設定的規則來過濾掉網頁。

在最近的一個Response.Write動作時,若使用者已經離線,則IsClientConnected函數會傳回False。當伺服器端script正在執行一個很耗時的動作,而您又希望知道客戶端是否依然在線上等待結果,這個屬性會很有用。最後,在IIS中可以開啟紀錄(logging)的功能,而AppendToLog方法可以在Web Server紀錄檔中寫入字串。因為紀錄檔是使用逗號來分隔的,所以不可以傳入含有逗號的字串:

<% Response.AppendToLog "This page was loaded at " & Now()  %>

Server物件
 

從名稱可以看得出來,Server物件代表Web server的應用程式。雖然Server物件只有一個屬性(ScriptTimeout) 以及四個方法(CreateObject、HTMLEncode、URLEncode及MapPath),但它卻在ASP程式中扮演舉足輕重的角色。

ScriptTimeout屬性設定並傳回秒數,當伺服器端script執行的時間超過這個設定,則會被強迫停止,並傳回錯誤訊息給客戶端,內定值是90秒。當程式不小心執行到無窮迴圈時,這個屬性非常的有用。

建立外部物件
 

您已經看過Server物件的CreateObject方法是如何運作的。這個方法可以讓您建立一個外部COM物件的實體,不管這個COM物件是標準程式庫(如ADODB),您自己撰寫的元件,或是從third-party提供者購買的。當這個方法與Script library搭配時特別有用,因為可以建立FileSystemObject或是Dictionary物件來補足VBScript語言先天的不足 (VBScript不提供集合物件以及檔案I/O的陳述式)。底下是個很有趣的範例,它可以動態的建立一個超連結的列表,來連結到同一目錄中其它的HTM文件:

<H1>CreateObject demo</H1>
This page demonstrates how you can use a FileSystemObject object
to dynamically create hyperlinks to all the other pages in 
this directory.<P>

<%
Set fso = Server.CreateObject("Scripting.FileSystemObject")
' Get a reference to the folder that contains this file.
aspPath = Request.ServerVariables("PATH_TRANSLATED")
Set fld = fso.GetFile(aspPath).ParentFolder 
' For each file in this folder, create a hyperlink.
For Each file In fld.Files
     Select Case UCase(fso.GetExtensionName(file))
        Case "HTM", "HTML"
            Response.Write "<A HREF=""" & file & """>" & file & "</A><BR>"
    End Select
Next
%>

請不要忘記,所有您想要建立的物件,都必須在執行Web server應用程式的機器上先註冊。使用On Error Resume Next陳述式來保護每個Server.CreateObject方法,是個不錯的做法。

既使在ASP程式中建立物件的方法與在VB或是VBScript中大同小異,您必須注意兩者間有個重要的差別:物件的範圍是整個網頁,而當網頁完全的被處理後,物件才會被釋放。這意味如果將物件變數設為Nothing並不會摧毀物件,因為ASP仍然存在對該物件的引用,而這可能會造成很多嚴重的後果。請看底下的程式碼:

Dim rs
Set rs = Server.CreateObject("ADODB.Recordset")
rs.Open "Authors", "DSN=Pubs"
...
Set rs = Nothing

在一般VB或是VBScript中,最後一行陳述式會關閉Recordset,並且釋放掉所有相關的資源,但在ASP script中,這動作並不會發生,除非您先明確地關閉Recordset:

rs.close
Set rs = Nothing

對HTML文字及URL字串編碼
 

如同您所知道的,HTML使用中括弧來當作定義標記的特殊字元。雖然這種編碼方式很簡單,但在傳送從某個地方讀入的資料時(如資料庫或是文字檔) 會造成一些問題。事實上,資料庫欄位中的<字元可能會讓瀏覽器誤判為HTML的標記。

最簡單的解決辦法就是求助於Server物件的HTMLEncode方法,它會將資料串轉換回為相對應HTML碼,如此便可以正確的在瀏覽器中顯示。底下的程式使用HTMLEncode方法來顯示資料庫的內容,而這個資料庫存放的是方程式。(方程式非常有可能包含特殊的符號):

<%
Set rs = Server.CreateObject("ADODB.Recordset")
rs.Open "SELECT * FROM Formulas", "DSN=MathDB"
Do Until rs.EOF
    Response.Write Server.HTMLEncode(rs("Formula")) & "<BR>"
Loop
%>

若您想在網頁上直接顯示出HTML碼,而不是想看到被瀏覽器直譯後的結果,HTMLEncode方法也很有用。例如底下的ASP程式會顯示htmltext變數的內容,而不會被瀏覽器當作一般的標記來處理:

This is the typical beginning of an HTML page<P>
<%  htmltext = "<HTML><BODY>"
    Response.Write Server.HTMLEncode(htmltext)
%>

底下是當您使用瀏覽器的「檢視原始檔」時所真正看到的內容:

This is the typical beginning of an HTML page<P>
<HTML><BODY;><P>

這些文字在瀏覽器中會以另一種方式呈現:

This is the typical beginning of an HTML page
<HTML><BODY>

在傳送特殊的<%及%>一組字元時會有點複雜,因為這些字元會讓ASP作語法分析時感到混淆。例如,若您想傳送底下的字串到瀏覽器中顯示:

<% Set obj = Nothing %>

不幸地,您不可能簡單地使用下面的方法來傳送,那會發生"Unterminated string constant"的錯誤:

<%  ' CAUTION: This doesn't work!
    Response.Write Server.HTMLEncode("<% Set obj = Nothing %>")
%>

要避免這種錯誤的辦法,其中之一是使用&(連接字串字元)來分開%>這兩個字元:

<%  ' This works!
    Response.Write Server.HTMLEncode("<% Set obj = Nothing %" & ">")
%>

另一個解決方式是使用倒斜線(\)來告訴ASP script,接下的字元直接顯示而不用作任何處理:

<%  ' This works too!
    Response.Write Server.HTMLEncode("<% Set obj = Nothing %\>".)
%>

URLEncode方法讓您解決在URL中類似的問題。我們第一次遇到這個問題是在 〈 傳送資料到伺服器〉 小節中。我們有時候可能會建立一個客戶端script,而它會使用Window.Navigate方法並依據所指定的URL來開啟新的網頁。有些時候,我們也會在伺服器端scripts中解決這類問題,但這個情況解決的方式很容易。例如您可以建立一個超連結讓使用者跳到其它的網頁去,並且在URL傳遞資料庫欄位的資料:

<%  ' This code assumes that rs contains a reference to an open Recordset.
Do Until rs.EOF
    Response.Write "<A HREF=""Select.asp?Name="
    Response.Write Server.URLEncode(rs("Name")) & """>"
    Response.Write rs("Name") & "</A></BR>"
    rs.MoveNext
Loop
%>

對應的路徑
 

Server物件中最後的方法是MapPath,它會將在客戶端瀏覽器所看到的邏輯路徑轉換為server machine上的實體路徑。當傳入到這個方法的參數是以/或是\字元來開頭,它會被當作相對於應用程式根目錄的路徑; 若沒有這兩個前導字元,它會被當作相對於目前的ASP文件的路徑。下面的程式會將瀏覽器重新導向到根目錄中default.asp網頁:

<%  Response.Redirect Server.MapPath("\default.asp") %>

而下面的程式會將瀏覽器導向到同一目錄中的two.asp:

<%  Response.Redirect Server.MapPath("two.asp") %>

您可以使用下面的程式來確定根目錄,目前目錄以及上一層目錄的名稱:

<%  rootDir = Server.MapPath("\")
    curDir = Server.MapPath(".")
    parentDir = Server.MapPath("..".)     %>

Application物件
 

Application物件代表在IIS中執行的Web server應用程式。每一個存取同網站上網頁的客戶端,都會共享同一個Appication物件變數。Appicaton物件的生命週期是開始於當第一個客戶端存取網頁時,而當管理員停止Web server或是server當機時結束。假使Application物件是在自己的位址空間(address space)中執行,您也可以在Application物件的屬性頁對話方塊中的 Directory 頁籤上按下Unload按鈕來結束該物件。

Application物件的介面很簡單,它只有一個屬性(Value),兩個集合(Contents與StatcObjects),以及兩個方法( Lock與Unlock)。不像我們介紹過的其它物件(但與Session物件相似,Session物件將於稍後講解),Application物件具有兩個事件( OnStart與OnEnd)。

在客戶端間共享資料
 

Application物件最主要的用途就是儲存讓不同客戶端中的script可共享的資料。這些資料可能是資料庫檔案的位置或是用來指明哪些資源可以使用的旗標。這些共享的資料可以透過Application物件的Value來取得,您必須把要存取的變數名稱指定給Value。因為Value是Application物件的預設屬性,通常我們會省略它:

<%  ' Increment a global counter. (WARNING: This might not work correctly.)
    Application("GlobalCounter") = Application("GlobalCounter") + 1
%>

然而上面的程式會產生一個問題。因為Application物件是在目前所連接客戶端中的ASP scripts共享,而多個不同的伺服器端script有可能在同一時間執行同一段程式碼。這將會造成Application變數的值不正確。要避免這個情況,每當您要儲存一個或是一組Application變數時,可以使用Lock及UnLock兩個方法將程式括弧起來:

<%  'Increment a global counter. (This code always works correctly.)
    Application.Lock
    Application("GlobalCounter") = Application("GlobalCounter") + 1
    Application.Unlock
%>

當您採用這個方法時,一次只能有一個script執行圍在Lock及UnLock中critical section的程式。此時第二個到達Lock方法的script會耐心的等候,直到第一個script執行完UnLock方法。

您必須避免在Lock之後,插入耗時的動作,並且應該盡可能的立刻呼叫Unload方法。


注意

一般而言,甚至在讀取Application物件的變數時,我們也必須使用Lock及UnLock方法。這個預防措施看起來是多餘的,但請記住在Windows NT或是Windows 2000 server上,一個執行緒可能會隨時取得優先權。當您在處理包含物件的變數時,上鎖(locking)變得更為重要,因為很多的屬性及方法是不能重覆進入(reentrant)的。


Global.asa檔案
 

Application物件的OnStart及OnEnd事件發生在Web application開始及結束時。問題發生在當Application物件被建立時,還沒有ASP文件被呼叫。因為這個原因,這些事件的程式碼必須放在特定的檔案中,一個叫做Global.asa的檔案,它必須放在application的根目錄中。底下的範例會記錄application開始的時間:

<SCRIPT LANGUAGE=vbscript RUNAT=Server>
Sub Application_OnStart()
    Application("StartTime") = Now()    
End Sub
</SCRIPT>

說明

Global.asa檔案中的事件只有當客戶端存取ASP檔案時才會發生,當客戶端讀取HTM或是HTML檔案時並不會發生這些事件。


您不可以在Global.asa中使用<%及%>定義符號。要插入VBScript程式唯一合法的方式是使用<SCRIPT RUNAT=Server>標記。OnStart事件通常用在當application開始時建立物件的實體,因此每個script中不再需要每次去呼叫Server.CreateObject方法。例如我們可以建立FileSystemObject類別的實體來讓ASP script使用,這將會加快每個script中程式的執行速度:

<SCRIPT LANGUAGE=vbscript RUNAT=Server>
Sub Application_OnStart()
    Set fso = Server.CreateObject("Scripting.FileSystemObject")
    ' This is an object, so we need a Set command.
    Set Application("FSO") = fso    
End Sub
</SCRIPT>

相對於OnStart事件,OnEnd事件比較少用到。一個典型的使用方式是用來釋放資源,例如關閉在OnStart事件中開啟的連結。另外一個比較常用的是將全域變數儲存到資料庫中來維持永續性(例如記數器記錄的網頁拜訪人次),下次application開始時就不用再重新做啟始的動作。


注意

因為Global.asa檔案必須放在application的主目錄中,若兩個application使用相同的主目錄(home directory),則它們也會共享Global.asa檔案,不用說,這可能造成數不盡的問題。例如,若您為了其中一個application改變了Global.asa ,其它的application也會受到影響,因此,筆者強烈的建議您,每個application使用不同的主目錄。


Contents及StaticObjects集合
 

您無法從Value屬性列舉出Application物件的所有變數,因為Value屬性不是集合。但是,Application物件提供您兩個集合讓您可以取得所有scripts可用的全域變數資訊。

Contents集合包含了所有您在script中加入到Application物件的元件引用,而它們可在ASP程式中使用。這個集合可以包含簡單的值,或是由Server.CreateObject方法建立的物件。我們可以使用下面的程式列舉出Application所有的變數:

<%  For Each item In Application.Contents
        If IsObject(Application.Contents(item)) Then
            objClass = TypeName(Application.Contents(item))
            Response.Write item & " = object of class " & objClass
        Else
            Response.Write item & " = " & Application.Contents(item)
        End If
    Next
%>

StaticObject集合與Contents集合相類似,但它只包含在Application範圍中由<OBJECT>標記所建立的物件。因為這個集合保證只包含物件,所以我們可以使用簡單的迴圈來列舉這個集合:

<%  For Each item In Application.StaticObjects
        objClass = TypeName(Application.StaticObjects(item))
        Response.Write item & " = object of class " & objClass
    Next
%>

Session物件
 

Session物件代表指定的客戶端及Web server間的連結。每當客戶端存取Server上的任何一份ASP文件,就會建立一個新的Session物件,並賦予它一個唯一的ID。只要瀏覽器保持開啟,這個物件就會一直存在。在預設中,若server在指定的時間內沒有收到客戶端的請求,這個物件就會被摧毀,這段時間預設值是20分鐘,但您可以透過Session物件的Timeout屬性來更改。舉例來說,若您的網站會接受到大量的請求,您可能會需要藉由縮短Timeout屬性,來隨時釋放分配給使用者的資源:

<%  'Reduce the timeout to 10 minutes.
Session.Timeout = 10 %>

Session物件也類似Application物件提供了Contents與StaticObject集合,Value屬性,以及OnStart與OnEnd事件。這兩個物件主要不同點在於IIS只會為每個application建立一個Application物件,但卻會為每個連結的使用者建立一個Session物件。

在網頁間共享資料
 

Session物件的Value屬性讓您存取session範圍變數,這代表只有被同樣客戶端請求的網頁能共享部分的資料。圖20-9描述application範圍與session範圍變數間的不同點。


 

圖20-9 Application變數與session變數。

在ASP script中的變數在瀏覽器跳到其它的網頁後就會被摧毀,也因此,session變數顯得格外的重要。Session變數為非狀態(stateless) HTTP協定帶來了狀態管理能力。首先,Web瀏覽器似乎無法適當的去區分使用者資訊,因為HTTP協定不會紀錄關於是哪位使用者發出請求。這個問題的解決辦法是由Web server產生一個特殊的cookie,並傳遞到客戶端瀏覽器中,而瀏覽器在隨後對server的請求中,會一併傳回這個cookie讓server來作識別。因為這個cookie並沒有特別指定到期期限,它會在瀏覽器關閉或是timeout時失效。我們也可以使用Abandon方法來強迫Session物件結束。

Value屬性是Session物件的預設屬性,我們可以透過它來取得Session物件中的變數,因為它是預設屬性,所以可以被省略:

<%  'Remember the user's name while the session is open.
Session("UserName") = Request.Form("txtUserName")   %>

Session物件對我們非常有用,但是卻會對application的效能及延展性帶來很大的衝擊。每個Session物件都會耗費一些server的資源,而有些session作業是連續的,因此每個session必須等到輪到它們。若您只有在需要的時候再去建立Session物件,便可減少這個問題。我們可以在ASP script的第一行加入下面這行陳述式來限制Session物件的數目:

<%@ EnableSessionState = False %>

或者,我們可以把HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet。\Services\W3SVC\ASP\Parameters登錄機碼的AllowSessionState的值從1改為0來讓ASP子系統的Session變數失效。

當客戶端存取不支援session狀態的網頁時,不會有任何Session物件被建立,而且Session_OnStart事件程序也不會被執行。這說明您不能再使用Session物件來儲存資料,您必須自己實作永續性機制(persistence scheme),例如使用真正,多使用者的資料庫。

Session事件
 

就如同筆者在前一節所提到的,Session物件提供OnStart及OnEnd事件。而就像Application物件一樣,這些事件的程式碼必須寫在Global.asa檔案中,所有的Session物件共用同樣的事件程序。典型地,我們使用OnStart事件來建立會在session中使用的資源,例如ADO Connection物件。底下的程式示範如何將Application及Session物件互相搭配使用:

<SCRIPT LANGUAGE=vbscript RUNAT=Server>
Sub Application_OnStart()
    ' Initialize the Connect string that points to the shared database.
    ' In a real application, the string might be read from an INI file.
    conn = "Provider=SQLOLEDB;Data Source= MySrv;UserID=sa;Password=MyPwd"
    Application("ConnString") = str
End Sub
Sub Session_OnStart()
    Set cn = Server.CreateObject("ADODB.Connection")
    cn.Open Application("ConnString")
    ' Make this Connection object available to all the ASP scripts  
    ' in the session.
    Set Session("Connection") = cn
End Sub
Sub Session_OnEnd()
    ' Release all the resources in an orderly way.
    Set cn = Session("Connection")
    cn.Close
    Set cn = Nothing
End Sub
</SCRIPT>
</程式碼>
這裡有另一個範例使用OnStart及OnEnd事件來追蹤目前有多少session:
<程式碼>
<SCRIPT LANGUAGE=vbscript RUNAT=Server>
Sub Application_OnStart()
    Application("SessionCount") = 0
End Sub
Sub Session_OnStart()
    Application.Lock
    Application("SessionCount") = Application("SessionCount") + 1
    Application.Unlock
End Sub
Sub Session_OnEnd()
    Application.Lock
    Application("SessionCount") = Application("SessionCount") - 1
    Application.Unlock
End Sub
</SCRIPT>

Locale-aware屬性
 

Session物件提供兩個屬性讓您的Web site可以應付全世界的使用者。LCID屬性設定或傳回本地ID,當我們對字串排序或是比較,或者使用任何關於日期,時間的函數,會需要這個ID。例如下面的程式片段會使用義大利格式來顯示目前日期及時間,並且儲存原始的local ID。

<%  currLocaleID = Session.LCID
    ' The locale ID of Italy is hex 410.
    Session.LCID = &H410
    Response.Write "Current date/time is " & Now()
    ' Restore the original locale ID.
    Session.LCID = currLocaleID
%>

另一個屬性可以讓您的Web site更國際化─CodePage,它可以設定或傳回從瀏覽器讀取或是寫入文字時所使用的code page。例如大多數的西方語言使用code page 1252,而以色列語則使用code page 1255。

Session集合
 

Session物件具有與Application物件一樣的Contents及StaticObjects集合,所以筆者將不再描述這兩個集合。(若要詳細瞭解這兩個集合,請參閱本章前面的 〈 Contents及StaticObjects集合〉 小節)。這些集合只包含session範圍的元素,這裡會有個有趣的問題: 在Global.asa檔案中如何建立一個session範圍的<OBJECT>標記呢? 答案在SCOPE特性中:

<OBJECT RUNAT=Server SCOPE=Session ID=Conn ProgID="ADODB. Connection">

ObjectContext物件
 

在ASP物件模型中,第六個也是最後一個的物件就是ObjectContext物件。這個物件只有在ASP script中執行交易並且使用Microsoft Transaction Server來管理時才有用。像這種關於交易的網頁必須在script開頭加入<%@ TRANSACTION %>。

ObjectContext物件有兩個方法,SetComplete及SetAbort,分別代表commit以及abort交易。因為本書並不包含MTS程式設計,筆者將不再詳細介紹這些方法及事件。


說明

當筆者撰寫本書的同時,Internet Information Server (IIS) 5.0正在beta版本,雖然最終版本可能還會有些異動,還是有可能取得關於ASP技術新功能的資訊。Server物件增加了三個新方法。Execute方法可以執行其它的ASP文件,並將結果傳回到目前的ASP script。Transfer方法類似Response.Redirect方法,但是更有效率,因為它不會傳送任何資料到client瀏覽器中。GetLastError方法傳回對新的ASPError物件的引用,該物件會傳回關於錯誤的細節資訊。Application及Session物件藉由Remove及RemoveAll方法來改善了Contents集合,它們可以讓您刪除在集合中的一個或是全部元素。最後,ASP的語法分析將更具效益,因此網頁將處理的更快。


ASP元件
 

如您所知,ASP script可以產生並且使用ActiveX元件的個體(instantiat),這項功能將大大地增強ASP script的彈性及威力。

在ASP script中使用元件
 

在ASP script中有兩個方法可以用來產生ActiveX元件的個體(instantiat):使用Server.CreateObject方法或是使用<OBJECT>標記並將SCOPE特性設為server。前者的方法較被Visual Basic程式設計人員愛好使用,而後者對HTML程式設計人員來說比較直覺。

然而,某些情況下,使用<OBJECT>標記對Visual Basic程式設計人員是有意義的,那就是建立一個session範圍或是application範圍的物件引用。就好比您想要建立一個ADO Connection讓所有在session中的scripts都可以共用。您可以在Session_OnStart事件程序中建立這個物件,並將引用儲存於Session變數中:

<SCRIPT LANGUAGE=vbscript RUNAT=Server>
Sub Session_OnStart()
    ' Create the ADO Connection object.
    Set conn = Server.CreateObject("ADODB.Connection")
    ' Open it.
    connStr = "Provider=SQLOLEDB;Data Source=MyServer;Initial Catalog=Pubs"
conn.Open connStr, "sa", "myPwd"
' Make it available to all ASP scripts.
    Set Session("conn") = conn

End Sub
</SCRIPT>

ASP script可以使用這個session-scoped的Connection物件,但必須先從Session物件中取出:

<%  ' Inside an ASP script
    Set conn = Session("conn")
conn.BeginTrans               %>

讓我們看看若是在Global.asa使用<SCRIPT>標記加上適當的SCOPE設定來宣告Connection物件,會有什麼事情發生:

<OBJECT RUNAT=server SCOPE=Session ID="Conn" PROGID="ADODB.Connection">
</OBJECT>
<SCRIPT LANGUAGE=vbscript RUNAT=Server>
Sub Session_OnStart()
    ' Open the connection (no need to create it).
    connStr = "Provider=SQLOLEDB;Data Source=MyServer;Initial Catalog=Pubs"
    conn.Open connStr, "sa", "myPwd"
End Sub
</SCRIPT>

當物件是以這種方式宣告的,您可以在目前的application中的任何一個session以它的名稱來使用它,如下面的ASP script:

<%  conn.BeginTrans %>

物件也可以使用這種方式來宣告為application範圍,在這兩種情形,這些物件會儲存在StaticObject集合中。


說明

大多數為ASP網頁設計的元件都是in-process,然而有些時候,您可能會需要建立out-of-process的元件。此時您必須手動更改HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W3SVC\ASP \Parameters登錄機碼中的AllowOutOfProcCmpts,將它由0(預設值)改成1。


使用自訂的ASP元件
 

在ASP網頁中,您可以使用任何符合ActiveX的元件,包括以Visual Basic撰寫的元件。舉例來說,您可以撰寫元件來補強VBScipt較弱的地方,如檔案管理、較快速的數學運算、字串函式等。然而,這些元件並不能真正歸類於ASP元件,因為它們並無法與ASP物件模型互動。我們真正需要的是能夠透過Request物件來讀取HTML表單,以及使用Response物件寫入資料的元件。

以Visual Basic撰寫ASP元件
 

使用Visual Basic撰寫ASP元件是出人意料的容易,它就像撰寫一個標準的ActiceX元件一般,除了一個細節外。第一件事情就是開始ActiveX DLL專案,在專案屬性對話方塊中 一般 頁籤內,將 執行緒模型 設為 公寓模型執行緒 ,並選擇 執行時無使用者介面 。Visual Basic 6為 執行時無使用者介面 提供一個新的選項 保留於記憶體中 (如圖20-10)。當您選擇這個選項,元件將會保留在記憶體中,直到client行程結束。當您預期您的元件會時常的載入記憶體並且被丟棄,這個功能就特別重要,因為它可以節省Windows連續由磁碟將元件載入的負載。當元件在IIS或是MTS中執行時,通常會需要服務上百或是上千的客戶端,因此這個選項將明顯地加速效能。


 

圖20-10 典型ASP元件專案的建議值

現在您必須把引用加入到ASP型態程式庫中。在IIS上有兩個程式庫已經在系統中註冊: Microsoft Active Server Pages Object Library以及Microsoft Active Server Pages 2.0 ObjectContext Class Type Library。前者的程式庫包括五個主要的ASP物件,而後者只包括ObjectContext物件的定義,該物件只有在發展MTS上的ASP元件時會用到。兩個程式庫都在Asp.dll檔案內。

如您所見,ASP元件並沒有什麼特別之處,唯一要解決的問題是:元件要如何取得在五個主要ASP物件的引用呢?當然,我們可以在script程式中建立元件後,將物件引用透過該元件的屬性或是方法來傳回,假使您知道關於在Visual Basic撰寫ASP元件的秘密,您會瞭解上述方式不是必須的。

一旦元件被ASP script建立,IIS會呼叫元件的OnStartPage方法(假使該元件有提供這個方法)。因此,唯一要做的事就是為這個方法加上程式碼:

' This is a class-level variable.
Dim sc As ASPTypeLibrary.ScriptingContext
Sub OnStartPage(AspSC As ASPTypeLibrary.ScriptingContext)
    ' Save the reference for later.
    Set sc = AspSC
End Sub

ScriptingContext物件傳給OnStartPage方法的只有ASP型態程式庫中的根物件(root object)。物件瀏覽器顯露出這個物件有五個屬性─Application、Request、Response、Server以及Session,而它們就是ASP物件模型的主要元素。所以我們可以很容易地設定或是取得Session及Application變數,或是使用Response.Write方法傳送HTML文字:

' Inside the component
Sub IncrementCounter(CounterName As String)
    sc.Application.Lock
    sc.Application(CounterName) = Application(CounterName) + 1
    sc.Application.Unlock
End Sub

當網頁建立實體的元件被unload時,元件會接收一個OnEndPage的事件,您可以在這裡釋放在OnStartPage事件中分配的資源或是關閉連結,但是我們通常會使用Terminate事件來代替。

實際的、有用的元件
 

現在您已經接近的這本書的尾聲,已經有能力撰寫比Hello World更複雜的程式,如ASP元件。在隨書光碟中,您可以找到ASPSample.QueryToTable元件完整的原始碼,它會接受一個連結字串(connection string及一個查詢字串,並且會自動建立HTML表格來顯示查詢的結果。它甚至能夠讓您調整每個欄位排列與格式。

在解釋原始碼之前,讓筆者先告訴您如何在ASP script中使用這個自訂元件:

<%    
Set tbl = Server.CreateObject("ASPSample.QueryToTable")
' Enter the next two lines as a single VBScript statement.
conn = "Provider=SQLOLEDB;Data Source=MyServer;" &
    "Initial Catalog=Pubs;User ID=sa;Password=MyPwd"
tbl.Execute conn, "SELECT * FROM Authors WHERE State = 'CA'"
tbl.GenerateHTML
%>

再簡單也不過了!Execute方法需要連結字串與SQL查詢字串,GenerateHTML方法會傳回元件所產生的HTML文字。您可以使用元件的ShowRecNumbers屬性來微調輸出表格的格式(值若為True則會在最左列顯示紀錄筆數),而AddFiled方法可讓您決定哪些欄位要在表格中顯示,在表格中對應的儲存格,水平或垂直排列的屬性。AddField的語法如下:

AddField FldName,Caption,HAlign,VAlign,PrefixTag,PostfixTag

若要使用預設選項來顯示欄位,只要傳送欄位名稱:

<%  tbl.AddField "au_lname"
    tbl.AddField "au_fname"    %>

您可以像下面的程式來指定欄標頭的標題(假使它與欄位名稱不同) 以及表格儲存格水平與垂直排列的屬性:

<%  tbl.AddField "au_lname", "Last Name", "center", "middle"
tbl.AddField "au_fname", "First Name", "center", "middle" %>

最後,您可以使用PrefixTag及PostfixTag參數來調整儲存格的格式:

<%  ' Display the State field using boldface characters.
    tbl.AddField "State", , "center", , "<B>", "</B>"
    ' Display the ZIP field using boldface and italic attributes.
    tbl.AddField "ZIP", , "center", , "<B><I>", "</I></B>"       %>

這個元件不會對最後兩個參數作驗證,您必須確保傳入的是合法的HTML序列格式。您使用一系列AddField方法所設定的欄位設計,會繼續在接下的查詢中保留,您也可以使用ResetFields方法來清除之前的設計。

實作元件
 

現在您已經知道這個元件是做什麼用的,要瞭解它的原始碼應該不會太困難。這個元件使用UDT型態的Field陣列來儲存每行的顯示資訊。AddFiled方法的動作只有將它本身的引數儲存到陣列中。若script直接呼叫Execute方法而省略呼叫AddField方法,這個元件會建立預設的欄位設計。

' Public Properties ------------------------
' True if Record numbers must be displayed
Public ShowRecNumbers As Boolean

' Private Members ---------------------------
Private Type FieldsUDT
    FldName As String
    Caption As String
    HAlign As String
    VAlign As String
    PrefixTag As String
    PostfixTag As String
End Type

' A reference to the ASP library entry point
Dim sc As ASPTypeLibrary.ScriptingContext
' The Recordset being opened
Dim rs As ADODB.Recordset
' Array information about the fields
Dim Fields() As FieldsUDT
' Number of elements in the Fields array
Dim FieldCount As Integer

當元件由ASP script產生個體,會呼叫它的OnStartPage方法。在這個方法中,這個元件會將引用儲存到ASPTypeLibrary.ScriptingContext物件中,並且初始Field陣列:

' This event fires when the component is instantiated
' from within the ASP script.
Sub OnStartPage(AspSC As ASPTypeLibrary.ScriptingContext)
    ' Save the reference for later.
    Set sc = AspSC
    ResetFields
End Sub

' Reset the field information.
Sub ResetFields()
    Dim Fields(0) As FieldsUDT
    FieldCount = 0
End Sub

Execute方法只是一個ADO Recordset的Open方法的包裝:

' Execute an SQL query.
Function Execute(conn As String, sql As String)
    ' Execute the query.
    Set rs = New ADODB.Recordset
    rs.Open sql, conn, adOpenStatic, adLockReadOnly
End Function

AddField方法對它的引數做了簡單的驗證,並將它們存在Fields陣列中第一個元素:

' Add a field to the table layout.
Sub AddField(FldName As String, Optional Caption As String, _
    Optional HAlign As String, Optional VAlign As String, _
    Optional PrefixTag As String, Optional PostfixTag As String)
    ' Check the values.
    If FldName = "" Then Err.Raise 5

    ' Add to the internal array.
    FieldCount = FieldCount + 1
    ReDim Preserve Fields(0 To FieldCount) As FieldsUDT
    With Fields(FieldCount)
        .FldName = FldName
        .Caption = Caption
        .HAlign = HAlign
        .VAlign = VAlign
        .PrefixTag = PrefixTag
        .PostfixTag = PostfixTag
        
        ' The default caption is the field's name.
        If .Caption = "" Then .Caption = FldName
        
        ' The default horizontal alignment is "left."
        Select Case LCase$(.HAlign)
            Case "left", "center", "right"
            Case Else
                .HAlign = "left"
        End Select
        .HAlign = " ALIGN=" & .HAlign
        
        ' The default vertical alignment is "top."
        Select Case LCase$(.VAlign)
            Case "top", "middle", "bottom"
            Case Else
                .VAlign = "top"
        End Select
        .VAlign = " VALIGN=" & .VAlign
    End With
End Sub

整個QueryToTable元件的中心是GenerateHTML方法,它會使用Recordset的內容以及在Fields陣列中的表格資訊來建立對應的HTML表格。雖然剛開始,這段程式看起來有點複雜,但筆者只花了幾分鐘來撰寫它。筆者使用一個private Send程序來讓程式碼更簡潔,實際上這個程序才真正透過Response物件傳送HTML碼:

' Generate the HTML text for the table.
Sub GenerateHTML()
    Dim i As Integer, recNum As Long, f As FieldsUDT
    ' Initialize the Fields array if not done already.
    If FieldCount = 0 Then InitFields
    ' Restart from the first record.
    rs.MoveFirst
    
    ' Output the table header and the border.
    Send "<TABLE BORDER=1>"
    Send "  <THEAD>"
    Send "   <TR>"
    ' Insert a column for the record number, if requested.
    If ShowRecNumbers Then
        Send "    <TH ALIGN=Center>Rec #</TH>"
    End If
    ' These are the fields' captions.
    For i = 1 To UBound(Fields)
        f = Fields(i)
        Send "    <TH" & f.HAlign & ">" & f.Caption & "</TH>"
    Next
    Send "   </TR>"
    Send "  </THEAD>"
    Send " <TBODY>"
    
    ' Output the body of the table.
    Do Until rs.EOF
        ' Add a new row of cells.
        Send "  <TR>"
        ' Add the record number if requested.
        recNum = recNum + 1
        If ShowRecNumbers Then
            Send "   <TD ALIGN=center>" & recNum & "</TD>"
        End If
        ' Send all the fields of the current record.
        For i = 1 To UBound(Fields)
            f = Fields(i)
            Send "   <TD" & f.HAlign & f.VAlign & ">" & f.PrefixTag & _
                rs(f.FldName) & f.PostfixTag & "</TD>"
        Next
        Send "  </TR>"
        ' Advance to the next record.
        rs.MoveNext
    Loop
    ' Close the table.
    Send " </TBODY>"
    Send "</TABLE>"
End Sub
' Send a line of text to the output stream.
Sub Send(Text As String)
    sc.Response.Write Text
End Sub
' Initialize the Fields() array with suitable values.
Private Sub InitFields()
    Dim fld As ADODB.Field
    ResetFields
    For Each fld In rs.Fields
        AddField fld.Name
    Next
End Sub

在隨書光碟中,可以找到QueryToTable元件完整的原始碼,以及一個使用這個元件,名為Test.asp的網頁。在Visual Basic中撰寫ASP元件的好處是您不用將它們先編譯成DLL再來除錯。這是因為Visual Basic整合發展環境為我們變了一點小魔術:當您使用Visual Basic提供的除錯工具在環境中測試程式時,IIS會以為script正在執行一個in-process的DLL(圖20-11)。圖20-12是這個元件所產生的表格範例。筆者鼓勵您加入其它的屬性及方法來加強這個元件的可用性,例如控制儲存格的顏色,值的格式等。


 

圖20-11在OnStartPage程序中加入中斷點,當ASP scipt呼叫這個方法時,按下F8逐行執行元件的原始碼。


 

圖20-12 範例元件所建立的表格,請注意您可以在網頁上方的控制項輸入新的查詢字串,重新作查詢。

WEBCLASSES
 

Visual Basic 6提供您一個新的工具,讓您發展Internet的程式:那就是WebClass元件。WebClass是在IIS內部執行的in-process元件,它截取並處理客戶端的請求,產生ASP文件。圖20-13解釋WebClass如何運作。


 

圖20-13 WebClass就像是瀏覽器與ASP的媒介

在深入細節前,筆者先澄清一點: WebClasses並不能達到您在ASP script或是以Visual Basic撰寫的元件(或是以其它語言撰寫的ActiveX DLL)所無法達成的事。它的不同點在於這個技術如何在低階中運作,以及程式設計人員如何使用它們。在標準的ASP應用程式中, 在客戶端瀏覽器請求ASP文件時,ASP script將會處理接下的動作,而當HTML網頁送回給客戶端後,便捨棄ASP文件。在WebClass應用程式中,當客戶端瀏覽器瀏覽在application中的網頁時,WebClass就開始存在,此時它會對使用者在網頁上的動作做出反應,例如按下一個超連結或是送出的按鈕時。

使用WebClasses有個關鍵性的好處: 相對於傳統script-based及component-based的ASP程式設計,您可以在Visual Basic的環境中發展WebClass應用程式,因此您可以使用手上現成的偵錯工具。因為WebClass通常是由多個網頁組成的,應該把它視為較高階的元件。例如,只要一個WebClass就可以實作出線上訂單管理系統,比起一組零散的ASP網頁來說,WebClass更容易讓其它應用程式重覆使用。WebClass是抽象的高階元件另一個證明是,在使用者連續的請求之間,您不需要特別去保持session的狀態資訊,例如使用cookies或是session變數,因為WebClass已經幫您處理完所有的事,但這也會降低應用程式的可調適性(Scalability)(您可以決定關閉這個選項,來產生更具可調適性的stateless WebClass元件)。最後,不像ASP scripts,WebClass的程式碼與影響網頁外觀的HTML程式碼是分開的,也因此較容易劃分在建立應用程式時,發展者與HTML編寫者的工作。

第一印象
 

我們使用Visual Basic 6提供的特殊設計師建立WebClass。WebClass設計師不像DHTMLPage設計師可以讓您不需要外部的HTML編輯器而直接設計網頁,它必須引進在Visual Basic外部建立好的網頁。設計師所做的只有繪出那些在網頁上能傳送請求到server的項目,例如FORM標記中的ACTION屬性或是超連結元素的HREF屬性。一般來說,那些包含有URL的標記或是屬性,應該就是向server發出請求的來源。當接收到特殊的請求時,您可以將這些項目與WebClass執行的動作結合起來。就某種意義來說,這個情況只不過是將Visual Basic事件導向程式模型應用在IIS及ASP應用程式上。

建立IIS專案
 

我們可以選擇IIS應用程式專案來建立WebClass。這個範本專案包含一個WebClass模組並且已經幫您設定好所需的專案屬性。IIS應用程式專案是ActiveX DLL專案型態,它的執行緒模型是 公寓模型執行緒 ,並且包含一或多個WebClass設計師模組。在專案屬性對話方塊的 一般 頁籤中,您會發現它選取了 執行時無使用者介面 (這並不難瞭解,因為它在IIS中執行),並且它也設定了 保留於記憶體中 旗標。當這個旗標被設定時,即使目前沒有WebClass元件在IIS中執行,Visual Basic執行時期程式庫也不會被unload。這個安排讓當有從客戶端來的請求時,WebClass元件可以快速的啟動。

WebClass模組有一些它們自己的屬性。它們包含Name屬性(在程式碼中用來識別WebClass的名稱),NameInURL屬性(在HTML及ASP程式中,WebClass物件的名稱) ,Public屬性(只能為True)以及StateManagement屬性。

StateManagement屬性用來決定在客戶端連續的發出請求時,WebClass的狀態類型,若設為1-wcNoState(預設值),WebClass元件會在送出回應給客戶端瀏覽器後就被終結; 若設為2-wcRetainInstance ,則WebClass元件會在同一個客戶端發出請求時一直存在,兩個選項各有利弊。若WebClass的執行個體被保留,在WebClass中的變數會在每個請求間自動保存,這會讓程式設計人員的工作更簡單。另一方面,每個在server上執行的元件都會佔用記憶體及CPU資源,所以設定。1-wcNoState會建立較具可調適性的解決方案 - 但是增加了程式寫作的複雜度。(要瞭解細節,請參閱本章稍後的 "狀態管理" 小節。)

一個WebClass本身包含並管理多個WebItems。每個WebItem分別代表要傳回給客戶端瀏覽器的HTML網頁。WebItem分為兩種:HTML範本的WebItem以及使用者自訂的WebItem。HTML範本基本上是指把已存在的網頁當作建立回應(response)網頁的範本。這種網頁通常會在將其中某些資料替代過後再傳回給客戶端瀏覽器。而使用者自訂的WebItem並不對應到任何已存在的HTML網頁,它通常使用程式來產生要傳回給客戶端的網頁,例如一長串的Response.Write指令。WebClass可以是只有HTML範本WebItems,只有自訂的WebItems,或是(通常)將兩者混合使用。

要建立IIS應用程式,首先必須先建立專案的目錄結構。要能夠有條理的將這些項目作分類,您必須至少有下面三個目錄:

若您想要使用一個目錄來存放上述三種不同類型的檔案也可以,雖然這不是一個好主意。若您建立了一個HTML範本檔案,而將它與WebClass原始碼檔案放在同一個目錄中,設計師會自動地建立新的HTML檔案,並會以原始檔名加上一個數字來命名。例如,若您有一個Order.htm的範本檔案,設計師將會在同一目錄中建立一個名為Order1.htm的檔案。若原始的範本檔案的目錄與WebClass專案的目錄不相同,就不會有上述更改名稱的動作。若您使用了很多的範本檔案,您也許會喜歡這些由WebClass產生的差異。

增加HTML範本WebItems
 

HTML範本WebItems無疑地是使用上最簡單的。要建立這樣的WebItem,您必須先使用像Microsoft FrontPage或是Micrsoft InterDev之類的HTML編輯器來建立HTML範本檔案。當建立這種範本檔案時,您不需要去注意在網頁中超連結或是URL所指到的目的地,因為它們會被 WebClass設計師 取代掉。同樣地應用在HTML中可以被指定URL的屬性,例如表單的ACTION屬性,或是IMG標記中的SRC屬性。您不可以直接地將表單上的按鈕與事件結合,但是您必須將按鈕所存在的表單中的ACTION屬性與事件結合,該按鈕的類型必須為SUBMIT。

當要匯入任何HTML範本檔案之前,您必須先儲存IIS應用程式。這個步驟是必要的,因為Visual Basic需要知道被更改的HTML範本檔案要儲存到哪個地方。如同在上一節所提及的,我們通常會把原始HTML檔案儲存在不同的目錄,也因此我們不需強迫Visual Basic以不同的名稱來建立被更改過的範本檔案。當您儲存專案後,您可以按下 WebClass設計師 工具列由左邊數過來的第五個按鈕。這個動作會建立一個新的HTML範本WebItem,您可以為它取一個有意義的名稱,它會用來在程式中辨識這個WebItem。

圖20-14展示已經匯入兩個HTML範本檔案後的 WebClass設計師 。如您所見,StartPage WebItem包含三個可以傳送請求到server並且可以在WebClass產生事件的項目- BODY元素中的BACKGROUND屬性及兩個超連結。即使設計師會顯示出所有請求的來源,大多數的情況下,您只能著重在它們的一小部份,例如超連結﹐ FORM標記中的ACTION屬性,以及IMG標記中的SRC屬性。若原始HTML檔案中的標記包含ID,則設計師會用此ID來識別這個標記,否則設計師會為每個標記指定一個唯一(unique)ID,這讓標記具有產生事件的能力。例如在網頁中第一個沒有ID的標記將被命名為Hyperlink1,而第二個沒有ID的標記將被命名為Hyperlink2等。這個ID是暫時的,如果您沒有將任何的標記特性和事件或webitem相連接,則在HTML網頁中ID不儲存,一旦當您連接標記特性時,ID將變成永久的並且儲存在HTML檔案中。


說明

若HTML範本檔案本身有錯誤時-舉例而言,不成對的標記 - 在設計師匯入範本時會產生錯誤。另外,您只能匯入METHOD屬性為POST的表單,因為只有這種表單發出請求到server時才會被WebClass捕捉。若您要加入HTML範本WebItem,但它包含使用GET方法的表單,此時「WebClass設計師」會發出警告,並且自動將METHOD方法改為POST方法。



 

圖20-14 建立兩個HTML範本WebItem之後的「WebClass設計師」。

連接WebItem
 

要啟動請求的潛能來源之一,(在WebClass中請求變成事件),您必須將它連結到WebItem或是自訂事件,您可以在 WebClass設計師 右半邊中所顯示的來源上按下滑鼠右鍵來達成。現在,讓我們專注於功能表上的 連接至WebItem 指令,它會顯示如圖20-15的對話方塊。在這個對話方塊中,您可以選擇目前在WebClass所中所定義的WebItems。(您不可以將特性連接到其它WebClass所定義的WebItem)。在您關閉對話方塊後,會在 目標 資料行中顯示已經和特性連接的webitem的名字。


 

圖20-15 這個對話方塊出現在當您將HTML範本上的標記特性連接到WebClass中的WebItem時。

在Visual Basic專案中,您不可以編輯原始的HTML範本,但是您可以使用設計師的工具列來呼叫您喜好的HTML編輯器。您可以透過功能表上的 工具 選項對話方塊上的 進階 頁籤, 外部的HTML編輯器 選項來更改要使用哪個編輯器。

Visual Basic發展環境中會持續的監控HTML範本檔案的日期及時間,一旦它發現有任何的更改,就會詢問您是否要重新載入新版本的檔案到設計師中並分析其語法。您也可以在HTML範本WebItem上按下滑鼠右鍵,並選擇快顯式功能表上的「重新整理」指令來更新範本。當某些時候,Visual Basic無法正確的辨別出HTML範本檔案是否被更改過時,這個指令會很有用。設計師會維持您之前所設定的關聯,所以您不需要在每次編輯HTML範本檔案時去將特性重新連接到WebItem。

當在HTML檔案中有引用其它的HTML檔案時,這寫檔案必須手動的放置到與WebClass專案相同的目錄或是它的子目錄中。同樣的原因,所有的URL必須相對於目前的子目錄,所以您可以任意的複製這些檔案到散佈應用程式專用的目錄而不用去編輯它們。只有在兩種情況下可以接受絕對路徑,一是當您所指到的檔案是放在網站中的特定目錄時,另一個是當您所指到的檔案是在另一個網站時。

撰寫程式
 

當您要執行這個專案時,必須先在WebClass設計師中撰寫程式。您必須撰寫程式來告訴WebClass在被啟動(activated)時該做什麼動作,並且告訴它當第一個WebClass應用程式啟動時必須傳送哪一個WebItem到客戶端。

當瀏覽器瀏覽主ASP檔案時,在WebClass的Start事件中的程式碼決定了當WebClass應用程式被啟動時要有哪些動作。通常在這個事件中典型的動作是將一個WebItem指定給WebClass的NextItem屬性,這會將瀏覽器導向到這個WebItem,例如底下的程式碼:

' This event fires when the WebClass is activated for the first time,
' that is, when a client browser references its main ASP file.
Private Sub WebClass_Start()
    Set NextItem = StartPage
End Sub

在這個事件程序中,您可能會需要作額外的動作,例如將資料庫查詢的結果傳回給客戶端的瀏覽器。將WebItem指定給NextItem屬性時,並不會立刻的改變執行的流程,因為Visual Basic只有當目前的程序執行完成時,才會在目標WeItem產生Respond事件。

當您準備要傳送資料到瀏覽器時,可以呼叫WebItem的WriteTemplate方法,如底下的程式:

' This event fires when the user jumps to the StartPage page.
Private Sub StartPage_Respond()
    StartPage.WriteTemplate
End Sub

此時,客戶端的瀏覽器會顯示StartPage.htm網頁。上述的情況比較特殊,因為start網頁不包含任何需要被替換的部分,所以WebClass將它會完全不改的傳送到客戶端。當使用這按下超連結,WebClass會接收到超連結所連接的WebItem所產生的Respond事件。再一次,我們藉由執行被呼叫的WebItem中的WriteTemplate方法來回應這個事件:

' This event fires when the user clicks the hyperlink on the 
' StartPage page that is linked to the QueryOrder WebItem.
Private Sub QueryOrder_Respond()
    QueryOrder.WriteTemplate
End Sub

在剛開始,您可能會對大量的程式碼只為了執行一個簡單的應用程式感到困惑。然而請不要忘記,WebClass真正發揮功能是在傳回給客戶端包含動態資料的網頁時。

您可以執行目前為止所建立的應用程式:Visual Basic發展環境會啟動IIS並且將會把start網頁載入到Internet Explorer。您可以在WebClass_Start事件中設定一個中斷點(breakpoint)看看當您按下超連結時會發生什麼事。請記得只有在執行IIS的機器上,您才可以對WebClass偵錯。同樣地,在偵錯的過程中,最好是只有一個瀏覽器的實體在執行,因為Visual Basic不會紀錄是哪一實體正在顯示由WebClass所產生的資料,而偵錯的過程中可能會影響到所有的實體。

擴展範例程式
 

要學習如何使用WebClasses最好的方法就是觀察一個完整的範例。因此,筆者準備了一個以SQL Server 7所附的NorthWind資料庫為基礎的實用範例。這個簡單的應用程式讓使用者完成三件不同的事情:

圖20-16描繪出這個範例應用程式的大綱。如您所見,它具有八個範本WebItems以及兩個自訂的WebItem。這張圖並沒有顯示出所有可能的超連結,例如像在查詢後將使用者帶回到StartPage WebItem或是訂單確認或是放棄時的超連結。


說明

為了要能夠正確的執行這個範例,您必須建立一個名為NorthWind的System DSN,並且指到SQL Server 7中的NorthWind資料庫。假使您沒有安裝SQL Server 7,您可以使用Access的Upsizeing精靈將NWind.mdb資料庫轉換到SQL Server 6.5中,建立一個SQL Server 6.5的版本。



 

圖20-16 範例應用程式的結構。

因為很多因素,在這個應用程式中最複雜的WebItem就算是Products了。這個WebItem必須顯示讓使用者輸入查詢條件的欄位,以及將目前查詢的結果以表格來顯示出。通常,若網頁中包含會變動列數的HTML表格,您必須使用自訂的WebItem,因為您必須使用Response.Write方法來動態的產生表格,就如同您在一般未使用WebItem的ASP應用程式中做的一樣。


說明

當筆者正在撰寫本章時,Microsoft正啟用新的網站,網址為http://vblive.rte.microsoft.com.。這個網站完全地使用WebClasses所寫成,這是瞭解這項技術的遣力最好地方。甚至最令人驚訝的,您可以下載這個網站完整的原始碼,藉此學習很多關於如何使用WebClasses的技巧。


WebClass基本技巧
 

現在您對WebClass已經有了基本的認識,我們來看看如何使用WebClass來解決您在發展IIS應用程式可能面對的問題。

存取ASP物件模型
 

使用WebClass程式模型其中之一的好處是,您可以把ASP物件模型中的所有物件當作是WebClass本身的屬性來存取。例如,要在輸出流(output stream)中寫入HTML碼,您可以使用Response物件,如下面:

Response.Wrote "<BODY>" & vbCrLf。

除了ASP主要的物件外(Request,Response,Server,Application以及Session ),在WebClass中其它的物件也可以使用: BrowerType物件。藉由這個物件,WebClass可以查詢客戶端瀏覽器的功能,例如是否支援ActiveX控制項、cookies以及VBScript。所有上述的功能都可當作是屬性使用,例如:

If BrowserType.VBScript Then
    ' Send VBScript code to the client browser.
    ...
ElseIf BrowserType.JavaScript Then
    ' Send JavaScript code to the client browser.
    ...
End If

其它支援的屬性,它們的功能都可以由名稱來瞭解,它們是Frames、Tables、Cookies、BackgroundSounds、JavaApplets、ActiveXControls、Browser (可以傳回"IE"或是 "Netscape")、Version、MajorVersion,MinorVersion及Platform (可以傳回"Win95" 或是 "WinNT")。這個物件會需要在IIS主目錄下與IIS同時安裝的Browscap.ini檔案。使用編輯器來查看該檔案可以讓您對BrowerType可以支援或是傳回的值有更進一步的認識。並且請記得每隔一段時間就去Microsoft的網站去下載該檔案的最新版本,它會包含較新版本的瀏覽器資訊。您也可以從別的網站找到這個檔案的最新版本,例如: http://www.cyscape.com/browscap 。

WebClass事件
 

就像所有的類別一樣,WebClass模組有它們自己的生命週期。底下是與WebClass生命週期有關的事件:

WebClass不提供Load及Unload事件;您可以把常放在這兩個事件中的動作分別使用BeginRequest及EndRequest事件來取代。

標記的取代
 

使用WebClasses相對於使用純ASP程式來說,優點在於您不用在網頁中寫script程式來產生動態的內容。至少在最簡單的情況下,WebClass提供了更好的解決辦法。

若您需要傳送到客戶端瀏覽器一個包含會變動部分的網頁-例如使用者名稱,訂單的加總或是產品的詳細資料-您只需要插入一對特殊的標記到HTML範本網頁中。當WebClass處理這個範本時,典型的方式是使用WriteTemplate方法,相對應的WebItem物件會接收一系列的ProcessTag事件,而每一個事件代表一對特殊的標記。在這個事件中,您可以指定參數來置換標記內合適的內容。


說明

WriteTemplate方法提供一個選擇性參數Template,它可以讓您指定傳回瀏覽器的範本,當您必須在幾個具有很多相同事件的範本作選擇時,這個屬性會很有用。


預設能用來產生ProcessTag事件的標記為<WC@tagname> 以及/WC@tagname。WC@就是在WebItem中標記的前置記號;而tagname是標記名稱,用來在ProcessTag事件中辨識哪一對標記要被取代。底下是一個HTML範本網頁的一小部份,它包含了兩組這種標記,它們將會分別被使用者名稱及目前日期時間取代:

<HTML><BODY>
Welcome back, <WC@USERNAME>Username</WC@USERNAME>. <P>
Current date/time is <WC@DATETIME></WC@DATETIME>
</BODY></HTML>

被開始與結束的WC@標記括起來的文字是標記內容。底下的程式用來在WebClass模組中處理標記,並且以有意義的資訊將它們取代:

' This code assumes that the previous template is associated
' with a WebItem named WelcomeBack.
Private Sub WelcomeBack_ProcessTag(ByVal TagName As String, _
    TagContents As String, SendTags As Boolean)
    Select Case TagName
        Case "WC@USERNAME"
            ' Replace with the user's name, held in a Session variable.
            TagContents = Session("UserName")
        Case "WC@DATETIME"
            ' Replace with the current date and time.
            TagContents = Format$(Now)
    End Select
End Sub

在這個事件的剛開始,TagContents引數會存有在WC@標記中所找到的文字。大多數的情況下,只有在輸出取代的字串時會使用到這個參數,但您也可以用它來辨識出要用哪一類型方式來取代。例如,在隨書光碟中有一個簡單的QueryResults WebItem應用程式中只使用了一個WC@FIELD標記,程式中會把TagContents引數的值用來當作資料庫欄位名稱,將查詢結果以HTML表格方式顯示。您將會在底下的程式片斷中發現這個做法大幅了簡化ProcessTag事件的結構,因為您不需要去一一測試TagName引數:

' The module-level rs variable points to the record that holds the results.
Private Sub QueryResults_ProcessTag(ByVal TagName As String, _
    TagContents As String, SendTags As Boolean)
    If rs.EOF Then
        ' Don't display anything if there isn't a current record.
        TagContents = ""
    ElseIf TagContents = "Freight" Then
        ' This field needs special formatting.
        TagContents = FormatCurrency(rs(TagContents))
    Else
        ' All other fields can be output as they are, but we need to
        ' account for Null fields.
        TagContents = rs(TagContents) & ""
    End If
End Sub

在下一頁中的圖20-17顯示傳回給客戶端瀏覽器的HTML網頁。在ProcessTag事件中代換標記時,有幾點細節是您必須要注意的:

  • 假使您需要,可以更換WC@標記的前置記號。WebItem物件中的TagPreFix屬性中的字串可以在設計時期的屬性視窗或是執行時期時使用程式來設定,如:

     
    QueryResults.TagPrefix = "QR@"


    在Visual Basic的readmevb.htm檔案中建議我們將預設標記的前置記號改為WC (但沒有解釋為什麼要這麼做)。在範例的應用程式中,筆者使用預設的WC@標記工作一切正常,但在製作程式碼時您應該遵照這個建議。

  • 假使在HTML範本中並未包含任何需要被取代的標記,則我們可以將TagPrefix屬性設為空字串來加快執行速度; 這會告訴WebClass網頁不需要置換的工作,可以省略語法分析的過程。
     
  • 在ProcessTag事件的一開始,SendTags屬性會被設定為False,這代表忽略開始及結束的標記並不要送出到輸出資料流。假使您把這個參數設定為True,則取代標記會寫入到輸出資料流中。即使這些標記並不會影響到瀏覽器的文字內容,您沒有必要去設定SendTags參數,除非您也將ReScanReplacements屬性設定為True。(請看下一點。)
     
  • 當在HTML範本中一找到成對取代標記時,就會產生ProcessTag事件。某些情況下,您可能需要作連續掃瞄 - 例如在您離開原始取代標記,或是新增一個取代標記後的第一次掃瞄。要強迫WebClass作連續掃瞄,您必須將WebItem的ReScanReplacements屬性設定為True,您可以在設計時期或是執行時期做這個設定。
     


 

圖20-17 當成功查詢OrderID時,或將結果顯示在client的瀏覽器中。

自訂的事件
 

並不是每個從瀏覽器傳回到server的請求都會直接對應到一個WebItem。事實上,大多數的情況您可能需要使用自訂的程式來處理傳回的請求,再去決定要使用哪一個WebItem,例如當您需要處理傳回來的表單資料,卻發現使用者有一個資訊沒有輸入完全時。在這些情況下,您需自訂的事件。

要建立自訂的事件,您可以在WebClass設計師的右半面板中所顯示的特性上按下右鍵,並選擇 連接至自訂事件 功能表選項。這個動作會建立一個自訂事件,並將這個屬性連接到自訂事件上。然後您可以在設計師的左半面板中,更改這個自訂事件的名稱,同時右半面板也會出現被更改過後的名稱。在您建立好這個自訂事件後,可以在上面連按兩下來輸入處理動作的程式碼。(您也可以使用快顯功能表中的 檢視程式碼 選項。)

在隨書光碟的範例應用程式中,每當筆者必須處理表單中的 Submit 按鈕時,筆者會建立一個自訂事件來處理由使用者輸入的資料,並將它們顯示到同一個網頁上,但是假使資料並不完全,或是不正確,筆者便會顯示一個適合的錯誤訊息。例如底下在QueryOrder WebItem的Submit事件中,程式會檢查使用者輸入的OrderID是否為空字串,然後從Orders資料表中取得對應這個OrderID的訂單記錄。請注意當OrderID是空的或是不對應到任何的訂單時,這個常式會在QueryIrderMsg變數中存入錯誤訊息,並且重新處理QueryOrder WebItem。這個WebItem包含了一個取代標記用來顯示錯誤訊息。(假使在QueryOrderMs g變數中有存入錯誤訊息。)

' The message that appears on top of the QueryOrder page
Dim QueryOrderMsg As String

' This event fires when the user enters an OrderID in the Query
' page and clicks the Submit button.
Private Sub QueryOrder_Submit()
    ' Don't accept a query with an empty order ID.
    If Request.Form("txtOrderID") = "" Then
        QueryOrderMsg = "Please insert an Order ID"
        QueryOrder.WriteTemplate
        Exit Sub
    End If
    OpenConnection
    ' This Recordset has to retrieve data from three different tables.
    rs.Open "SELECT OrderID, Customers.CompanyName As CompanyName," _
        & " OrderDate, RequiredDate, ShippedDate, Freight, " _
        & " Shippers.CompanyName As ShipVia, " _
        & "FROM Orders, Customers, Shippers " _
        & "WHERE Orders.CustomerID = Customers.CustomerID " _
        & "AND Orders.ShipVia = Shippers.ShipperID " _
        & "AND Orders.OrderID = " & Request.Form("txtOrderID")
    If rs.EOF Then
        ' No record matches the search criteria.
        CloseConnection
        QueryOrderMsg = "OrderID not found"
        QueryOrder.WriteTemplate
        Exit Sub
    End If
    ' If everything is OK, display the results.
    Set NextItem = QueryResults
End Sub
Private Sub QueryResults_Respond()
    ' Show the results, and then close the connection.
    QueryResults.WriteTemplate
    CloseConnection
End Sub

(請參閱前面一節中, QueryResults_ProcessTag事件的程式碼 。)兩個不同的常式實際上是執行資料庫連接的開啟與關閉:

' Open the connection to the database.
Private Sub OpenConnection()
    ' Close the Recordset if necessary.
    If rs.State And adStateOpen Then rs.Close
    ' If the connection is closed, open it.
    If (cn.State And adStateOpen) = 0 Then
        cn.Open "DSN=NorthWind"
        Set rs.ActiveConnection = cn
    End If
End Sub
' Close the Recordset and the connection.
Private Sub CloseConnection()
    If rs.State And adStateOpen Then rs.Close
    If cn.State And adStateOpen Then cn.Close
End Sub

有一點是您必須要注意的:一般來說,您不應該在WebClass變數中存放用來讓不同事件程序間共享的資訊,因為假使StateManagement屬性被設定為wcNoState,則WebClass會在連續呼叫時被摧毀,當然,在WebClass中變數的值也是一樣。上面的程式看起來似乎違犯這個規則,因為它在QueryQrderMsg、cn及rs變數中存放資訊。但是若您仔細觀察後會發現,這些資訊絕不會需要在客戶端連續的請求間保持,因此這種處理資料的方式是安全的。例如,QueryQrder_Submit事件指定一個字串給QueryOrderMsg變數,然後呼叫QueryOrder.WriteTemplate方法。這個方法會立刻產生所使用變數的QueryOrder_ProcessTag事件程序。同樣的,這也應用在ADO Recordset分別在QueryOrder_Submit事件開啟以及QueryResults_Response事件關閉時。

自訂的WebItems
 

在之前筆者曾經提到,WebItems有兩種,分別是範本WebItems以及自訂WebItems。相對於範本WebItems與HTML範本檔案相關,自訂的WebItems是由Visual Basic程式所組成,它使用Response.Wrote方法來產生HTML網頁。不用說,使用自訂WebItem比使用範本WebItem困難。然而,使用自訂WebItem可以獲得更大的彈性。例如,當以要建立一個表格來顯示結果而您卻不確定會有多少列時,您通常必須使用到自訂WebItem。

自訂WebItem可以是範本WebItem事件的目標(target),它也跟範本WebItem一樣提供Respond事件及自訂事件。例如,在範例應用程式中的Products自訂WebItem,是StartPage WebItem中超連結的目標。Products WebItem的目的是提供一個表單給使用者,讓他們可以只要在combo box中輸入想要的產品名稱第一個字,就可以選擇該產品的目錄。(如圖20-18。)首先,您可能會覺得使用範本WebItem也可以顯示出類似的表單,但仔細觀察後您會發現所需要的是一個自訂WebItem,因為您必須把產品目錄填入combo box中,這不是範本WebItem簡單的取代方法可以允許的。Products_Respond另外使用了一個名為BuildProductsForm的輔助常式,實際上是由這個常式來產生表單,要分開成兩個程序的原因待會就會明白;

' This event fires when the Products WebItem is reached
' from the Start page.
Private Sub Products_Respond()
    ' Display the Products form.
    BuildProductsForm False
End Sub

' Dynamically build the Products form; if the argument is True,
' the three controls are filled with data coming from Session variables.
Private Sub BuildProductsForm(UseSessionVars As Boolean)
    Dim CategoryID As Long, ProductName As String, SupplierName As String
    Dim selected As String
    If UseSessionVars Then
        CategoryID = Session("cboCategory")
        ProductName = Session("txtProduct")
        SupplierName = Session("txtSupplier")
    Else
        CategoryID = -1
    End If
    ' Build the page dynamically.
    Send "<HTML><BODY>"
    Send "<H1>Search the products we have in stock</H1>"
    Send "<FORM action=""@@1"" method=POST id=frmSearch name=frmSearch>", _
        URLFor("Products", "ListResults")
    Send "Select a category and/or type the first characters of the " _
        & "product's name or the supplier's name<P>"
    Send ""
    ' We need a table for alignment purposes.
    Send "<TABLE border=0 cellPadding=1 cellSpacing=1 width=75%>"
    Send "<TR>"
    Send "  <TD><DIV align=right>Select a category  </DIV></TD>"
    Send "  <TD><SELECT name=cboCategory style=""HEIGHT: 22px; " _
        & "WIDTH: 180px"">"
    ' Fill the combo box with category names.
    ' The first item is selected only if CategoryID is -1.
    selected = IIf(CategoryID = -1, "SELECTED ", "")
    Send "<OPTION " & selected & "VALUE=-1>(All categories)"
    ' Then add all the records in the Categories table.
    OpenConnection
    rs.Open "SELECT CategoryID, CategoryName FROM Categories"
    ' Add all the categories to the combo box.
    Do Until rs.EOF
        selected = IIf(CategoryID = rs("CategoryID"), "SELECTED ", "")
        Send "    <OPTION @@1 value=@@2>@@3</OPTION>", selected, _
            rs("CategoryID"), rs("CategoryName")
        rs.MoveNext
    Loop
    rs.Close
    Send "</SELECT>"
    Send "</TD></TR>"
    
    ' Add the txtProduct text box, and fill it with the correct value.
    Send "<TR>"
    Send " <TD><DIV align=right>Product name  </DIV></TD>"
    Send " <TD><INPUT name=txtProduct value=""@@1"" style=""HEIGHT: " _
        & " 22px; WIDTH: 176px""></TD></TR>", ProductName
    Send "<TR>"
    Send " <TD><DIV align=right>Supplier </DIV>"
    Send " <TD><INPUT name=txtSupplier value=""@@1"" style=""HEIGHT: " _
        & "22px; WIDTH: 177px"">", SupplierName
    Send "<TR><TD><TD>"
    Send "<TR>"
    Send " <TD><DIV align=right> </DIV></TD>"
    Send " <TD><INPUT type=submit value=""Search"" id=submit1 " _
        & "style=""HEIGHT: 25px; WIDTH: 90px"">"
    If BrowserType.VBScript Then
        Send "     <INPUT type=button value=""Reset fields"" id=btnReset" _
            & " Name=btnReset style=""HEIGHT: 25px; WIDTH: 90px"">"
    End If
    Send "</TD></TR></TABLE></P><P></P>"
    Send "</TABLE>"
    Send "</FORM>"
    Send "<HR>"

    ' Insert client-side script for the Reset Fields button.
    If BrowserType.VBScript Then
        Send "<SCRIPT LANGUAGE=VBScript>"
        Send "Sub btnReset_onclick()"
        Send "   frmSearch.cboCategory.Value = -1"
        Send "   frmSearch.txtProduct.Value = """""
        Send "   frmSearch.txtSupplier.Value = """""
        Send "End Sub"
    End If
    Send "</SCRIPT>"

    ' If this is a blank form, we must complete it.
    If Not UseSessionVars Then
        Send "<P><A HREF=""@@1"">Go Back to the Welcome page</A>", _
            URLFor(Default)
        Send "</BODY></HTML>"
    End If
End Sub


 

圖20-18 Products自訂WebItem所產生的表單。combo控制項包含所有在NorthWind資料庫中,Categories資料表內的目錄。

您可能會同意,這不是您所謂的具「可讀性」的程式。但它仍然沒有讓筆者花很多時間去建立。事實上,筆者只執行Microsoft InterDev (當然,您可以使用自己喜好的HTML編輯器),並且建立一個表單,它包含一個表格,而表格上有三個控制項。然後筆者把產生的HTML碼匯入到Visual Basic程式編輯器中,而在這些HTML text 「 附近」加上一些程式。整個過程大概需要10分鐘而已。

前面的常式具有許多有趣的特性。首先,要讓Visual Basic程式看起來更合理,筆者建立一個名為Send的輔助常式,它會使用Response.Write方法將資料傳送到輸出到資料流。但Send常式還做了更多的事;它也提供一個方法,來依據placeholder數目動態取代輸出字串中的變數。這個常式甚至有能力處理出現在引號內字串的取代參數。這些參數必須使用特殊方式來處理,因為若您想要正確地在客戶端瀏覽器中顯示,必須把參數中的每個雙引號變成兩個。底下是這個常式完整的程式碼。如您所見,這段程式不只是可用在這個應用程式,也可用在任何其它的WebClass應用程式中。

' Send a string to the output stream, and substitute @@n placeholders
' with the arguments passed to the routine. (@@1 is replaced by the
' first argument, @@2 by the second argument, and so on.) Only one
' substitution per argument is permitted. If the @@n placeholder
' is enclosed with double quotes, any double quote is replaced by
' two consecutive double quotes.
Private Sub Send(ByVal Text As String, ParamArray Args() As Variant)
    Dim i As Integer, pos As Integer, placeholder As String
    For i = LBound(Args) To UBound(Args)
        placeholder = "@@" & Trim$(Str$(i + 1))
        ' First search the quoted placeholder.
        pos = InStr(Text, """" & placeholder)
        If pos Then
            ' Double all the quotes in the argument.
            pos = pos + 1
            Args(i) = Replace(Args(i), """", """""")
        Else
            ' Else, search the unquoted placeholder.
            pos = InStr(Text, placeholder)
        End If
        If pos Then
            ' If a placeholder found, substitute it with an argument.
            Text = Left$(Text, pos - 1) & Args(i) & Mid$(Text, pos + 3)
        End If
    Next
    ' Send the result text to the output stream.
    Response.Write Text & vbCrLf
End Sub

另一個在BuildProductsForm常式中使用的有趣技巧,是傳送一段VBScript程式到客戶端的機器上,用來處理當使用者按下Reset欄位按鈕的動作。我們不能依靠使用TYPE=Reset設定的標準按鈕,因為這樣的按鈕會回存那些由server端所接收的值,而某些情況下,server不一定會傳回空白的欄位。因為這個原因,要讓使用者清除表單上欄位的唯一辦法,就是提供一個具有客戶端script的按鈕。將伺服器端及客戶端的程式混合使用是很具威力的技巧,而它也提供了最大的可調適性(scalability),因為它把某些工作由server轉到客戶端上執行,而這些工作通常在客戶端上能很容易的完成。然而,有些客戶端瀏覽器也許不能執行VBScript,因此WebClass只有在BrowerType.VBSciprt屬行傳回True時才會送出client-sciprt程式。另一個比較好的辦法是傳送JavaScript程式,這樣一來,不論是MicroSoft或是Netscape瀏覽器都可以接受。

URLFor方法
 

最後一點有趣的是在BuildProductsForm常式中,定義使用者按下Search按鈕後會發生什麼動作。如您所知,範本WebItems及自訂WebItems都可以提供自訂的事件,這些事件會出現在WebClass設計師左半邊面板中。然而,在這兩種WebItems中建立及呼叫自訂事件的方法是不同的。自訂的WebItem在執行時期動態地建立HTML碼,因此設計時無法在右半邊面板中顯示內容。因為這個原因,您只能手動為自訂WebItem來建立自訂的事件-,也就是在WebItem上按下滑鼠右鍵,由功能表選擇 加入自訂事件 。(您也可以同樣的方式為範本WebItem加上自訂事件,但應該會很少用到。)

Products WebItem是一個簡單的應用程式,除了標準的Respond事件外,它還提供兩個自訂的事件,ListResults及RestoreResults。(如圖20-19。)當使用者按下Search按鈕時會產生ListResult事件,而當使用者從OrderRecap網頁回到Products網頁時會產生RestoreResult事件。( 圖20-16 。)當使用者按下Search按鈕時,Product WebItem使用表單中的值來將所有符合該條件的產品以動態建立的表格將結果傳回,傳回的表格將會顯示在原本的表單下面。


 

圖20-19 在加入自訂Products WebItem及它的兩個自訂事件ListResults及RestoreResults後的WebClass設計師。

這裡就會產生一個問題;我們要如何在當使用者按下Search按鈕時,在Products WebItem中產生ListResults事件呢? 這個答案就是底下在BuildProductsForm程序中的一行程式:

Send "<FORM action=""@@1"" method=POST id=frmSearch name=frmSearch>", _
    URLFor("Products", "ListResults")

URLFor方法需要兩個引數,WebItem的名稱及事件的名稱,並且會在請求送到server時產生一個URL,用來產生特定事件給特定的WebItem。您可以省略第二個引數,此時WebClass會啟動預設的Response事件。


小秘訣

URLFor方法的引數型態為Variant,它可接受WebItem物件的引用或是名稱。因為效率的考量,通常會採用傳送WebItem的名稱,如底下的範例:

' The following two lines yield the same results, but the second
' line is slightly more efficient.
Response.Write URLFor(Products, "ListResults")
Response.Write URLFor("Products", "ListResults")

我們回應在自訂WebItem中自訂事件的方式與範本WebItem相同。例如,當使用者按下Products WebItem中的Search按鈕時,會執行底下的程式。如您所見,它重覆使用BuildProductsForm常式,並且執行名稱為BuildProductsTable的輔助常式,它會產生包含查詢結果的HTML表格。

' This event fires when the Product WebItem is invoked
' from the Search button on the form itself.
Private Sub Products_ListResults()
    ' Move data from controls on the form to Session variables.
    ' This allows you to return to the page later and reload these
    ' values in the controls.
    Session("cboCategory") = Request.Form("cboCategory")
    Session("txtProduct") = Request.Form("txtProduct")
    Session("txtSupplier") = Request.Form("txtSupplier")
    ' Rebuild the Products form, and then generate the result table.
    BuildProductsForm True
    BuildProductsTable
End Sub
' This private procedure builds the table that contains the
' result of the search on the Products table.
Private Sub BuildProductsTable()
    Dim CategoryID As Long, ProductName As String, SupplierName As String
    Dim selected As String, sql As String
    Dim records() As Variant, i As Long
    ' Retrieve the values from the Session variables.
    CategoryID = Session("cboCategory")
    ProductName = Session("txtProduct")
    SupplierName = Session("txtSupplier")
    
    ' Dynamically build the query string.
    sql = "SELECT ProductID, ProductName, CompanyName, QuantityPerUnit, " _
        & "UnitPrice FROM Products, Suppliers " _
        & "WHERE Products.SupplierID = Suppliers.SupplierID "
    If CategoryID <> -1 Then
        sql = sql & " AND CategoryID = " & CategoryID
    End If
    If ProductName <> "" Then
        sql = sql & " AND ProductName LIKE '" & ProductName & "%'"
    End If
    If SupplierName <> "" Then
        sql = sql & " AND CompanyName LIKE '" & SupplierName & "%'"
    End If
    ' Open the Recordset.
    OpenConnection
    rs.Open sql
    
    If rs.EOF Then
        Send "<B>No records match the specified search criteria.</B>"
    Else
        ' Read all the records in one operation.
        records() = rs.GetRows()
        ' Now we know how many products meet the search criteria.
        Send "<B>Found @@1 products.<B><P>", UBound(records, 2) + 1
        Send "You can order a product by clicking on its name."
    
        ' Build the result table.
        Send "<TABLE BORDER WIDTH=90%>"
        Send " <TR>"
        Send "  <TH WIDTH=35% ALIGN=left>Product</TH>"
        Send "  <TH WIDTH=30% ALIGN=left>Supplier</TH>"
        Send "  <TH WIDTH=25% ALIGN=left>Unit</TH>"
        Send "  <TH WIDTH=20% ALIGN=right>Unit Price</TH>"
        Send " </TR>"
        ' Add one row of cells for each record.
        For i = 0 To UBound(records, 2)
            Send " <TR>"
            Send "  <TD><A HREF=""@@1"">@@2</A></TD>", _
                URLFor("OrderProduct", CStr(records(0, i))), records(1, i)
            Send "  <TD>@@1</TD>", records(2, i)
            Send "  <TD>@@1</TD>", records(3, i)
            Send "  <TD ALIGN=right>@@1</TD>", _
                FormatCurrency(records(4, i))
            Send " </TR>"
        Next
        Send "</TABLE>"
    End If
    CloseConnection
    
    ' Complete the HTML page.
    Send "<P><A HREF=""@@1"">Go Back to the Welcome page</A>", _
        URLFor("StartPage")
    Send "</BODY></HTML>"
End Sub

這個事件程序執行結果的範例如圖20-20。


 

圖20-20 成功執行查詢的Products資料表。

UserEvent事件
 

讓我們繼續分析BuildProductsTable的程式。請注意在結果表格最左邊欄位中的產品名稱,會使用底下的陳述是建立超連結:

Send "  <TD><A HREF=""@@1"">@@2</A></TD>", _
    URLFor("OrderProduct", CStr(records(0, i))), records(1, i)

在URLFor方法中的第二個引數是使用者所見產品的ProductID。很明顯地,OrderProduct WebItem不能分別為每一個可能的ProductID提供一個事件,實際上,也沒有必要這麼做。當WebClass產生一個WebItem事件,而該事件名稱找不到相對應的標準事件(如Respond) 或是在執行時期的自訂事件時,WebItem元件會接收到一個UserEvent事件。這個事件會接收名為EventName的引數,EventName就是在URLFor方法中所傳入的事件的名稱(第二個引數)。在這個特殊的範例中,當使用者按下結果表格中的產品名稱時,WebClass會產生OrderProduct_UserEvent,並將所選擇產品的ID傳給該事件:

' This event fires when the user clicks on a product's name
' in the Products page, asking to order a given product.
' The name of the event is the ID of the product itself.
Private Sub OrderProduct_UserEvent(ByVal EventName As String)
    Dim sql As String
    ' Build the query string, and open the Recordset.
    sql = "SELECT ProductID, ProductName, CompanyName, QuantityPerUnit," _
        "UnitPrice FROM Products INNER JOIN Suppliers " _
        & " ON Products.SupplierID = Suppliers.SupplierID " _
        & "WHERE ProductID = " & EventName
    OpenConnection
    rs.Open sql
    ' Use the URLData property to send the ProductID to the page
    ' being shown in the browser. This value is then sent
    ' to the OrderRecap WebItem if the user confirms the inclusion
    ' of this product in the shopping bag.
    URLData = CStr(rs("ProductID"))
    ' Write the template. (This fires an OrderProduct_ProcessTag event.)
    OrderProduct.WriteTemplate
    CloseConnection
End Sub

因為OrderProduct是一個範本WebItem,所以UserEvent可以執行WebItem的WriteTemplate方法,它們會依序產生ProcessTag事件。在這個事件程序中的程式會執行取代標記的動作,並且會在表格填入一列關於所選擇產品的資料。(圖20-21。)

' The WebClass fires this event when the OrderProduct template is
' being interpreted. The only WC@ tag in this template is WC@FIELD, and the
' TagContents corresponds to the database field that must be displayed.
Private Sub OrderProduct_ProcessTag(ByVal TagName As String, _
    TagContents As String, SendTags As Boolean)
    If TagContents = "UnitPrice" Then
        TagContents = FormatCurrency(rs("UnitPrice"))
    Else
        TagContents = rs(TagContents)
    End If
End Sub


 

圖20-21 OrderProduct WebItem執行的結果

URLData屬性
 

當使用者輸入要購買產品的數量後會跳到名為OrderRecap的自訂WebItem,在這個WebItem中會顯示出在目前訂單中所有的項目,並計算出之前訂單的加總。要正確地實作出這個WebItem,您必須先解決一個小問題;要如何傳送使用者在OrderProduct網頁所選擇的產品ID呢?假使StateManagement屬性設為wcRetailInstance,您可以很容易的將ID存放在WebClass變數中;但是假使WebClass元件在網頁傳送到瀏覽器後會被摧毀,您就必須使用其它的方法了。

在客戶端請求間保存資料的眾多技巧中,最簡單的一個就是使用URLData屬性。當您指定一個字串給這個屬性,這個字串將會被傳送到瀏覽器中。當瀏覽器傳回下一個請求時會一併傳回這個字串,而WebClass就可以透過查詢URLData屬性來讀取它。換句話說,指定給這個屬性的字串並不會儲存在任何地方,而只是持續的在server與客戶端間傳來傳去。底下是在OrderProduct_UserEvent程序中設定URLData屬性的陳述式:

URLData=CStr(rs("ProductID"))

OrderRecap_Respond事件程序用來處理當應用程式加入新的產品到使用者目前的購物袋時,在這個事件中會使用URLData屬性來取得ProductID。這種購物袋是使用一個儲存在Session變數中的二維陣列來實作:

Private Sub OrderRecap_Respond()
    ' The shopping bag is a two-dimensional array in a Session variable.
    ' This array has three rows: row 0 holds ProductID, row 1 holds
    ' Quantity, and row 2 holds UnitPrice. Each new product appends
    ' a new column.
    Dim shopBag As Variant, index As Integer, sql As String
    ' Retrieve the current shopping bag.
    shopBag = Session("ShoppingBag")

    If URLData <> "" Then
        ' Add a new product to the shopping bag.
        If IsEmpty(shopBag) Then
            ' This is the first product in the bag.
            ReDim shopBag(2, 0) As Variant
            index = 0
        Else
            ' Else extend the bag to include this product.
            index = UBound(shopBag, 2) + 1
            ReDim Preserve shopBag(2, index) As Variant
        End If
        ' Store the product in the array.
        shopBag(0, index) = URLData
        shopBag(1, index) = Request.Form("txtQty")
    End If
    ' Dynamically build the response page.
    Send "<HTML><BODY>"
    Send "<CENTER>"
    If IsEmpty(shopBag) Then
        ' No items are in the bag.
        Send "<H1>Your shopping bag is empty</H1>"
    Else
        ' Open the Products table to retrieve the products in the order.
        sql = "SELECT ProductID, ProductName, CompanyName, " _
            & "QuantityPerUnit, UnitPrice " _
            & "FROM Products INNER JOIN Suppliers " _
            & "ON Products.SupplierID = Suppliers.SupplierID "
        For index = 0 To UBound(shopBag, 2)
            sql = sql & IIf(index = 0, " WHERE ", " OR ")
            sql = sql & "ProductID = " & shopBag(0, index)
        Next
        OpenConnection
        rs.Open sql
        
        ' Build the table with the products in the shopping bag.
        Send "<H1>Your shopping bag contains the following items: </H1>"
        Send "<TABLE BORDER WIDTH=100%>"
        Send " <TR>"
        Send "  <TH WIDTH=5% ALIGN=center>Qty</TH>"
        Send "  <TH WIDTH=30% ALIGN=left>Product</TH>"
        Send "  <TH WIDTH=25% ALIGN=left>Supplier</TH>"
        Send "  <TH WIDTH=20% ALIGN=left>Unit</TH>"
        Send "  <TH WIDTH=10% ALIGN=right>Unit Price</TH>"
        Send "  <TH WIDTH=10% ALIGN=right>Price</TH>"
        Send " </TR>"
        ' Loop on all the records in the Recordset.
        Dim total As Currency, qty As Long
        Do Until rs.EOF
            ' Retrieve the quantity from the shopping bag.
            index = GetBagIndex(shopBag, rs("ProductID"))
            ' Remember the UnitPrice for later so that you don't need to
            ' reopen the Recordset when the order is confirmed.
            shopBag(2, index) = rs("UnitPrice")
            ' Get the requested quantity.
            qty = shopBag(1, index)
            ' Update the running total. (No discounts in this demo!)
            total = total + qty * rs("UnitPrice")
            ' Add a row to the table.
            Send " <TR>"
            Send "  <TD ALIGN=center>@@1</TD>", qty
            Send "  <TD ALIGN=left>@@1</TD>", rs("ProductName")
            Send "  <TD ALIGN=left>@@1</TD>", rs("CompanyName")
            Send "  <TD ALIGN=left>@@1</TD>", rs("QuantityPerUnit")
            Send "  <TD ALIGN=right>@@1</TD>", _
                FormatCurrency(rs("UnitPrice"))
            Send "  <TD ALIGN=right>@@1</TD>", _
                FormatCurrency(qty * rs("UnitPrice"))
            Send " </TR>"
            rs.MoveNext
        Loop
        CloseConnection
        
        ' Store the shopping bag back in the Session variable.
        Session("ShoppingBag") = shopBag
        ' Add a row for the total.
        Send " <TR>"
        Send "  <TD></TD><TD></TD><TD></TD><TD></TD>"
        Send "  <TD ALIGN=right><B>TOTAL</B></TD>"
        Send "  <TD ALIGN=right>@@1</TD>", FormatCurrency(total)
        Send " </TR>"
        Send "</TABLE><P>"
        ' Add a few hyperlinks.
        Send "<A HREF=""@@1"">Confirm the order</A><P>", _
            URLFor("CustomerData")
        Send "<A HREF=""@@1"">Cancel the order</A><P>", _
            URLFor("OrderCancel")
    End If
    Send "<A HREF=""@@1"">Go back to the Search page</A>", _
        URLFor("Products", "RestoreResults")
    Send "</CENTER>"
    Send "</BODY></HTML>"
End Sub

前面的常式使用一個輔助函數來搜尋購物袋中的ProductID,它會傳回符合的欄位索引,當找不到符合的ProductID時會傳回-1:

Function GetBagIndex(shopBag As Variant, ProductID As Long) As Long
    Dim i As Integer
    GetBagIndex = -1
    For i = 0 To UBound(shopBag, 2)
        If shopBag(0, i) = ProductID Then
            GetBagIndex = i
            Exit Function
        End If
    Next
End Function

OrderRecap WebItem處理後的結果如圖20-22所示。


 

圖20-22 OrderRecap WebItem處理後的結果,顯示目前購物袋的內容

專業的風格
 

您可以使用之前所學習的技巧發展出相當地複雜及具威力的WebClass元件。然而,若要開發出更高效率及可調適性(scalable)的應用程式,您還需要學習一些細節。

Navigation
 

一個WebCllass應用程式與一般標準的應用程式間有許多不同點,值得注意的其中之一就是在WebItem間的Navigation。在傳統的應用程式中,程式設計人員任何時候都能夠控制使用者能夠作什麼動作,而且在應用程式中,程式發展者若不允許表單顯示,使用者就不會看到這個表單。相反地,在Internet應用程式中,使用者只要簡單地在瀏覽器位址欄位中輸入URL,就可以瀏覽任何網頁。這個事實會牽連到很多您定義程式結構的方法:

您也可以使用這個方法跳到範例應用程式中的WebItem自訂事件。

Response.Redirect URLFor("Products","RestoreResults")

狀態管理(State management)
 

在發展WebClasss應用程式時,狀態管理扮演很重要的角色。如您所知,HTTP協定本身是非狀態的(stateless),這代表它不會「記得」上一次請求的資訊。就像一般的ASP應用程式,使用WebClass時,您有幾種方法去克服這個問題,而每一種解決方法都各有利弊。

假使WebClass的StateManagement屬性設定為wcRetainInstance,您就可安心的在WebClass變數中存放資訊,因為在客戶端請求間,WebClass的實體(instance) 會一直存在,直到由程式呼叫ReleaseInstance方法後才會被摧毀。使用這種方法的代價就是減少了IIS應用程式的可調適性。同時,因為WebClass元件使用了公寓模型執行緒,因此只能在建立它們的執行緒中執行,當接下來客戶端的請求抵達時,它可能必須等待,直到執行緒變為有空的。


小秘訣

在預設中,IIS會為ASP初始兩個執行緒,並且如有需要會增加數目,每個處理器最多可以有十個執行緒。您可以更改HKEY_LOCAL_MACHINE\SYSTEM\Current-ControlSet\ Services\W3SVC\ASP\Parameters登錄機碼中NumInitialThreads及ProcessorMaxThreads的值來更改所需的數目。您可以把資料存放在Application及Session變數中,這樣資料就可在不同客戶端請求間保存,即使WebClass元件被摧毀然後再建立後。


假使您要存放大量的資料,您可能會需要建立一個伺服器端的元件來存放這些資料,然後指定給Application或Session變數。通常,假如伺服器端元件使用公寓模型執行緒的物件模型 - 就像所有在VIsual Basic中所編寫的元件,您不應該將它們存放到Application變數中。

假使您必須存放大量的資料,您可以求助server機器上的資料庫。這個解決方法可以允許在多個客戶端間彼此分享資料,但它需要在每次讀取或寫入資料時建立一個連接(connection)並且開啟Recordset。開啟或是關閉連接並不會像聽起來那麼沒有效率,因為資料庫連結是可以共用的(pooled)。

就像之前〈URLData屬性〉一小節所介紹的,您可以使用URLData屬性在server與客戶端間來回傳送資料。這個技術相等於使用Request.QueryString屬性,但實作起來比較簡單。使用這個技術好處之一是資料存在於網頁本身中,因此假使使用者按下 上一頁 按鈕且再一次傳送表單資料,WebClass所接收到資料與與原始傳送到網頁資料一樣。另一個好處是,即使瀏覽器不支援cookie,URLData屬性也可以運作。然而這個技術也不是沒有缺點。在URLData屬性中不可儲存超過2KB的資料,而且在來回傳送資料時,會稍微拖慢每個請求的速度。假使HTML網頁中有包含METHOD特性為GET的表單時,您不可以使用這項技術,但這並不能說是真正的限制,因為WebClasses只能使用在POST方法的表單。

您可以使用cookie,就像在一般ASP應用程式中使用Request.Cookies及Response.Cookies集合。就像使用URLData屬性一樣,傳給cookie的資料數也有限制。比較遭的情況是使用者可能因為某些安全上的考量而關閉cookie,或是瀏覽器不支援cookie (這情況很少)。此外,當您使用許多存放大量資訊的cookies時,會降低效率,所以您最好只使用cookies來儲存稍後要從資料庫讀出的資料ID。

另一個在網頁儲存狀態資訊的方法是使用隱藏控制項,這會在WebClass產生網頁被初始,並且當網頁資料傳回時再透過Request.Form集合變數來讀取。使用這個方法的問題是它只能用在具有表單的網頁,而且像這種隱藏欄位的值會出現在網頁原始檔中。假使欄位的值可以被看到對您是嚴重的問題,您必須先對它編碼。

測試與部署
 

要測試一個WebClass應用程式不會比測試任一個ASP元件困難多少,因為您可以使用Visual Basic環境所提供偵錯工具所帶來的好處。WebClass有些額外的特性在作語法偵錯時很有用。

Trace會傳送一個字串到OutputDebugString Windows API函數中。有一些類似DBMON的工具可以解譯出這個字串。當WebClass已經編譯過後,在偵錯器中使用Trace方法會特別有用, 因為此時已經沒有辦法可以顯示出訊息。請記得WebClass應用程式是使用 執行時無使用者介面 的選項,因此您不可以使用MsgBox陳述式在螢幕顯示訊息。然而,您可以使用App物件的方法將訊息寫入到紀錄檔,或是寫入到Windows NT事件日誌中。

當WebClass應用程式發生致命性的錯誤時(此時程式無法繼續),程式會接收到FatalErrorResponse事件。您可以使用Response.Write方法將自訂的訊息傳送到客戶端瀏覽器中反應這個事件,在那之後,您應該將SendDefault引數設為False來阻止WebClass送出標準的錯誤訊息。

Private Sub WebClass_FatalErrorResponse(SendDefault As Boolean)
    Response.Write "A fatal error has occurred.<P>"
    Response.Write "If the problem persists, please send an e-mail"
    Response.Write "message to the Web administrator."
    SendDefault = False
End Sub

在FatalErrorResponse事件中,您可以查詢WebClass的Error物件,它會透過Number、Source及Description屬性傳回詳細資料。在FatalErrorResponse事件外,這個物件會傳回Nothing。所有無可挽回的錯誤都會對應到一個由WebClass程式庫所提供的wcrErrxxxx列舉常數,例如wcrErrCannotReadHtml或是wcrErrSystemError。

致命性的錯誤會自動登錄到Windows NT事件日誌中,但您可以將HKEY_LOCAL。_MACHINE\SOFTWARE\Microsoft\Visual Basic\6.0\WebClass登錄機碼中LogErrors的值由1設為0來關閉這項功能。在Windows 95與Windows 98中,日誌檔是建立在Windows目錄中。

最後,在直譯(interpreted)與編譯(compiled)過的WebClass兩種元件間有一些不同的特性,您必須注意到;

  • 在編譯過的版本,只有系統DSN可以作用,其它種類的DSN只有在直譯版本中作用。
     
  • MDB資料庫只能在直譯版本使用,而編譯版本則不行。Visual Basic中的readmevb.html檔案中有提到這個問題,事實上,當筆者嚐試在編譯過的WebClass使用MDB檔案時,遭遇過各種不同的問題。但是筆者不能確定是不是所有編譯過的WebClass都無法使用MDB資料庫。
     
  • 當您在編譯過的WebClass中存取SQL Server資料庫時,您會發現無法正確地登錄,除非您授予對應WebClass元件身份的使用者登入存取權限。您可以在存放WebClass應用程式目錄的屬性頁對話方塊的安全頁籤中設定這個身份。
     
  • 在直譯的程式中可以在Application變數中儲存對WebClass物件的引用,但這個動作在編譯過的應用程式中會發生錯誤。
     
  • 您可以使用 封裝暨部署精靈 來部署WebClass應用程式。但請記住這個精靈不會自動識別在應用程式中所有的ASP,HTM及圖片檔案,所以您必須手動的增加到散佈檔案的列表中。
     

要讓編譯過的WebClass元件能在IIS下正確地執行,您必須散佈特定的WebClass執行時期檔案,它包含在Mswcrun.dll檔案中。

我們一路經過了Internet Information Server,ASP應用程式以及WebClass元件,來到終點。這一章是關於Internet程式技術的最後一章。DHTML應用程式與WebClass元件兩者撰寫的方法都與傳統程式寫作不同,但所換回的是您仍可使用最喜歡的語言來撰寫強大Internet或intranet應用程式的能力。Internet程式設計也是本書最後探討的主題,但是藉由Windows API函數,我們還有許多有趣方法可以提昇Visual Basic的能力,筆者在附錄中將專門探討這些進階的技術。