(Optional: Download CreateDatabase.zip)
Unzip the code of the article into a new Virtual Directory test_mvc (http://localhost/test_mvc/) on your IIS server. Make sure you have enabled ASP scripts for the virtual directory. Make sure the default.asp is a default web page for this Virtual Directory.
A sample of application is here
Introduction
How many companies are still extensively using ASP this days and why? Typically the case is a company with huge ASP system in the core and few developers who know the system from it’s childhood. The system is usually too complex, with spaghetti-like style of code inside (because of a nature of ASP). I believe there is a little or no ways for new developers to be involved in support or development of the code. The reason is that the threshold for new developers is too high:
- market encourages developers to learn new stuff and forget old ones.
- huge spaghetti-like code makes the effort to study the code meaningless.
These ASP systems exist because they work. Existing developers may want to change something, but the the threshold for them is high too:
- high workload with existing app support
- necessity to study new languages (like VB.NET or C#) and technologies (like MVC)
What is easier for ASP developer: to write a new code with C#+MVC or continue with ASP?
Microsoft has dropped the support of ASP and there is a little of innovations this days. Some use JavaScript, some use jQuery, XML but that is it. There is very little of innovations for the server-side that may make the life of programmers easier.
The aim of this effort is to bring MVC concept to ASP. To give a way to write/re-write the ASP app in MVC-like style. Untangle spaghetti-like code. To let newcomers recognize the common code-patterns and pick up the support of existing ASP apps easier.
Background
There is a MVC out there. Let’s embed MVC patterns into the classic ASP.
Router
In the core of MVC there is a Router. The Router selects the Controller and Action (mostly).
Router is a code that decides which Controller and Action to call.
Let's study the Router with comparison MVC and classic scripting.
The IIS internal process locates the folder and file and executes it.
How URLs are processed. |
with classic ASP, WebForms, PHP etc. | with MVC pattern |
| |
The IIS internal process locates the folder and the file and executes it. | The IIS passes the execution to Router. Router locates the Class and Function and executes it. |
There will be a discussion later on how to make the nice URLs with MVC pattern.
Samples of URLs (controllers are highlighted)
- Http://localhost/Home/Index
- Http://localhost/Home/About
- Http://localhost/User/Edit/1
Controller
What is this Controller? Why should you care to have Controllers in your code? I may say that it is a significant part of the MVC concept, but these words mean nothing.
Let’s go from practice. It’s rare, but suppose you have a document to develop a part of some system:
Where are you going to store the code for this logic? As an ASP developer you would probably create a set of pages to pull and display some data from the database, and a couple of pages to receive Forms with user input to update the database back. It is OK when the system is not too large. It is OK when the User interacts with the system in a simple way.
How is about that system?
You will have to create some sort of "Architecture" in your code to do not get totally lost in the code when the system is finished. When I say "Architecture" I mean some repetitive patterns of the code to handle the similar situations. Anyways the system will be a mess of HTML, SQL, code to access the database, handle user input, and display data at the end of development. By the way, I believe that the system on the picture is VERY SIMPLE. What happened if it is 10-20 times bigger?
A remedy for the problem is the code encapsulation with the repetitive standard structures of the code and the special agreement regarding the naming of the components. So there is a place for a special component - a Controller, which interacts with the user and contains a sort of the business logic:
Controller is a class. It holds all the aspects of user interaction mostly regarding ONE type of business entity. There should be a number of controllers in the Application. Controllers must have names ending with Controller: HomeController
, PublicationController
, StatusController
, UserController
. Controller should not have a user interface code or HTML markup. It is a good practice to avoid the SQL in the controller too. Controller should handle user input, query and update the object model and prepare the data to be displayed. All Controllers should reside in the folder /Controllers.
Controller handles the user input , do the things and controls what is going to be displayed (but not how).
Controller should have methods that compose the logic of Controller. Like this:
Class PublicationController
Dim Model
Public Sub List ()
...
End Sub
Publis Sub Edit (vars)
...
End Sub
Publis Sub Delete(vars)
...
End Sub
...
End Class
These methods of Controller are Actions. Router should be able to call them.
These are samples of URLs with Actions are highlighted.
- Http://localhost/Home/Index
- Http://localhost/Home/About
- Http://localhost/User/Edit/1
Router disassembles the URL and calls one of the Controllers and one of the Actions within the Controller.
There is no standard Router for ASP. It will be a default.asp to work as a Router.
The URLs we are going to employ will be similar to:
- http://localhost/test_mvc/default.aspx?controller=Home&action=Index
- http://localhost/test_mvc/default.aspx?controller=Home&action=About
- http://localhost/test_mvc/default.aspx?controller=User&action=Edit&id=2
or their shorter form (if you have selected default.asp as a default page for the virtual directory):
- http://localhost/test_mvc/?controller=User&action=List
There is no Reflection in the VBScript and we don’t know whether there is a Controller class and it’s Action method out there. And we don’t need to.
Router simply calls the Action method of the Controller object with VBScript Eval instructions.
So the these two are equal:
Set controllerInstance = New HomeController
controller = "Home"
Set controllerInstance = Eval("New "+ controller + "Controller")
But the second one is more flexible and can be used to route the HTTP request based on the controller name. The Action is selected using the same technique.
The code of Router: {text of defaut.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)
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)
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
So the Controller is instantiated and one of it's Actions is called. Next, Action executes the business logic. There may be a business logic of any kind in the controller. You may query or update the database, send e-mails, process files or user input, or even launch a rocket to Mars. Typically there is a logic to update/insert/delete/list records from/to database, but it may be literally anything there. There should be NO HTML markup in the Controller.
At the end of Action, if you want to display soemthing (besides the static content of View) you need to initialize the variable Model and include the View as the last line of the Action.
Look at the example below: If the user hits the link http://localhost/test_mvc/?controller=User&action=List , Router selects the UserController and List action. The List action prepares the list of Users and assign this list to the variable Model. Variable Model is used in the View ../views/User/List.asp This view is attached at the end of action List.
{ text of /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
%>
Note: as opposed to NET MVC, where View is selected automatically, we need to directly point to the needed View. View should have the same name as Action. They reside in the folder with the same name as the Controller. For example: /Views/Home/Index.asp, /Views/User/Edit.asp.
Views
View provides appearance. It is only Views should have an HTML markup inside them. There should be NO or absolute minimum of the logic in a View. The business of a View is to take the Model and display it to the user in the best way.
{ text of /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>
So why are these Views so amazing? It is all in the Order and Separation. Design your Views separately from your code. Express Yourself. Change the appearance as easy as 1-2-3. Have a fullscreen or mobile view (or both) without necessity to touch the business logic code. Concentrate on the business logic and data when you work with Controller/Action. Scale your system up to dozens Controllers and hundreds of Views. Enjoy the transparency and easy code navigation. Get rid of spaghetti-like code forever.
Data
Data for ASP projects is usually stored in the database. As an ASP programmer you probably have some data-access utilities and mix them with SQL and HTML in your ASP pages.
They use a term Model to refer the data in MVC projects.
Model
There are broader and narrower meanings of Model term when we talk about MVC.
The broader meaning of the Model.
Usual application has a set of Business objects to work with the database. This set of classes composes a broader meaning of Model or Data Model. Business objects often are stored in Classes and use the Active Record pattern.
{a sample of class diagram}
When the data is needed from the database, one row is fetched from table into the object:
- The object of the corresponding class is created
- data is fetched from the database and assigned to every attribute of the object
- the ready object is passed to the business logic.
{code fragments to fetch the data and initialize the User object}
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
When we need to save the data of object into the database, the reverse process is performed: the SQL Update query is executed with attributes of the object and it's ID.
If there is a need to process several records, then the objects are joined into the List or Dictionary.
It is very easy to generate skeletons of classes for your project with the basic set of database operations.
The advantage of this approach is the ability to split the Data Model from the HTML (appearance) and from the business logic (Controller/View). Reuse the data-access code. Consistent SQL code in a single place.
The narrower meaning of Model.
When the Controller/Action perform the business logic (read/write the database, process user input etc) they may need to display something in the corresponding View. So the Controller prepares the Model to pass it to the View. This is a narrower meaning of Model. The representation of of the narrower Model term is a variable that has the same name Model, but may hold specific types of the value for every specific Action-View pair.
For example: If we need to display a list of users the controller: UserController
and action: List
are called.
{router calls the action}
Set controllerInstance = Eval ( "new UserController")
Eval ( "controllerInstance.List() ")
CONTROLLER/ACTION call the Data Model :
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
The the records from "User" table are fetched from the database. Objects of Class: User
are created and stored in the List or Dictionary. The List or Dictionary is assigned to the variable Model.
The variable Model is accessible from the code of the VIEW.
So when the ACTION includes the VIEW, it displays the Model as a list of User objects.
{ code of the view /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>
Masterpage
(code is located in advanced example)
In the classic ASP there is an #include directive is used to create headers, footers that will be reused on multiple pages.
There is a concept of masterpages in the .NET . The masterpage provides
- a solid view: no header and footer files, just one page, design and see what you get
- a single point of client resource inclusion: CSS, JavaScripts, etc.
- easy to switch over
Here we have a Masterpage is included in the default.asp
:
%> <!--#include file="views/shared/Site.htmltemplate" --> <%
Masterpage renders header and part of its content and calls the Route() to include dynamic content. After controller/action render a model, the code flow gets back to masterpage. Masterpage then renders the rest of the common content.
Here is the tastiest part: in order to switch over the appearance just create another masterpage with new CSS, JavaScripts, menus, etc. included, and then edit only one place: #include reference in the default.asp
Partial view
There is a way to skip rendering Masterpage. If the URL has a variable "partial" , when calling default.asp, the Masterpage is not included but the Route() is called directly.
http://localhost/test_mvc/?controller=PublicationPost&action=List&partial
It can be useful while testing or when updating the page via AJAX.
Server-side master-details.
Master-details are cooked from both sides: from masterform there should be a call to details and a place to display the result. From the details form there should be an ability to get and display a coherent details data.
The details form to display the list of Posts for a given PublicationID:
http://localhost/test_mvc/?controller=PublicationPost&action=ListByPublicationID&PublicationID=1&partial
It is just the same list of Posts, but filtered by PublicationID
The Master form to display the Publication has a call to display the details (a list of Posts) from its view:
<%=Html.RenderControllerAction("PublicationPost","ListByPublicationID", PrepareVariables ( Array("PublicationID="+CStr(Model.Id)))) %>
RenderControllerAction
works very much like the Router. It can call and render a controller/action from within another controller/action.
Form handling: User input handling
In contrast with WebForms, the ASP.NET MVC application sends data back to the server in the same way as it does the Classic ASP. It just sends POST or GET forms to the target URL.
We are going to follow this way too:
There is a Controller/Action UserController.Edit that displays Edit form to the user: http://localhost/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
After including a view this will produce the 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>
When user click the Submit button, the form sends the POST request to the Router for Controller/Action UserController.EditPost.
Router recognizes the Post request, collects the variables and calls the controller/action responsible for the form handling. The Action updates the Data Model.
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
Action may redirect the client to the result page after all.
So the only difference between Classic ASP and MVC pattern we are using is an entry point. When Classic ASP usually has a page to receive a Post request, MVC pattern has the Router and a Controller/Action.
For the purpose of this publication I've put pairs of actions within controllers to update the model:
UserController.Edit + UserController.EditPost
UserController.Create + UserController.CreatePost
UserController.Delete + UserController.DeletePost
Naturally, isn't it?
Nice URLs
There is only one entry point in the application - the default.asp page. So these URLs work fine:
- http://localhost/?controller=Home&action=Index
- http://localhost/default.aspx?controller=Home&action=Index
May it look nice like this?: http://localhost/Home/Index
While the MVC pattern is relatively new, the Classic ASP is really old. There is no real built-in ASP router in the IIS. Thus we need to call The Page (default.asp). However there are couple of tricks to make nice URLs:
- URL rewrite
- using URL's like http://localhost/?home/Index or http://localhost/?/home/Index with slight changes to the Router code.
- using 404 handler (borrowed from simplicity framework)
using 404 handler for nice URLs
In order to try the "Nice URLs" in the the IIS 7 follow the steps:
- In the IIS manager select your web application.
- Select the "Error pages" feature.
- "Feature settings" should be set to the "Custom error pages"
- The 404 error should be set to your entry page. Type should be "Execute URL", "local"
With these settings the IIS will call your page when the URL in your webapplication points to the non-existing page or folder (the MVC URLS really are the non-existing pages and folders).
Your page (which is working now as 404 handler) can parse and interprete the URL. This "Nice URL" adjustment may require some additional work in the Router, as the original GET parameters are going to be changed.
Security with MVC pattern
The router file default.asp could be the only ASP file in your IIS folder. If you change the location of the application and update the links to the modules in the default.aps, the entire code may be placed out of IIS folder structure.
Open source and proprietary frameworks for Classic ASP and MVC
There is a number of open source Classic ASP frameworks and proprietary ones that implement MVC:
History
While working on this article I have found the JavaScript in the Classic ASP is really very interesting option. The JavaScript has been in the Classic ASP since the beginning, a way longer than node.jes or Rhino. It is not as modern as these two, but if you had an experience with IE, you know how to fix the things that are missing or not working in the Classic ASP JavaScript. For more, please read my next article: