這是一個全部用 100% VBScript 寫的 MVC 框架。可以將其視為經典 ASP 的 Spring Boot。
Sane 是一個功能相對齊全的 MVC 框架,它為經典 ASP 帶來了理智。它在風格上與 .NET MVC 和 Rails 有一些相似之處,但並不完全類似於其中任何一種。它固執己見,因為它假設控制器將存在於特定的資料夾位置,但該位置在某種程度上是可配置的。
此框架的主要特點包括:
All/Any
布林測試、 Min/Max/Sum
、用於投影的Map/Select
以及用於過濾器的Where
,支援基本的 lambda 樣式表達式Up
和Down
步進遷移的資料庫遷移—版本控制資料庫更改Scripting.Dictionary
對象KVArray
及其幫助器方法來使建立 HTML 變得容易所有這些都是用 VBScript 編寫的。真的。
Sane 根據 GPLv3 條款獲得許可。
注意:該框架是從現實世界的內部工作流程路由專案中提取的,因此它有一些粗糙的邊緣。
這可以快速概述一個控制器的程式碼流及其使用的模型和視圖。它顯示了上面列出的功能有多少實際上是一起使用的。
“類似的還有很多,但這一個是我的。”
主要是因為這是一個有趣的項目,突破了經典 ASP 的極限。絕大多數開發人員討厭 VBScript 和 Classic ASP,其中大部分都有充分的理由。困擾經典 ASP 的許多問題都源自於其開發時期(即 20 世紀 90 年代中期)的限制。開發人員無法使用當今被認為是基本實踐(廣泛使用類別等),因為該語言的設計目的不是以我們所謂的「快速」方式執行,並且使用這些實踐會導致應用程式陷入困境並崩潰。因此,ASP 社群被迫像使用 PHP 一樣使用 ASP —— 作為一個內聯的基於頁面的模板處理器,而不是一個完整的應用程式框架。另外,說實話,Microsoft 向每個人推銷 ASP,無論其技能水平如何,而且網路上找到的大多數教程都很糟糕,並鼓勵了可怕的不良實踐。
今天我們知道得更清楚了,由於摩爾定律,運算能力自 90 年代中期以來已經增長了大約 500 倍,因此我們有能力做幾年前不可想像的事情。
該框架是從以這種方式構建的真實項目中提取的。它運行得很好,並且沒有理由(從功能上來說)它不應該作為一個可行的應用程式框架。也就是說,實際上,如果我們今天需要開發一個應用程序,我們將使用現代框架,例如 .NET MVC 或其競爭對手之一,因此這實際上只是為了對其他人有所幫助。而且建造起來很有趣。 :)
依賴性:此演示是針對 Microsoft Northwind 範例資料庫建構的。在此下載 SQL Server BAK 檔案並將其還原到 SQL Server 執行個體中。 SQL 腳本和 MDF 檔案也可用。
File -> Open Web Site...
並選擇Demo
目錄AppDALlib.DAL.asp
並修改連接字串以指向您的資料庫。/index.asp
檔案index.asp
會自動將您重新導向到 Home 控制器/App/Controllers/HomeController.asp
並載入預設操作Index
。
下面的一些功能有對應的 ASPUnit 測試。在 Tests 目錄中找到它們。
<%
Class OrdersController
Public Model
Public Sub Show
MVC.RequirePost
dim id : id = Request ( " Id " )
set Model = new Show_ViewModel_Class
set Model.Order = OrderRepository.FindById(id)
%> <!-- #include file="../../Views/Orders/Index.asp" --> <%
End Sub
End Class
MVC.Dispatch
%>
控制器只知道網域,而不知道資料庫。喜悅!
操作是沒有參數的函數(與 Rails 類似,但與 .NET MVC 不同)—與傳統 ASP 中一樣,參數是從Request
物件中提取的。 MVC.RequirePost
允許您將操作限制為僅回應POST
請求,否則會出錯。
MVC.Dispatch
是框架的神奇入口點。由於視圖被#include
到控制器中,並且應用程式可以有1..n
控制器,每個控制器有1..n
操作,因此除了幾個簡單的控制器之外,擁有一個中央整體MVC 調度程序是不可行的。這是因為 ASP 為每個頁面視圖載入並編譯整個#include
d 檔案。有鑑於此,框架將實例化委託給控制器本身,讓它們負責啟動框架,而不是讓框架負責載入和實例化所有控制器以及載入和編譯每個請求的所有視圖。該框架確實根據命名約定動態實例化控制器,但每個請求僅解析和載入一個控制器,而不是所有控制器。透過僅加載一個控制器,我們可以使用節省的資金來加載許多有用的庫,從而使開發對開發人員更加友好。
由於這種方法,「路由引擎」實際上只是一個知道如何建立控制器檔案 URL 的類,因此Routes.UrlTo("Orders", "Show", Array("Id", order.Id))
產生URL /App/Controllers/OrdersController.asp?_A=Show&Id=123
(用於 order.Id = 123)。 URL 指向控制器並透過_A
參數提供要執行的操作的名稱。操作參數透過KVArray
資料結構傳遞,該結構只是在整個框架中廣泛使用的鍵/值對數組。例如,以下是在眾多 HTML 幫助器之一中使用的兩個KVArray
:
<%= HTML.LinkToExt( " View Orders " , _
" Orders " , _
" List " , _
array ( " param1 " , " value1 " , " param2 " , " value2 " ), _
array ( " class " , " btn btn-primary " , " id " , " orders-button " )) %>
該方法在幕後建立一個錨點,該錨點路由到正確的控制器/操作組合,透過查詢字串傳遞指定的參數,並具有指定的 HTML class
和id
屬性。由於有一些輔助方法(例如KeyVal
和KVUnzip
KVArray
很容易處理。
KVArray
資料結構是大部分框架的基礎,並且大大簡化了編碼。從根本上來說, KVArray
只不過是一個標準的 VBScript 數組,應該始終以兩個一組的方式使用。換句話說,要建立一個KVArray
我們只需要建立一個數組,其中元素 0 是第一個鍵,元素 1 是其值,元素 2 是第二個鍵,元素 3 是其值,依此類推。
本質上,您可以將KVArray
想像為使用System.Object
樣式呼叫的一種方式,就像 .NET 的Html.ActionLink
中所做的那樣。
例如:
dim kvarray : kvarray = Array( 6 )
'Element 1: Name = Bob
kvarray( 0 ) = "Name"
kvarray( 1 ) = "Bob"
'Element 2: Age = 35
kvarray( 2 ) = "Age"
kvarray( 3 ) = 35
'Element 3: FavoriteColor = Blue
kvarray( 4 ) = "FavoriteColor"
kvarray( 5 ) = "Blue"
但實際上你永遠不會這樣寫,而是使用內聯Array
建構函數,如下:
dim params : params = Array( "Name" , "Bob" , "Age" , 35 , "FavoriteColor" , "Blue" )
或者為了提高可讀性:
dim params : params = Array( _
"Name" , "Bob" , _
"Age" , 35 , _
"FavoriteColor" , "Blue" _
)
若要以 2 為單位迭代此陣列並使用KeyVal
取得目前鍵和值:
dim idx, the_key, the_val
For idx = 0 to UBound(kvarray) step 2
KeyVal kvarray, idx, the_key, the_val
Next
在每次迭代中, the_key
將包含目前鍵(例如「Name」、「Age」或「FavoriteColor」),而the_val
將包含該鍵對應的值。
但為什麼不使用字典呢?
字典很棒,但它們是 COM 元件,並且至少在歷史上實例化的成本很高,而且因為執行緒不應該放置在會話中。對於該框架中的用例來說,它們的使用也很麻煩,並且沒有簡單的方法可以使用動態數量的參數內聯實例化它們。
我們真正需要的是一個快速、只進的鍵值資料結構,它允許我們迭代值並提取每個鍵和值來建立類似具有任意屬性的 HTML 標記或具有任意列的 SQL where
子句,而不是快速尋找各個鍵。因此,我們需要數組和字典的混合體,以滿足我們的特定需求並允許內聯聲明任意數量的參數。 KVArray
讓我們可以非常自然地編寫程式碼,如上面的LinkToExt
範例,或使用Routes.UrlTo()
手動建立 URL:
<%
< a href = " <%= Routes.UrlTo( " Users " , " Edit " , array( " Id " , user.Id)) %> " >
< i class = " glyphicon glyphicon-user " >< /a >
< /a >
%>
我們也可以建立通用儲存庫Find
方法,可以像這樣使用:
set expensive_products_starting_with_C = ProductRepository.Find( _
array( "name like ?" , "C%" , _
"price > ?" , expensive_price _
) _
)
set cheap_products_ending_with_Z = ProductRepository.Find( _
array( "name like ?" , "%Z" , _
"price < ?" , cheap_price _
) _
)
在示範儲存庫中有這樣的範例,其中KVUnzip
也可以非常有效地使用來幫助輕鬆建立 sql where
子句。以下的範例來自ProductRepository.Find()
方法,接受包含謂詞鍵值對的KVArray
並將其解壓縮為兩個用於建立查詢的單獨陣列:
If Not IsEmpty(where_kvarray) then
sql = sql & " WHERE "
dim where_keys, where_values
KVUnzip where_kvarray, where_keys, where_values
dim i
For i = 0 to UBound(where_keys)
If i > 0 then sql = sql & " AND "
sql = sql & " " & where_keys(i) & " "
Next
End If
...
dim rs : set rs = DAL.Query(sql, where_values)
set Find = ProductList(rs)
<%
Class OrderModel_Class
Public Validator
Public OrderNumber, DateOrdered, CustomerName, LineItems
Public Property Get SaleTotal
SaleTotal = Enumerable(LineItems).Sum( " item_.Subtotal " ) ' whaaaa?
End Property
Public Sub Class_Initialize
ValidatePattern Me, OrderNumber, " ^d{9}[d|X]$ " , " Order number format is incorrect. "
ValidateExists Me, DateOrdered, " DateOrdered cannot be blank. "
ValidateExists Me, CustomerName, " Customer name cannot be blank. "
End Sub
End Class
Class OrderLineItemModel_Class
Public ProductName, Price, Quantity, Subtotal
End Class
%>
透過從模型的Class_Initialize
建構函數中呼叫適當的Validate*
輔助方法來驗證模型:
Private Sub Class_Initialize
ValidateExists Me , "Name" , "Name must exist."
ValidateMaxLength Me , "Name" , 10 , "Name cannot be more than 10 characters long."
ValidateMinLength Me , "Name" , 2 , "Name cannot be less than 2 characters long."
ValidateNumeric Me , "Quantity" , "Quantity must be numeric."
ValidatePattern Me , "Email" , "[w-]+@([w-]+.)+[w-]+" , "E-mail format is invalid."
End Sub
目前僅包括ValidateExists
、 ValidateMinLength
、 ValidateMaxLength
、 ValidateNumeric
和ValidatePattern
。這些輔助方法實際上所做的是建立對應驗證類別的新實例並將其附加到模型的Validator
屬性。例如,當模型使用ValidateExists Me, "Name", "Name must exist."
以下是幕後實際發生的事情:
Sub ValidateExists(instance, field_name, message)
if not IsObject(instance.Validator) then set instance.Validator = new Validator_Class
instance.Validator.AddValidation new ExistsValidation_Class.Initialize(instance, field_name, message)
End Sub
這裡Me
是域模型實例。然後使用Validator_Class
(透過YourModel.Validator
)驗證所有已註冊的驗證規則,如果發現錯誤則設定Errors
和HasErrors
欄位。這與觀察者模式類似。我們通過Me
原因是因為這使我們能夠為每個具有強語義含義的驗證提供一個措辭方便的方法,例如ValidateExists
。這需要一些密碼術,但這是值得的。
新增新的驗證很容易,只需新增一個新的驗證類別和輔助Sub
。例如,若要新增要求字串以字母「A」開頭的驗證,您將建立StartsWithLetterAValidation_Class
和輔助方法Sub ValidateStartsWithA(instance, field_name, message)
,然後透過ValidateStartsWithA Me, "MyField", "Field must start with A."
可以透過 Automapper 樣式的轉換將 ADO Recordset 轉換為域模型的連結列表來建立域模型。說啥?
Class OrderRepository_Class
Public Function GetAll()
dim sql : sql = "select OrderNumber, DateOrdered, CustomerName from Orders"
dim rs : set rs = DAL.Query(sql, empty) 'optional second parameter, can be scalar or array of binds
dim list : set list = new LinkedList_Class
Do until rs.EOF
list.Push Automapper.AutoMap(rs, new OrderModel_Class) ' keanuwhoa.jpg
rs.MoveNext
Loop
set GetAll = list
Destroy rs ' no passing around recordsets, no open connections to deal with
End Function
End Class
' Convenience wrapper lazy-loads the repository
dim OrderRepository__Singleton
Function OrderRepository()
If IsEmpty(OrderRepository__Singleton) then
set OrderRepository__Singleton = new OrderRepository_Class
End If
set OrderRepository = OrderRepository__Singleton
End Function
使用empty
關鍵字是該框架採用的常見方法。 VBScript 的一個常見抱怨是它不允許可選參數。雖然這在技術上是正確的,但很容易解決,但實際上在線找到的每個範例都涉及傳遞空字串、空值或類似的方法。使用內建的 VBScript 關鍵字empty
是處理可選參數的一種語義上有意義的方式,清楚地表明我們專門打算忽略可選參數。在本例中, DAL.Query
方法接受兩個參數:SQL 查詢和包含綁定值的可選第二個參數。第二個參數可以是DAL.Query("select a from b where a = ?", "foo")
中的單一值,也可以是綁定數組,例如DAL.Query("select a from b where a = ? and c = ?", Array("foo", "bar")
。在上面的範例中,它被明確忽略,因為 SQL 中沒有綁定變數。
在這個範例中, DAL
變數只是lib.Data.asp
中的Database_Class
的一個實例。在原始專案中,DAL 是一個自訂類,可作為一組延遲載入的Database_Class
實例的入口點,允許在工作流程期間在資料庫之間共用和移動資料。
Automapper
物件是一個 VBScript 類,它嘗試將來源物件中的每個欄位對應到目標物件中的對應欄位。來源物件可以是記錄集或自訂類別。該函數可以映射到新的或現有的物件。 Automapper
物件包含三個方法: AutoMap
嘗試映射所有屬性; FlexMap
允許您選擇要對應的屬性子集,例如Automapper.FlexMap(rs, new OrderModel_Class, array("DateOrdered", "CustomerName"))
只會將兩個指定欄位從來源記錄集複製到新模型實例;和DynMap
,它允許您動態地重新映射值,有關人為的示例,請參見:
Automapper.DynMap(rs, new OrderModel_Class, _
array( "target.CustomerName = UCase(src.CustomerName)" , _
"target.LikedOrder = src.CustomerWasHappy" ))
因為來源和目標都可以是具有實例方法的任何對象,所以這是管理 CRUD 方法中的模型綁定的非常有用的方法,例如:
Public Sub CreatePost
dim new_product_model : set new_product_model = Automapper.AutoMap(Request.Form, new ProductModel_Class)
... etc
End Sub
因為它是#include
到控制器操作中的,所以視圖可以完全存取控制器的Model
實例。這裡它存取視圖模型的Order
屬性並迭代LineItems
屬性(這將是儲存庫內建構的LinkedList_Class
實例)以建立視圖。使用視圖模型,您可以建立不依賴特定記錄集結構的豐富視圖。請參閱示範中的HomeController
以取得範例視圖模型,該視圖模型包含四個單獨的網域物件列表,用於建立儀表板摘要視圖。
MVC.RequireModel
方法提供強型別視圖的功能,模仿 .NET MVC 中的@model
指令。
<% MVC.RequireModel Model, " Show_ViewModel_Class " %>
< h2 >Order Summary</ h2 >
< div class = " row " >
< div class = " col-md-2 " >
Order # <%= Model.Order.OrderNumber %>
</ div >
< div class = " col-md-10 " >
Ordered on <%= Model.Order.DateOrdered %>
by <%= Model.Order.CustomerName %>
for <%= FormatCurrency (Model.Order.SaleTotal) %>
</ div >
</ div >
< table class = " table " >
< thead >
< tr >
< th >Product</ th >
< th >Price</ th >
< th >Qty</ th >
< th >Subtotal</ th >
</ tr >
<% dim it : set it = Model.Order.LineItems.Iterator %>
<% dim item %>
<% While it.HasNext %>
<% set item = it.GetNext() %>
< tr >
< td > <%= item .ProductName %> </ td >
< td > <%= item .Price %> </ td >
< td > <%= item .Quantity %> </ td >
< td > <%= item .Subtotal %> </ td >
</ tr >
<% Wend %>
</ thead >
</ table >
在列表上提供可連結的 lambda 樣式呼叫。從單元測試來看:
Enumerable(list) _
.Where( "len(item_) > 5" ) _
.Map( "set V_ = new ChainedExample_Class : V_.Data = item_ : V_.Length = len(item_)" ) _
.Max( "item_.Length" )
V_
是Map
方法使用的特殊實例變量,用來表示「lambda」表達式的結果。 item_
是另一個特殊的實例變量,表示目前正在處理的項目。因此在這種情況下, Map
會迭代列表中的每個項目並執行傳遞的「lambda」表達式。 Map
的結果是EnumerableHelper_Class
的新實例,其中包含由表達式建構的ChainedExample_Class
實例的清單。然後Max
處理該枚舉以傳回單一值,即最大長度。
包裝連接詳細資訊和對資料庫的存取。除了已經顯示的範例之外,它還可以處理:
DAL.Execute "delete from Orders where OrderId = ?", id
set rs = DAL.PagedQuery(sql, params, per_page, page_num)
DAL.BeginTransaction
、 DAL.CommitTransaction
和DAL.RollbackTransaction
該類別還透過Class_Terminate
方法自動關閉並銷毀包裝的連接,該方法在類別準備好銷毀時調用。
Class Migration_01_Create_Orders_Table
Public Migration
Public Sub Up
Migration.Do "create table Orders " & _
"(OrderNumber varchar(10) not null, DateOrdered datetime, CustomerName varchar(50))"
End Sub
Public Sub Down
Migration.Do "drop table Orders"
End Sub
End Class
Migrations.Add "Migration_01_Create_Orders_Table"
可以透過位於migrate.asp
的 Web 介面逐步進行遷移。 Migration.Do
執行 SQL 指令。遷移按照載入的順序處理。建議遵循如上所示的結構化命名方案,以便於訂購。有一些特殊命令,例如Migration.Irreversible
,可讓您停止向下遷移的進行等。
從中提取框架的實際專案包含大約 3 打遷移,因此它非常適合在開發過程中對資料庫進行版本控制。
注意:遷移 Web 介面非常基本且不美觀
相依性:要使用遷移功能,您必須先使用腳本meta_migrations
! Create Migrations Table.sql
](Sane/Framework/Data/Migrations/!建立遷移表.sql)。
由於在經典 ASP 中並不總是可以使用逐步偵錯,因此這使得偵錯和追蹤變得更加容易。
以有意義的方式Dump
輸出物件:
dim a : a = GetSomeArray()
Dump a
輸出:
[Array:
0 => «elt1»
1 => «elt2»
2 => «elt3»
]
它甚至使用Class_Get_Properties
欄位處理自訂類別:
Dump Product
輸出:
{ProductModel_Class:
Id : Long => «17»,
Name : String => «Alice Mutton»,
CategoryId : Long => «6»,
Category : Empty => «»,
CategoryName : String => «Meat/Poultry»,
SupplierId : Long => «7»,
Supplier : Empty => «»,
SupplierName : String => «Pavlova, Ltd.»,
UnitPrice : Currency => «250»,
UnitsInStock : Integer => «23»,
UnitsOnOrder : Integer => «0»,
ReorderLevel : Integer => «0»,
Discontinued : Boolean => «True»
}
它處理嵌套,正如在演示中的OrdersController.asp
的Show
操作中放置對Dump Model
呼叫時所看到的那樣:
{OrderModel_Class:
Id : Long => « 11074 » ,
CustomerId : String => « SIMOB » ,
OrderDate : Date => « 5 / 6 / 1998 » ,
RequiredDate : Date => « 6 / 3 / 1998 » ,
ShippedDate : Null => «» ,
ShipName : String => « Simons bistro » ,
ShipAddress : String => « Vinbæltet 34 » ,
ShipCity : String => « Kobenhavn » ,
ShipCountry : String => « Denmark » ,
LineItems : LinkedList_Class =>
[ List:
1 =>
{OrderLineItemModel_Class:
ProductId : Long => « 16 » ,
ProductName : String => « Pavlova » ,
UnitPrice : Currency => « 17.45 » ,
Quantity : Integer => « 14 » ,
Discount : Single => « 0.05 » ,
ExtendedPrice : Currency => « 232.09 »
}
] }
quit
立即停止執行。 die "some message"
停止執行並向螢幕輸出「some message」。 trace "text"
和comment "text"
都寫入包含「文字」的 HTML 註釋,這對於在不破壞佈局的情況下追蹤幕後很有用。
Flash.Success = "Product updated."
、 Flash.Errors = model.Validator.Errors
等
如果在創建模型時遇到錯誤,我們應該能夠重新顯示表單, FormCache
用戶的內容仍然填寫。
例如,在Create
操作中我們可以:
Public Sub Create
dim form_params : set form_params = FormCache.DeserializeForm( "NewProduct" )
If Not form_params Is Nothing then
set Model = Automapper.AutoMap(form_params, new Create_ViewModel_Class)
Else
set Model = new Create_ViewModel_Class
End If
% > <!--#include file= "../../Views/Products/Create.asp" --> < %
End Sub
在CreatePost
:
Public Sub CreatePost
dim new_product_model : set new_product_model = Automapper.AutoMap(Request.Form, new ProductModel_Class)
new_product_model.Validator.Validate
If new_product_model.Validator.HasErrors then
FormCache.SerializeForm "NewProduct" , Request.Form
Flash.Errors = new_product_model.Validator.Errors
MVC.RedirectToAction "Create"
Else
ProductRepository.AddNew new_product_model
FormCache.ClearForm "NewProduct"
Flash.Success = "Product added."
MVC.RedirectToAction "Index"
End If
End Sub
put
包裝Response.Write
並根據傳遞的類型改變其輸出,並為列表和陣列提供特殊輸出。H(string)
HTML 對字串進行編碼Assign(target, src)
抽象化了物件使用set
的需要Choice(condition, trueval, falseval)
是一個功能更強大的iif
HTML.FormTag(controller_name, action_name, route_attribs, form_attribs)
HTML.TextBox(id, value)
HTML.TextArea(id, value, rows, cols)
HTML.DropDownList(id, selected_value, list, option_value_field, option_text_field)
*Ext
變體Edit
操作中: HTMLSecurity.SetAntiCSRFToken "ProductEditForm"
<%= HTML.Hidden("nonce", HTMLSecurity.GetAntiCSRFToken("ProductEditForm")) %>
EditPost
操作: HTMLSecurity.OnInvalidAntiCsrfTokenRedirectToActionExt "ProductEditForm", Request.Form("nonce"), "Edit", Array("Id", Request.Form("Id"))
MVC.ControllerName
、 MVC.ActionName
*Ext
變體的MVC.RedirectTo(controller_name, action_name)
或MVC.RedirectToActionPOST(action_name)
該框架提供了工具來幫助緩解 OWASP Top 10 中的以下三個問題:
Database_Class
支援參數化查詢。H()
方法來對所有其他輸出進行簡單編碼。HtmlSecurity
幫助程式提供按表單和按站點隨機數檢查來減輕此威脅。其餘七個漏洞大部分或全部由開發人員和/或管理員負責。
整個框架中使用的一個習慣用法是 VBScript 不允許方法重載的解決方法。一般來說有兩種情況,一種是方法功能齊全,有多個參數,另一種是方法簽章被簡化。這是透過讓全功能方法將Ext
附加到末尾以將其表示為簡化方法的「擴展」版本來處理的。
例如,這是來自HTML_Helper_Class
:
Public Function LinkTo(link_text, controller_name, action_name)
LinkTo = LinkToExt(link_text, controller_name, action_name, empty, empty)
End Function
Public Function LinkToExt(link_text, controller_name, action_name, params_array, attribs_array)
LinkToExt = "<a href='" & Encode(Routes.UrlTo(controller_name, action_name, params_array)) & "'" & _
HtmlAttribs(attribs_array) & ">" & link_text & "</a>" & vbCR
End Function
這是來自MVC_Dispatcher_Class
:
Public Sub RedirectTo(controller_name, action_name)
RedirectToExt controller_name, action_name, empty
End Sub
' Redirects the browser to the specified action on the specified controller with the specified querystring parameters.
' params is a KVArray of querystring parameters.
Public Sub RedirectToExt(controller_name, action_name, params)
Response.Redirect Routes.UrlTo(controller_name, action_name, params)
End Sub
Proc
和Func
類別在 VBScript 中實作了 lambda(旨在直接解決這個人的批評),但從未發表過。布萊恩(Brian)突破了界限。 LinkedList_Class
及其迭代器改編自他的工作,並為其量身定制了極其強大的 lambda 功能,以避免過度拖累 ASP。該框架也採用了他的一些編碼約定,例如_Class
後綴和延遲載入的全域範圍單例函數的使用。