Click here to Skip to main content
Click here to Skip to main content
Go to top

Implementing the MVC "Using" Pattern for HTML Helpers in Classic ASP

, 31 Mar 2014
Rate this:
Please Sign up or sign in to vote.
Implementing the ASP.NET MVC HTML Helper "using pattern" in Classic ASP by using the With keyword, an HTML tag class and helper methods.

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 the following:

  1. The VBScript With statement creates a new execution scope (new stack frame).
  2. When the With statement is complete any temporary local variables are destroyed.
  3. VBScript classes have destructors that are called when the class is destroyed.

Taking the form example above, the basic steps are:

  1. Create a class for the HTML element we want to generate.
  2. Create a method that writes the opening tag.
  3. 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='POST' action='someurl.asp'>
  ... 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  '/tr
    End With  '/thead
    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  '/tr
    End With  '/tbody
  End With  '/table
End With  '/div

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='tile'>
  <div class='tile-hdr'>This is a tile</div>
  <div class='tile-body'>
    <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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

David Cantrell

United States United States
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web03 | 2.8.140916.1 | Last Updated 31 Mar 2014
Article Copyright 2014 by David Cantrell
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid