经典 ASP 和 MVC
为 Classic ASP 实现类似 MVC 的功能。
将文章的代码解压到 IIS 服务器上的新虚拟目录 test_mvc(https:///test_mvc/)中。请确保已为虚拟目录启用了 ASP 脚本。请确保 default.asp 是此虚拟目录的默认网页。
一个应用示例 在这里
引言
如今还有多少公司仍在广泛使用 ASP,为什么?通常的情况是,一家公司核心业务有一个巨大的 ASP 系统,并且只有少数几个开发人员从系统早期就了解它。系统通常过于复杂,内部代码风格“意大利面条化”(由于 ASP 的特性)。我认为新开发人员几乎没有机会参与代码的维护或开发。原因是新开发人员的入门门槛太高。
- 市场鼓励开发人员学习新东西并忘记旧东西。
- 庞大的意大利面条式代码使得研究代码的努力变得毫无意义。
这些 ASP 系统之所以存在,是因为它们有效。现有开发人员可能想改变一些东西,但对他们来说门槛也很高。
- 现有应用的维护工作量大
- 需要学习新语言(如 VB.NET 或 C#)和技术(如 MVC)
对于 ASP 开发人员来说,什么更容易:用 C#+MVC 编写新代码,还是继续使用 ASP?
Microsoft 已停止对 ASP 的支持,目前创新很少。有些人使用 JavaScript,有些人使用 jQuery,XML,但仅此而已。服务器端方面的创新很少,可以使程序员的生活更轻松。
这项工作的目的是将 MVC 概念引入 ASP。提供一种以类似 MVC 的风格编写/重写 ASP 应用的方式。解开意大利面条式的代码。让新来者识别常见的代码模式,并更容易地接手现有 ASP 应用的维护。
背景
市面上已有 MVC。让我们将 MVC 模式嵌入到 Classic ASP 中。
路由
MVC 的核心是路由。路由(Router)选择控制器(Controller)和操作(Action)(主要)。
路由是决定调用哪个控制器和操作的代码。
让我们通过比较 MVC 和经典脚本来研究路由。
IIS 内部进程定位文件夹和文件并执行它。
URL 是如何处理的。 | |
使用 Classic ASP、WebForms、PHP 等。 | 使用 MVC 模式 |
![]() |
![]() |
IIS 内部进程定位文件夹和文件并执行它。 | IIS 将执行交给路由。路由定位类和函数并执行它。 |
稍后将讨论如何使用 MVC 模式创建漂亮的 URL。
URL 示例(**控制器**已高亮显示)
- https:///Home/Index
- https:///Home/About
- https:///User/Edit/1
控制器 (Controller)
这个控制器是什么?为什么你应该关心在你的代码中有控制器?我可以告诉你,这是 MVC 概念的重要组成部分,但这些话毫无意义。
让我们从实践出发。虽然很少见,但假设你有一个文档需要开发某个系统的一部分。
你打算将这段逻辑的代码存储在哪里?作为 ASP 开发人员,你可能会创建一个页面集来从数据库中提取和显示一些数据,以及几个页面来接收用户输入的表单以更新数据库。当系统不太大时,这是可以的。当用户以简单的方式与系统交互时,这也是可以的。
这个系统怎么样?
你将不得不创建某种“架构”来避免在系统完成后完全迷失在代码中。当我说“架构”时,我指的是处理相似情况的一些重复代码模式。无论如何,在开发结束时,系统将是 HTML、SQL、访问数据库的代码、处理用户输入和显示数据的混乱集合。顺便说一句,我认为图中的系统非常简单。如果它大 10-20 倍怎么办?
解决问题的方法是通过重复的标准代码结构进行代码封装,并对组件命名达成特殊约定。所以这里有一个特殊组件的位置——一个与用户交互并包含业务逻辑的控制器。
控制器是一个类。它主要负责处理与一个业务实体相关的用户交互的所有方面。应用程序中应该有多个控制器。控制器**必须**以 Controller 结尾:HomeController
、PublicationController
、StatusController
、UserController
。控制器**不应该**包含用户界面代码或 HTML 标记。最好避免在控制器中使用 SQL。控制器应处理用户输入,查询和更新对象模型,并准备要显示的数据。所有控制器都应驻留在 /Controllers 文件夹中。
控制器处理用户输入,执行操作,并控制**将显示什么**(而不是**如何**显示)。
控制器应具有组成控制器逻辑的**方法**。如下所示:
Class PublicationController
Dim Model
Public Sub List ()
...
End Sub
Publis Sub Edit (vars)
...
End Sub
Publis Sub Delete(vars)
...
End Sub
...
End Class
这些控制器的方法是**操作**。路由应该能够调用它们。
这些是带有**操作**高亮显示的 URL 示例。
- https:///Home/Index
- https:///Home/About
- https:///User/Edit/1
路由会拆解 URL 并调用控制器中的一个控制器和一个操作。
ASP 没有标准的路由。它将是 default.asp 来作为路由。
- https:///test_mvc/default.aspx?controller=Home&action=Index
- https:///test_mvc/default.aspx?controller=Home&action=About
- https:///test_mvc/default.aspx?controller=User&action=Edit&id=2
或者它们的简短形式(如果您已将 default.asp 选为虚拟目录的默认页面)
- https:///test_mvc/?controller=User&action=List
VBScript 中没有反射,我们也不知道是否存在控制器类及其操作方法。而且我们也不需要。
路由只是使用 VBScript 的 Eval 指令来调用控制器操作方法。
所以这两个是等价的
Set controllerInstance = New HomeController
controller = "Home"
Set controllerInstance = Eval("New "+ controller + "Controller")
但后一个更灵活,可以根据控制器名称用于路由 HTTP 请求。操作使用相同的技术进行选择。
路由的代码:{default.asp 的文本}
<!--#include file="utils/utils.inc" -->
<!--#include file="models/models.inc" -->
<!--#include file="controllers/controllers.inc" -->
<%
Const defaultController = "Home"
Const defaultAction = "Index"
If not Route () then
result = RouteDebug ()
End If
Function ContentPlaceHolder()
If not Route () then
result = RouteDebug ()
End If
End Function
Function Route ()
Dim controller, action , vars
controller = Request.QueryString("controller")
action = Request.QueryString("action")
set vars = CollectVariables()
Route = False
If IsEmpty(controller) or IsNull(controller) then
controller = defaultController
End If
If IsEmpty(action) or IsNull(action) then
action = defaultAction
End If
Dim controllerName
controllerName = controller + "Controller"
Dim controllerInstance
Set controllerInstance = Eval ( " new " + controllerName)
Dim actionCallString
If (Instr(1,action,"Post",1)>0) then
actionCallString = " controllerInstance." + action + "(Request.Form)"
ElseIf Not (IsNothing(vars)) then
actionCallString = " controllerInstance." + action + "(vars)"
Else
actionCallString = " controllerInstance." + action + "()"
End If
Eval (actionCallString)
Route = true
End Function
Function RouteDebug ()
Dim controller, action , vars
controller = Request.QueryString("controller")
action = Request.QueryString("action")
Response.Write(controller)
Response.Write(action)
dim key, keyValue
for each key in Request.Querystring
keyValue = Request.Querystring(key)
'ignore service keys
if InStr(1,"controller, action, partial",key,1)=0 Then
Response.Write( key + " = " + keyValue )
End If
next
End Function
Function CollectVariables
dim key, keyValue
Set results = Server.CreateObject("Scripting.Dictionary")
for each key in Request.Querystring
keyValue = Request.Querystring(key)
'ignore service keys
if InStr(1,"controller, action, partial",key,1)=0 Then
results.Add key,keyValue
End If
next
if results.Count=0 Then
Set CollectVariables = Nothing
else
Set CollectVariables = results
End If
End Function
%>
Actions
因此,控制器被实例化并调用其操作之一。接下来,操作执行业务逻辑。控制器中可以包含任何类型的业务逻辑。您可以查询或更新数据库,发送电子邮件,处理文件或用户输入,甚至发射火箭到火星。通常有更新/插入/删除/列出记录到/从数据库的逻辑,但实际上可以是任何东西。控制器中**不应**包含 HTML 标记。
在操作结束时,如果您想显示**某些内容**(除了视图的静态内容),则需要初始化变量 Model 并将视图包含在操作的最后一行。
请看下面的示例:如果用户单击链接 https:///test_mvc/?controller=User&action=List ,路由将选择UserController 和List 操作。List 操作准备用户列表并将该列表分配给变量Model。变量 Model 在视图 ../views/User/List.asp 中使用。此视图在 List 操作的末尾附加。
{ /Controller/UserController.asp 的文本 }
<%
class UserController
Dim Model
private sub Class_Initialize()
end sub
private sub Class_Terminate()
end sub
public Sub List()
Dim u
set u = new UserHelper
set Model = u.SelectAll
%> <!--#include file="../views/User/List.asp" --> <%
End Sub
public Sub Create()
set Model = new User
%> <!--#include file="../views/User/Create.asp" --> <%
End Sub
public Sub CreatePost(args)
Dim obj, objh
set objh = new UserHelper
set obj = new User
obj.FirstName = args("FirstName")
obj.LastName = args("LastName")
obj.UserName = args("UserName")
obj.ProjectID = args("ProjectID")
'form values should be cleaned from injections
'checkboxes shoud use the syntax: obj.ProjectID = (args("ProjectID") = "on")
obj.Id = objh.Insert(obj)
Response.Redirect("?controller=User&action=list")
End Sub
public Sub Edit(vars)
Dim u
set u = new UserHelper
set Model = u.SelectById(vars("id"))
%> <!--#include file="../views/User/Edit.asp" --> <%
End Sub
public Sub EditPost(args)
Dim obj, objh
set objh = new UserHelper
set obj = objh.SelectById(args("id"))
obj.FirstName = args("FirstName")
obj.LastName = args("LastName")
obj.UserName = args("UserName")
obj.ProjectID = args("ProjectID")
'form values should be cleaned from injections
'checkboxes shoud use the syntax: obj.ProjectID = (args("ProjectID") = "on")
objh.Update(obj)
Response.Redirect("?controller=User&action=list")
End Sub
public Sub Delete(vars)
Dim u
set u = new UserHelper
set Model = u.SelectById(vars("id"))
%> <!--#include file="../views/User/Delete.asp" --> <%
End Sub
public Sub DeletePost(args)
Dim res, objh
set objh = new UserHelper
res = objh.Delete(args("id"))
if res then
Response.Redirect("?controller=User&action=list")
else
Response.Redirect("?controller=User&action=Delete&id=" + CStr(args("id")))
end if
End Sub
public Sub Details(vars)
Dim u
set u = new UserHelper
set Model = u.SelectById(vars("id"))
%> <!--#include file="../views/User/Details.asp" --> <%
End Sub
End Class
%>
注意:与 .NET MVC 不同,在 .NET MVC 中,视图是自动选择的,我们需要直接指向所需的视图。视图的名称应与操作的名称相同。它们位于与控制器同名的文件夹中。例如:/Views/Home/Index.asp、/Views/User/Edit.asp。
视图
视图提供外观。只有视图内部应该包含 HTML 标记。视图中**不应该**有任何逻辑,或者逻辑应该极少。视图的职责是接收Model并以最佳方式将其显示给用户。
{ /Views/User/List.aspx 的文本 }
List Users
<%=Html.ActionLink("Create new User", "User", "Create" , "") %> <br/>
<table>
<tr>
<td>FirstName</td>
<td>LastName</td>
<td>UserName</td>
<td>ProjectID</td>
<td></td>
</tr>
<%
if IsNothing(Model) then
%> <tr><td colspan="4">No records</td> </tr><%
Else
Dim obj
For each obj in Model.Items
%>
<tr>
<td><%=Html.Encode(obj.FirstName) %></td>
<td><%=Html.Encode(obj.LastName) %></td>
<td><%=Html.Encode(obj.UserName) %></td>
<td><%=Html.Encode(obj.ProjectID) %></td>
<td>
<%=Html.ActionLink("Edit", "User",
"Edit" , "id=" + CStr(obj.Id)) %> |
<%=Html.ActionLink("Delete", "User",
"Delete" , "id=" + CStr(obj.Id)) %> |
<%=Html.ActionLink("Details", "User",
"Details" , "id=" + CStr(obj.Id)) %>
</td>
</tr>
<%
Next
End If
%>
</table>
那么为什么这些视图如此令人惊叹?一切都在于顺序和分离。将视图与代码分开设计。表达您的想法。轻松更改外观,就像 1-2-3 一样。拥有全屏或移动视图(或两者兼有),而无需触及业务逻辑代码。在处理控制器/操作时,专注于业务逻辑和数据。将您的系统扩展到几十个控制器和数百个视图。享受透明度和轻松的代码导航。永远摆脱意大利面条式的代码。
Data
ASP 项目的数据通常存储在数据库中。作为 ASP 程序员,您可能有一些数据访问实用程序,并将它们与 SQL 和 HTML 混合在您的 ASP 页面中。
在 MVC 项目中,他们使用 Model 这个术语来指代数据。
模型
当我们谈论 MVC 中的 Model 术语时,存在更广泛和更狭义的含义。
Model 的更广泛含义。
典型的应用程序拥有一组用于操作数据库的业务对象。这组类构成了 Model 或 Data Model 的更广泛含义。业务对象通常存储在类中,并使用Active Record 模式。
{类图示例}
当需要从数据库获取数据时,会从表中将一行数据提取到对象中。
- 创建相应类的对象。
- 从数据库中提取数据并分配给对象的每个属性。
- 准备好的对象被传递给业务逻辑。
{用于获取数据和初始化 User 对象的代码片段}
Class User
private mId
private mFirstName
...
public property get Id()
Id = mId
end property
public property let Id(val)
mId = val
end property
public property get FirstName()
FirstName = mFirstName
end property
public property let FirstName(val)
mFirstName = val
end property
...
End Class 'User
class UserHelper
...
public function SelectAll()
objCommand.CommandText = "Select * from [User]"
set records = objCommand.Execute
if records.eof then
Set SelectAll = Nothing
else
Dim results, obj, record
Set results = Server.CreateObject("Scripting.Dictionary")
while not records.eof
set obj = PopulateObjectFromRecord(records)
results.Add obj.Id, obj
records.movenext
wend
set SelectAll = results
records.Close
End If
end function
private function PopulateObjectFromRecord(record)
if record.eof then
Set PopulateObjectFromRecord = Nothing
else
Dim obj
set obj = new User
obj.Id = record("Id")
obj.FirstName = record("FirstName")
obj.LastName = record("LastName")
obj.UserName = record("UserName")
obj.ProjectID = record("ProjectID")
set PopulateObjectFromRecord = obj
end if
end function
end class 'UserHelper
当我们需要将对象的数据保存到数据库时,会执行反向过程:使用对象的属性及其 ID 执行 SQL Update 查询。
如果需要处理多个记录,则将对象加入到 List 或 Dictionary 中。
使用代码生成工具为您的项目生成具有基本数据库操作的类骨架非常容易。
这种方法的优点是将数据模型与 HTML(外观)和业务逻辑(控制器/视图)分开。重用数据访问代码。在单个位置进行一致的 SQL 代码。
Model 的更狭义含义。
当控制器/操作执行业务逻辑(读/写数据库、处理用户输入等)时,它们可能需要在相应的视图中显示某些内容。因此,控制器会准备 Model 以将其传递给视图。这是 Model 的更狭义含义。狭义 Model 术语的表示是一个与 Model 同名的变量,但对于每个特定的操作-视图对,它可能持有特定类型的值。
例如:如果我们想显示用户列表,则调用控制器:UserController
和操作:List
。
{路由调用操作}
Set controllerInstance = Eval ( "new UserController")
Eval ( "controllerInstance.List() ")
控制器/操作调用数据模型
class UserController
Dim Model
...
public Sub List()
Dim u
set u = new UserHelper
set Model = u.SelectAll
%> <!--#include file="../views/User/List.asp" --> <%
End Sub
然后从“User”表中获取记录,从数据库中提取。创建 Class: User
的对象并存储在 List 或 Dictionary 中。List 或 Dictionary 被分配给变量 Model。
变量 Model 可以从 VIEW 的代码中访问。
因此,当操作包含视图时,它会显示 Model 作为用户对象列表。
{ 视图 /Views/User/List.asp 的代码 }
List Users
<table>
<tr>
<td>FirstName</td>
<td>LastName</td>
<td>UserName</td>
<td>ProjectID</td>
</tr>
<%
if IsNothing(Model) then
%> <tr><td colspan="4">No records</td> </tr><%
Else
Dim obj
For each obj in Model.Items
%>
<tr>
<td><%=Html.Encode(obj.FirstName) %></td>
<td><%=Html.Encode(obj.LastName) %></td>
<td><%=Html.Encode(obj.UserName) %></td>
<td><%=Html.Encode(obj.ProjectID) %></td>
</tr>
<%
Next
End If
%>
</table>
主控页面
(代码位于高级示例中)
在 Classic ASP 中,使用 #include 指令来创建将在多个页面上重用的页眉和页脚。
在 .NET 中有一个主控页面的概念。主控页面提供
- 统一的视图:没有页眉和页脚文件,只有一个页面,设计并看到结果。
- 客户端资源包含的单一入口点:CSS、JavaScript 等。
- 易于切换
我们在这里将主控页面包含在 default.asp
中。
%> <!--#include file="views/shared/Site.htmltemplate" --> <%
主控页面渲染页眉和部分内容,然后调用 Route() 以包含动态内容。控制器/操作渲染模型后,代码流程会返回到主控页面。然后主控页面渲染剩余的通用内容。
这是最有趣的部分:为了切换外观,只需创建一个新的主控页面,其中包含新的 CSS、JavaScript、菜单等,然后只编辑一个地方:default.asp 中的 #include 引用。
局部视图
有一种方法可以跳过渲染主控页面。如果 URL 包含变量“partial”,在调用 default.asp 时,主控页面不会被包含,而是直接调用 Route()。
https:///test_mvc/?controller=PublicationPost&action=List&partial
这在测试或通过 AJAX 更新页面时可能很有用。
服务器端主明细。
主明细从两端进行处理:从主表单应该有一个对明细的调用和一个显示结果的地方。从明细表单应该能够获取并显示连贯的明细数据。
显示给定 PublicationID 的帖子列表的明细表单。
https:///test_mvc/?controller=PublicationPost&action=ListByPublicationID&PublicationID=1&partial
这只是帖子列表,但按 PublicationID 过滤。
显示出版物的“主”表单在其视图中有一个调用来显示明细(帖子列表)。
<%=Html.RenderControllerAction("PublicationPost","ListByPublicationID", PrepareVariables ( Array("PublicationID="+CStr(Model.Id)))) %>
RenderControllerAction
的工作方式与路由非常相似。它可以从另一个控制器/操作中调用和渲染一个控制器/操作。
表单处理:用户输入处理
与 WebForms 不同,ASP.NET MVC 应用程序以与 Classic ASP 相同的方式将数据发送回服务器。它只是将 POST 或 GET 表单发送到目标 URL。
我们也将遵循这个方法。
有一个控制器/操作 UserController.Edit,它向用户显示编辑表单:https:///test_mvc/?controller=User&action=Edit&id=16
class UserController Dim Model ... public Sub Edit(vars) Dim u set u = new UserHelper set Model = u.SelectById(vars("id")) %> <!--#include file="../views/User/Edit.asp" --> <% End Sub ... End Class
包含视图后,这将生成 HTML。
<form action="?controller=User&action=EditPost" id="EditPost" method="post"> <input id='id' name='id' type='hidden' value='1' /> <input id='FirstName' name='FirstName' type='text' value='Bhaskara' /> <input id='LastName' name='LastName' type='text' value='Ramachandra' /> <input id='UserName' name='UserName' type='text' value='BRamachandra' /> <input id='ProjectID' name='ProjectID' type='text' value='1' /> <button type="submit">Submit</button> </form>
当用户单击提交按钮时,表单会将 POST 请求发送到路由,以处理 Controller/Action UserController.EditPost。
路由识别 Post 请求,收集变量,并调用负责处理表单的控制器/操作。操作会更新数据模型。
class UserController ... public Sub EditPost(args) Dim obj, objh set objh = new UserHelper set obj = objh.SelectById(args("id")) obj.FirstName = args("FirstName") obj.LastName = args("LastName") obj.UserName = args("UserName") obj.ProjectID = args("ProjectID") objh.Update(obj) Response.Redirect("?controller=User&action=list") End Sub ... End Class
操作完成后,可以将客户端重定向到结果页面。
因此,我们使用的 Classic ASP 和 MVC 模式之间的唯一区别在于入口点。Classic ASP 通常有一个接收 Post 请求的页面,而 MVC 模式有路由和控制器/操作。
为了本出版物的目的,我在控制器中放置了操作对来更新模型。
UserController.Edit + UserController.EditPost UserController.Create + UserController.CreatePost UserController.Delete + UserController.DeletePost
自然,不是吗?
漂亮的 URL
应用程序只有一个入口点——default.asp 页面。因此,这些 URL 可以正常工作:
- https:///?controller=Home&action=Index
- https:///default.aspx?controller=Home&action=Index
是否可以看起来像这样:https:///Home/Index?
虽然 MVC 模式相对较新,但 Classic ASP 已经非常老了。IIS 中没有真正的内置 ASP 路由。因此,我们需要调用页面(default.asp)。但是,有一些技巧可以创建漂亮的 URL。
- URL 重写
- 使用类似 https:///?home/Index 或 https:///?/home/Index 的 URL,并对路由代码进行一些小的更改。
- 使用 404 处理程序(改编自simplicity framework)
使用 404 处理程序创建漂亮的 URL。

要尝试 IIS 7 中的“漂亮的 URL”,请按照以下步骤操作:
- 在 IIS 管理器中,选择您的 Web 应用程序。
- 选择“错误页面”功能。
- “功能设置”应设置为“自定义错误页面”。
- 404 错误应设置为您的入口页面。类型应为“执行 URL”,“本地”。
使用这些设置,当您的 Web 应用程序中的 URL 指向不存在的页面或文件夹时(MVC URL 实际上是不存在的页面和文件夹),IIS 将调用您的页面。
您的页面(现在作为 404 处理程序运行)可以解析和解释 URL。这种“漂亮的 URL”调整可能需要对路由进行一些额外的*工作,因为原始 GET 参数将发生更改。
MVC 模式下的安全性
路由文件 default.asp 可能是您 IIS 文件夹中唯一的一个 ASP 文件。如果您更改应用程序的位置并更新 default.aps 中模块的链接,则整个代码可以放在 IIS 文件夹结构之外。
Classic ASP 和 MVC 的开源和专有框架
有许多开源 Classic ASP 框架和专有框架实现了 MVC。
历史
在撰写本文时,我发现 Classic ASP 中的 JavaScript 确实是一个非常有趣的选择。JavaScript 在 Classic ASP 中一直存在,比 node.jes 或 Rhino 早得多。它不像后两者那样现代,但如果您有过 IE 的经验,您就知道如何修复 Classic ASP JavaScript 中缺失或不起作用的问题。更多信息,请阅读我的下一篇文章。