Migrating Classic ASP To ASP.NET Razor Web Pages Part Three- Include Files And Reusable Code

This is the third in a series of articles that explore how to migrate classic ASP web sites to the ASP.NET Web Pages framework. I covered Razor syntax and data access in the previous articles. This article shows how to deal with reusable code that typically inhabits include files in classic ASP.

The other articles in the series are

Reusable code can be more or less anything from site-wide functions, application constants or snippets of HTML and server-side mark-up that are used as part of a site's UI template, such as headers, menus and footers. I will start by looking at application constants, then I will cover how to manage common functions before reviewing the templating features offered by Razor.

Site Constants

In the last article, I illustrated how to store the connection string for the database in web.config, which is an XML-based configuration file. The web.config file is the ideal place to store other arbitrary application level constants. The connection string was stored in a node called connectionStrings which you had to create. The name is important because the ConfigurationManager class, the API for accessing web.config contents, looks for it when you reference the ConfigurationManager.ConnectionStrings property. Application level settings are stored in a node called appSettings, and they are accessed via the ConfigurationManager.AppSettings property. The following shows an email address being stored in the existing web.config file:

<?xml version="1.0" encoding="utf-8" ?>
        <add key="siteContact" value="[email protected]" />
        <add name="classified"
             connectionString="Provider=Microsoft.Jet.OleDb.4.0;Data Source=|DataDirectory|/CLassified_2000.mdb"
             providerName="System.Data.OleDb" />
        <compilation debug="true" targetFramework="4.0" />

Individual appSettings are stored as a key/value pair. If you want to reference the value in code, you do so like this:

@Imports System.Configuration
    Dim contact = ConfigurationManager.AppSettings("siteContact")
End Code

Notice the Imports statement at the top of the file - this saves you from having to use the fully qualified namespace to reference the ConfigurationManager class as discussed in my previous migration article.

Site Wide Procedures

Any experienced classic ASP developer soon accumulates a set of common custom utility procedures. These may be generic to all sites or they may only be relevant to the current project. Some of them might perform operations and return values (functions) while others might be responsible for tasks that don't return values like rendering snippets of HTML, e.g binding values to dropdown lists and rendering them (subs). There are a number of options available to cater for these.

The App_Code Folder

Sites built using the ASP.NET Web Pages framework are based on the Web Site project template - as opposed to the Web Application project template. Web Site projects can be deployed in exactly the same way as classic ASP sites - you simply copy the site files containing source code to a location that the web server can "see". The site is compiled when the first request is made. Web Application projects on the other hand need to be compiled prior to deployment. Web Site projects can include a special ASP.NET folder called App_Code. Any source code that you place in App_Code is compiled at runtime and is made accessible to any other code in the site. You can place classes and modules in here and they will act in the same way as if you have included them in every page of your classic ASP site. You can also place files containing special Razor constructs - Functions and Helpers in App_Code to achieve the same effect.

Razor Functions

When migrating from a scripting language like VBScript (or PHP for that matter) developers need as much help as they can get. The designers of the Razor engine felt that the change from being able to stick a procedure anywhere in a .asp file to having to declare procedures within classes or modules was one burden too many for developers making the transition from scripting to a strongly typed language. So they came up with a couple of tricks to apparently remove this obligation.

The first uses the Functions keyword which is preceded by the Razor @ symbol. It is terminated with the End Functions statement, and the enclosed code is a standard VB Shared function. The following example is one I use to determine whether a checkbox was checked when a containing form was posted and then return a value that a database bit field will accept:

    Public Shared Function OnAsBit(ByVal s As String) As Integer
        Return If(String.IsNullOrEmpty(s), 0, 1)
    End Function
End Functions

You can place this in a vbhtml file in App_Code and then call it in your code by prefixing it with the name of the file. For example, if you name the file MyFunctions, you would call the method as follows:

Dim result = MyFunctions.OnAsBit(Request("check"))

You could add the function to the page that contains the form processing logic. However, if you did that, the function would only be available to that page.

Razor Helpers

The second feature introduced by the ASP.NET Web Pages framework to help with code reuse is called a Helper. It is denoted by the Helpers keyword, prefixed again by the Razor @ sign. Helpers don't return values like Functions. However, they can include intermixed Razor syntax and HTML. Their role is to provide a way of dynamically generating snippets of HTML. They are kind of like classic ASP Subs. Here's a really simple example for illustration. It takes a collection of strings and outputs them to an ordered list:

@Helper OrderedList(items As IEnumerable(Of String))
    @For Each item In items
End Helper

Again, these should be placed in their own file in App_Code so that they will be available across the whole site. If you named the file that contains this helper Helpers.vbhtml, you would call it within your page in the following manner:

@Helpers.OrderedList(New String(){"Apple", "Banana", "Cherry", "Damson"})

Notice that the name of the file, which at runtime is compiled to the name of a class containing the method, is prefixed with an @ sign. This is because the helper is intended to render output to the browser.

Class Files in App_Code

Razor Functions are useful and serve a purpose in that they ease the scripting developer gently into an Object Oriented (class-based) development framework. However, they are a bit like stabilisers on a bicycle. Sooner or later, you have to consider the grown-up alternative. As I mentioned previously, all procedures and subs have to live in a class or a module (or structure) within VB.NET. When you create a Razor function, the Razor Engine takes the name of the file you put it in and uses that to generate a class to house your function. But there is nothing to stop you creating your own class file to shortcut the process.

Here's an example that features the same method from earlier:

Imports Microsoft.VisualBasic
Public Class FormExtensions
    Public Shared Function OnAsBit(ByVal s As String) As Integer
        Return If(String.IsNullOrEmpty(s), 0, 1)
    End Function
End Class

The file type is a VB Class file. It is available in WebMatrix by selecting All in the Choose A File Type dialog. I have named this one FormExtensions.vb. The resulting method is called as follows:

Dim result = FormExtensions.OnAsBit(Request("check"))

This is the way with Shared methods in VB. You do not need to create an instance of the class that owns the method in order to call it.

Extension methods provide an alternative approach which offers the benefit of reduced typing when calling the method. Extension methods are declared in Modules rather than classes and they are marked with the <Extension()> attribute. Here's the OnAsBit method defined as an extension method:

Imports Microsoft.VisualBasic
Imports System.Runtime.CompilerServices
Public Module StringExtensions
    Public Function OnAsBit(ByVal s As String) As Integer
        Return If(String.IsNullOrEmpty(s), 0, 1)
    End Function
End Module

Note that the Function is no longer 'Shared' and that System.Runtime.CompilerServices has been imported. Extension methods are called as if they are native methods of the type that they extend. In this case, the type is String (the type specified as the first parameter of the method):

Dim result = Request("check").OnAsBit()

Class Library

If your procedures are generic and likely to be used across multiple sites, you can create a class library. You won't be able to do this using WebMatrix, but Visual Studio Express offers the facility to generate class libraries. The advantage of creating class libraries is that every time you update them, all the applications that depend on them will be able to take advantage of any new features or bug fixes. The mechanics behind creating a class library and making it available to your site are exactly the same for both C# and VB. I have covered the basics in a previous article: Creating Reusable Components For ASP.NET Razor Web Pages.

Executing Code For All Pages

The original classic ASP site had one include file that was added to all pages. It contained code for setting the connection string and opening a connection, but it also contained code that established whether the current user was logged in (authenticated) or not. Now, there is a membership API that has been developed to work specifically with ASP.NET Web Pages that means you just need one line of code to check if the current user is authenticated. However, for the sake of comparison with the classic ASP site, I have chosen not to implement it in this migration. I have instead replaced the code in the include file with a line-for-line translation to ASP.NET. Here is the original classic ASP code followed by the .NET version:


<!-- METADATA TYPE="typelib" 
              FILE="C:\Program Files\Common Files\System\ado\msado15.dll" -->
  Dim objConn
  Set objConn = Server.CreateObject("ADODB.Connection")
  objConn.Open "Provider=Microsoft.Jet.OLEDB.4.0; " & _
               "Data Source= C:\Users\MIKE\Desktop\Ch15\classified_2000.mdb"
  If Session("blnValidUser") = True and Session("PersonID") = "" Then
    Dim rsPersonIDCheck
    Set rsPersonIDCheck = Server.CreateObject("ADODB.Recordset")
    Dim strSQL 
    strSQL = "SELECT PersonID FROM Person " & _ 
             "WHERE EMailAddress = '" & Session("EMailAddress") & "';"
    rsPersonIDCheck.Open strSQL, objConn
    If rsPersonIDCheck.EOF Then
      Session("blnValidUser") = False
      Session("PersonID") = rsPersonIDCheck("PersonID")
    End If
    Set rsPersonIDCheck = Nothing
  End If

    Dim objConn = Database.Open("classified")
    If Session("blnValidUser") And Session("PersonID") = 0 Then
        Dim strSQL = "SELECT PersonID FROM Person WHERE EMailAddress = @0"
        Dim rsPersonIDCheck = objConn.QueryValue(strSQL, Session("EmailAddress"))
        If rsPersonIDCheck Is Nothing Then
            Session("blnValidUser") = False
            Session("PersonID") = rsPersonIDCheck.PersonID
        End If
    End If
End Code

The key to the ASP.NET version is the file name: _pagestart.vbhtml. When a request comes in to your site, the Web Pages framework looks for any files named _pagestart.vbhtml or _pagestart.cshtml anywhere on the route through the directory structure to the page that has been requested. If any are found, their contents are executed. In the migration, the _pagestart file is place in the root directory, which means that it will be executed for all requests. If it had been placed in a subfolder, only requests for pages in that subfolder (or subfolders of that subfolder) will cause the pagestart file to be executed. Consequently, it is a good place to check that a user is authenticated, for example, when they access restricted pages which have been located in a particular folder.


HTML Templating

I have already looked at Helpers, which offer one way of producing reusable HTML template blocks. The role of the helper is more that of a macro for snippets of HTML. Your existing Include files are more likely to consist of self-contained blocks of reusable code such as headers, footers, menus and so on. The Razor way to deal with these is in two parts: layout pages and partials. A layout page acts as the base template for the HTML of the site. It can be anything from a bare-bones wireframe HTML structure containing placeholders for pluggable content to a more or less complete page with just one area for dynamic content to be displayed.

The layout page is defined by the fact that it has a call to RenderBody() in it. Here is the most simple layout page possible:

<!DOCTYPE html>
<html lang="en">
        <meta charset="utf-8" />

This file will be saved with a name like _layout.vbhtml anywhere in the application folder structure. The leading underscore in the file name prevents it from being browsed directly. The layout page is merged at runtime with any file that has it set as the Layout property. Here's a Hello World page that does exactly that:

    Layout = "~/_layout.vbhtml"
End Code
<div>Hello World</div>

When HelloWorld.vbhtml is requested, its content is merged with that of _layout.vbhtml and injected into the spot where the RenderBody() call is placed. Note the tilde (~) sign: that resolves at runtime to point to the root of the site folder structure. If you want to specify the same payout page for all pages in your site (or a folder) you can use the _pagestart.vbhtml file to achieve this.

Partial pages are used primarily for pluggable widgets or stand-alone sections of reusable UI. You could use one for a navigation system like this:

    <li><a href="~/">Home</a></li>
    <li><a href="~/About">About</a></li>
    <li><a href="~/Contact">Contact</a></li>

This can be plugged in to your layout or child page by calling the RenderPage() method:

<!DOCTYPE html>
<html lang="en">
        <meta charset="utf-8" />

You can nest layout pages and even call RenderPage within partial pages, which permits some complex levels of traversal and code reuse. It is worth noting the order in which each file will have its content executed. Code in the page which is requested will be executed first, followed by any code in the layout page. This means that you cannot set variables in the layout page which the host page relies on. They will not have been set at the point that the host page is executed.