Introduction
ASP.NET MVC has a very useful method of calling HTML Helpers known as the "using pattern." This pattern allows the developer to effectively build reusable HTML components that are easily programmed and called.
In this article, we will discuss a way to adapt this pattern:
<% using(Html.BeginForm(url)) %>
<% { %>
Username: <input type='text' name='name' id='name'>
<br>
Password: <input type='password' name='password' id='password'>
<br />
<input type="submit" value="Submit">
<% } %>
as this:
<% With Form(url) %>
Username: <input type='text' name='name' id='name'>
<br>
Password: <input type='password' name='password' id='password'>
<br />
<input type="submit" value="Submit">
<% End With %>
which will enable us to eventually structure our code so we can code "at the level of intent" whenever possible, as shown:
With NavBar
With NavItems
put NavItem("Pure", "<a href="http://purecss.io/">http://purecss.io</a>")
put NavItem("YUI Library", "<a href="http://yuilibrary.com/">http://yuilibrary.com</a>")
End With
End With
The included demos show how easy it can be to generate complex layouts using these simple methods, as shown in these screenshots: (the previous excerpt is taken directly from the blog layout demo)
Background
After my previous article (A Simplified Parameterized Query Class in Classic ASP), I wanted to take a brief detour and discuss a capability that I always envied in ASP.NET MVC. With the "using pattern", HTML helpers can be written to encapsulate HTML in a way that is semantically meaningful yet allows the caller to flexibly add arbitrary HTML within the generated tags. In the example above, this allows the caller to create a form tag with arbitrary attributes that is automatically closed when the block has completed execution.
This type of capability is actually very easy to implement in VBScript for use in Classic ASP. First, we will discuss the naive approach that requires extensive duplication. Second, we will consider a simpler and more flexible method that eliminates this duplication, and then we will extend that out even farther.
In this article, I intentionally provide a variety of methods to accomplish the same goal. In some cases, I write HTML directly to the output stream, while in others I use one helper function or another to accomplish the same goals. The point is to demonstrate the flexibility inherent in this method.
The Basic Concept
We will take advantage of the following:
- The VBScript
With
statement creates a new execution scope (new stack frame). - When the
With
statement is complete, any temporary local variables are destroyed. - VBScript classes have destructors that are called when the class is destroyed.
Taking the form example above, the basic steps are:
- Create a class for the HTML element, we want to generate.
- Create a method that writes the opening tag.
- Create a class destructor that writes the closing tag.
The resulting class looks like this:
Class Form_Class
Public Sub Open(attribs)
response.write "<form " & attribs & ">"
End Sub
Private Sub Class_Terminate
response.write "</form>"
End Sub
End Class
As you can see, the class has a method called Open()
that accepts a string
containing arbitrary tag attributes and writes out the created tag, and a destructor that writes out the closing tag. However, using such a class inline would be cumbersome and no better (and probably less elegant) than writing the HTML itself. Therefore, in order to make this easier to use, we should create a matching helper method that manages the class for us:
Function Form(url)
dim T : set T = new Form_Class
T.Open "method='POST' action='" & url & "'"
set Form = T
End Function
This method instantiates the class, calls the Open()
method with a predefined string
of attributes and the passed URL, and returns the resulting instantiated object. This is what allows the With
statement to function properly. The calling code then looks like this:
<% With Form("someurl.asp") %>
... any HTML/ASP code here ...
<% End With %>
When the With
statement is encountered the system creates a new scope based on the passed object. In this case, the passed object is an instance of Form_Class
with its Open()
method already called. When the With
statement goes out of scope, the Form_Class
instance is destroyed and its destructor is called, which outputs the closing tag.
And the generated result is exactly as expected:
<form method=
... any HTML/ASP code here ...
</form>
It should be pointed out of course that the actual HTML output is not nicely formatted as shown in these examples. This article is intended to demonstrate the concept, not provide an off-the-shelf solution.
We can generate classes for any HTML we desire. However, this becomes cumbersome and error-prone as there is significant duplication of code, as seen in this example that creates simple table elements:
Class Table_Class
Public Sub Open
response.write "<table>"
End Sub
Private Sub Class_Terminate
response.write "</table>"
End Sub
End Class
Class Table_Row_Class
Public Sub Open
response.write "<tr>"
End Sub
Private Sub Class_Terminate
response.write "</tr>"
End Sub
End Class
Class Table_Header_Class
Public Sub Open
response.write "<th>"
End Sub
Private Sub Class_Terminate
response.write "</th>"
End Sub
End Class
Class Table_Cell_Class
Public Sub Open
response.write "<td>"
End Sub
Private Sub Class_Terminate
response.write "</td>"
End Sub
End Class
Function table()
set table = new Table_Class
table.Open
End Function
Function tr()
set tr = new Table_Row_Class
tr.Open
End Function
Function td()
set td = new Table_Cell_Class
td.Open
End Function
Function th()
set th = new Table_Header_Class
th.Open
End Function
As you can see, this will become very problematic going forward. Fortunately, there is a much more elegant solution that eliminates much of this redundancy.
The Better Approach
By creating a generic class to manage any HTML tag, we can simplify the problem to one class and a small set of helper functions. In the process, we can create a class that can either create and return a tag or write the tag to the output stream. This gives us even more flexibility.
We class itself is quite simple. It is initialized via an Init()
method that takes two parameters: the name of the tag to be produced, and a string
containing the attributes that will be written into the opening tag. The WriteToStream
property changes the mode of the class from string
generation to string
output. The SelfClosing
property is used to determine how to close the tag. The Choice()
method is simply an inline if function with a meaningful name.
Class HTML_Tag_Class
Private m_name
Private m_attribs
Private m_self_closing
Private m_write_to_stream
Public Sub Init(name, attribs)
m_name = name
m_attribs = attribs
End Sub
Public Property Let WriteToStream(bool)
m_write_to_stream = bool
End Property
Public Property Get OpenTag
dim s : s = "<" & m_name & Choice(Len(m_attribs) > 0, " " & m_attribs, "")
s = s & Choice(m_self_closing, "/>", ">")
OpenTag = s
End Property
Public Property Get CloseTag
CloseTag = Choice(m_self_closing, "", "</" & m_name & ">")
End Property
Public Property Let SelfClosing(bool)
m_self_closing = bool
End Property
Public Sub Open
If m_write_to_stream then response.write OpenTag & vbCR
End Sub
Public Sub Close
If m_write_to_stream then response.write CloseTag & vbCR
End Sub
Public Default Property Get ToString
ToString = OpenTag & CloseTag
End Property
Private Sub Class_Initialize
m_self_closing = false
m_write_to_stream = true
End Sub
Private Sub Class_Terminate
If m_write_to_stream then Close
End Sub
End Class
To actually manage the class, we consider three helper functions: two that return an object representing an inline HTML string
, and a third that returns an object that automatically generates and outputs an HTML container string. The latter will be used in the With
statements.
Function HTMLTag(name, attribs)
dim T : set T = new HTML_Tag_Class
T.Init name, attribs
T.WriteToStream = false
set HTMLTag = T
End Function
Function SelfClosingHTMLTag(name, attribs)
dim T : set T = new HTML_Tag_Class
T.Init name, attribs
T.WriteToStream = false
T.SelfClosing = true
set SelfClosingHTMLTag = T
End Function
Function HTMLContainer(name, attribs)
dim T : set T = new HTML_Tag_Class
T.Init name, attribs
T.WriteToStream = true
T.Open
set HTMLContainer= T
End Function
With these functions in place, we can now define methods for any tags we desire. For example:
Function div(class_name)
set div = HTMLContainer("div", "class='" & class_name & "'")
End Function
Function table(class_name)
set table = HTMLContainer("table", "cellpadding='0' cellspacing='0' border='0'
class='" & class_name & "'")
End Function
Function thead
set thead = HTMLContainer("thead", empty)
End Function
Function tbody
set tbody = HTMLContainer("tbody", empty)
End Function
Function tr
set tr = HTMLContainer("tr", empty)
End Function
Function th
set th = HTMLContainer("th", empty)
End Function
Function td
set td = HTMLContainer("td", empty)
End Function
Function link(text, url)
dim T : set T = HTMLTag("a", "href='" & url & "'")
link = T.OpenTag & text & T.CloseTag
End Function
These functions can then be called as such:
With div("first")
With table("pure-table")
With thead
With tr
With th
put "Head 1"
End With
With th
put "Head 2"
End With
End With
End With
With tbody
With tr
With td
put link("Google", "<a href="http://www.google.com/">http://www.google.com</a>")
End With
With td
put link("Microsoft", "<a href="http://www.microsoft.com/">http://www.microsoft.com</a>")
End With
End With
End With
End With
End With
While it can be annoying reading the With
statements, it is still more elegant than many other approaches used to generate HTML from code, especially if we are thoughtful enough to add a brief comment for clarification as shown.
Another example taken from the demo:
Sub StackedFormDemo
With StackedForm("", "post")
With Fieldset
With ControlGroup
put Label("Username", "name")
put TextBox("name")
End With
With ControlGroup
put Label("Password", "password")
put PasswordBox("password")
End With
With ControlGroup
put Label("Email Address", "email")
put TextBox("email")
End With
With FormControls
put Checkbox("cb")
put Label("I've read the terms and conditions", "cb")
put SubmitButton("Sign in")
End With
End With
End With
End Sub
Here, the function StackedForm()
creates a form with the Pure CSS framework classes necessary to create a form that stacks visually, and the other functions generate the corresponding framework HTML. There is a corresponding AlignedForm()
method in the demo as well.
And of course, you can create these functions in any way you like, with any parameters you like. The possibilities are really only limited by your imagination -- and willingness to code the solution.
Taking It Farther
But still, the above is not that much better than outputting HTML directly. So we can take this a step farther towards development of truly meaningful components. There are two main things we can do: create components that provide meaningful names to simple HTML fragments, and create components that output complex HTML.
Non-Complex Components
CSS frameworks typically have grids that use nested div
tags. The class names used are often optimized for ease of typing but this sacrifices some meaning. We would prefer something that is a bit more meaningful to us as developers, but still outputs framework-compliant code.
Consider the following class, using the Pure CSS framework: (chosen because it was easy enough to use in this demo, but you can easily imagine this capability for Bootstrap, etc.)
Class Pure_CSS_Layout_Class
Public Function Layout()
set Layout = HTMLContainer("div", "id='layout'")
End Function
Public Function Main()
set Main = HTMLContainer("div", "id='main'")
End Function
Public Function Header()
set Header = HTMLContainer("div", "class='header'")
End Function
Public Function Content()
set Content = HTMLContainer("div", "class='content'")
End Function
Public Function Footer()
set Content = HTMLContainer("div", "class='footer'")
End Function
Public Function Row()
set Row = HTMLContainer("div", "class='pure-g'")
End Function
Public Function Col(size)
set Col = HTMLContainer("div", "class='pure-u-" & size & "'")
End Function
End Class
This class encapsulates the basic layout capabilities in the Pure framework and provides access using terminology that is much friendlier for our purposes. To make this even easier, let's use a trick from the Tolerable
library:
dim Pure_CSS_Layout_Class__Singleton
Function Pure()
If IsEmpty(Pure_CSS_Layout_Class__Singleton) then
set Pure_CSS_Layout_Class__Singleton = new Pure_CSS_Layout_Class
End If
set Pure = Pure_CSS_Layout_Class__Singleton
End Function
When we include the file lib.Pure.asp, this gives us a global object named Pure
that can never be accidentally overwritten. (You could overwrite the singleton behind it, but then you are really trying to shoot yourself in the foot!)
Now we can construct our CSS grid as follows: (the Pure.Col("1-3")
bits create a column one third across the grid).
<% With Pure.Layout %>
<% With Pure.Main %>
<% With Pure.Header %>
<h1>Hello World</h1>
<% End With %>
<% With Pure.Content %>
<% With Pure.Row %>
<% With Pure.Col("1-3") %>
<p>Arbitrary HTML</p>
<% End With %>
<% With Pure.Col("1-3") %>
<p>Arbitrary HTML</p>
<% End With %>
<% With Pure.Col("1-3") %>
<p>Arbitrary HTML</p>
<% End With %>
<% End With %>
<% End With %>
<% End With %>
<% End With %>
One can easily imagine creating a Grid_Class
that provides Grid.Row()
and Grid.Col()
methods, and translates any parameters into the correct div
class syntax for a given CSS framework. This would then enable developers to swap out CSS grids with minimal/no changes to the actual ASP code.
More Complex Components
More complex HTML can be generated by having the class output raw HTML or a combination of raw HTML and generated HTML. The following is an example of a component we've all used at one time or another: the Tile
. This is simply a box containing two sections, a header and body.
Class HTML_Tile_Class
Private m_title
Public Sub Init(title)
m_title = title
End Sub
Public Sub Open
response.write "<div class='tile'>"
response.write "<div class='tile-hdr'>"
With div("tile-title")
response.write m_title
End With
response.write "</div>"
response.write "<div class='tile-body'>"
End Sub
Private Sub Class_Terminate
response.write "</div>"
response.write "</div>"
End Sub
End Class
Function Tile(title)
dim T : set T = new HTML_Tile_Class
T.Init title
T.Open
set Tile = T
End Function
The Tile
is generated as follows:
<% With Tile("This is a tile") %>
<p>This is some arbitrary tile content</p>
<% End With %>
And the output is the more complex component:
<div class=
<div class=
<div class=
<p>This is some arbitrary tile content</p>
</div>
</div>
The possibilities are truly endless. In fact, in the attached code there is a demo that recreates the Blog
layout demo found on the Pure framework's website, using the included HTML_Tag_Class
, the helper methods, and a custom class called Blog_Layout_Class
. The following is an excerpt from this class and shows how we can begin to code at the level of intent using these methods. When this method is called, the blog sidebar is generated.
Public Sub Sidebar
With div("sidebar pure-u-1 pure-u-med-1-4")
With Header
With HeaderGroup
put "<h1 class='brand-title'>A Sample Blog</h1>"
put "<h2 class='brand-tagline'>Creating a blog layout using Pure</h2>"
End With
With NavBar
With NavItems
put NavItem("Pure", "<a href="http://purecss.io/">http://purecss.io</a>")
put NavItem("YUI Library", "<a href="http://yuilibrary.com/">http://yuilibrary.com</a>")
End With
End With
End With
End With
End Sub
Summary
As we can see, it is definitely possible to emulate the "using pattern" found in MVC directly in "old-school" Classic ASP. There is really nothing preventing us from constructing our code in this manner, other than many years of online tutorials teaching poor coding practices. By stepping back and re-evaluating the framework and language, and by adopting better coding practices, we can unleash a significant amount of power in this supposed obsolete language.
My next article will focus on an optimized data structure designed to allow any method to accept optional parameters. This actually unleashes a significant amount of expressive power in our old friend VBScript. In fact, it opens up possibilities ranging from powerful HTML generation to dynamic data repositories and more. See you next time.