Filling in the Gaps - Problem Design Solution 3.5 - Part 2
Prologue
In a previous post, I began dissecting the code in my ASP.Net 3.5 Website Programming: Problem - Design - Solution book attempting to fill in gaps that I found (such as where does the code in the book go and adding in code missing from the book). This is a continuation of that post, continuing in Chapter 3.
Problem
In this post, I will attempt to address the missing pieces regarding:
- The BasePage class.
- The SiteMap Provider.
- The SiteMap Handler.
- A URL Rewrite Class.
- An addition to our Helper Class.
- Configuring the ELMAH Error Logging Add-In.
So with no further ado, let's get started.
Solution
The BasePage Class has a fair amount of code presented in the book. Specifically I found the following code pieces in chapter 3:
- The MoveHiddenFields Property (pg 106)
- The MoveHiddenFieldsToBottom Function (pg 106)
- The OverRidden Render Subroutine (pg 107)
- The PrimaryKeyID Property (pg 107)
- The CreateMetaControl Subroutine (pg 138)
- The GetMetaValue Function (pg 139)
- The PageKeyWords Property (pg 139)
I did find, however, that we should also include a property to add content to the Description META tag. Adding this functionality is mentioned on page 121, however code is not included. It's very similar to your PageKeyWords property. This PageDescription property should be added to your BasePage class as follows *Gap-Filled*:
Protected Property PageDescription() As String
Get
Return GetMetaValue("DESCRIPTION")
End Get
Set(ByVal value As String)
CreateMetaControl("DESCRIPTION", value)
End Set
End Property
I also found, mentioned on page 122, a Robots META tag. I was unable to find any listing of this code in the book or in the downloaded files. So I created my own *Gap-Filled*. The book mentioned that the content section of the tag could contain combinations of action (index, noindex) and directive (follow, nofollow). I created an enumeration of the six valid values (including all and none) and called the CreateMetaControl with the corresponding string values. The code is as follows *Gap-Filled*:
Protected Enum RobotMetaOptions
IndexFollow
IndexNoFollow
NoIndexFollow
NoIndexNoFollow
All
None
End Enum
Protected Property PageRobotMeta() As RobotMetaOptions
Get
Return GetMetaValue("ROBOTS")
End Get
Set(ByVal value As RobotMetaOptions)
Select Case value
Case RobotMetaOptions.IndexFollow, RobotMetaOptions.All
CreateMetaControl("ROBOTS", "index, follow")
Case RobotMetaOptions.IndexNoFollow
CreateMetaControl("ROBOTS", "index, nofollow")
Case RobotMetaOptions.NoIndexFollow
CreateMetaControl("ROBOTS", "noindex, follow")
Case RobotMetaOptions.NoIndexNoFollow, RobotMetaOptions.None
CreateMetaControl("ROBOTS", "noindex, nofollow")
End Select
End Set
End Property
In looking around the web, it seems that really, the index-follow option (or all option) is redundant since that is the default action if there is no Robots META tag. I included the options just for the sake of completeness.
We can also notice that there is reference on page 121 to page title information. I found that again this is added in the code download, but I didn't find reference to it in the book. *Gap-Filled* I added the following property from the code download for PageTitle to my BasePage class:
Protected Property PageTitle() As String
Get
If Not IsNothing(Master) Then
If Master.Page.Title = String.Empty Then
Return String.Empty
Else
Return Master.Page.Title
End If
Else
If Page.Title = String.Empty Then
Return String.Empty
Else
Return Page.Title
End If
End If
End Get
Set(ByVal value As String)
If Not IsNothing(Master) Then
Master.Page.Title = value
Else
Page.Title = value
End If
End Set
End Property
There are a couple of other methods that I think may come in handy. Since I haven't read past this chapter yet, I'm not sure if they are added later as I can't find them in the index, but they are in the code download. I decided to add them just in case. I added the following to my BasePage class:
Public ReadOnly Property BaseUrl() As String
Get
Dim url As String = Me.Request.ApplicationPath
If url.EndsWith("/") Then
Return url
Else
Return url & "/"
End If
End Get
End Property
Public ReadOnly Property FullBaseUrl() As String
Get
Return Me.Request.Url.AbsoluteUri.Replace( _
Me.Request.Url.PathAndQuery, "") & Me.BaseUrl
End Get
End Property
There were also a couple that I decided NOT to add as I felt it was redundant to add them just to have them call the Helpers class. These include:
- SEOFriendlyURL
- ConvertToHTML
- GetUserProfile
- FormatPrice
Looking through the rest of the code, I'm thinking that some of that may be added later in the book or at least it may not apply till later so I will move on to the next class that needs work.
A Custom SiteMapProvider
On page 125, we're informed that we should be using a custom SiteMapProvider to generate our site map. Unfortunately, we aren't given ANY code that I find on how this is done with the exception of the settings to register the provider in the web.config file. We'll come back to that piece, but first let's create the provider so we know what to register. We're told that we can get a good primer on doing this from a series of 2006 MSDN articles. I found that some of the code comes across into our code download, and some doesn't. Also the article is in C#, so I relied heavily on the code download. Create a TBHSiteMapProvider.vb class in your 'navigation' folder *Gap-Filled* and add the following to get started:
Public Class TBHSiteMapProvider
Inherits StaticSiteMapProvider
Private Const _errmsg1 As String = "Missing node ID"
Private Const _errmsg2 As String = "Duplicate node ID"
Private Const _errmsg3 As String = "Missing parent ID"
Private Const _errmsg4 As String = "Invalid parent ID"
Private Const _errmsg5 As String = "Empty or missing connectionStringName"
Private Const _errmsg6 As String = "Missing connection string"
Private Const _errmsg7 As String = "Empty connection string"
Private Const _errmsg8 As String = "Invalid sqlCacheDependency"
Const _cacheDependancyName As String = "__SiteMapCacheDependency"
Private _connect As String
Private _database, _table As String
Private _2005dependency As Boolean = False
Private _indexID, _indexTitle, _indexUrl, _indexDesc, indexRoles, _indexParent As Integer
Private _nodes As New Dictionary(Of Integer, SiteMapNode)(16)
Private _root As SiteMapNode
Public ReadOnly _lock As Object = New Object()
End Class
This will get our class started, you may need to add import statements as the need arises, but this gets our class going. You'll notice that our class declaration gives us an error. We need to define a number of methods in this class before it will compile. So let's get started, create an Initialize method. The code IS different than on the MSDN article starting from about the half way point. Add the following to your class:
Public Overloads Overrides Sub Initialize(ByVal name As String, _
ByVal config As NameValueCollection)
'Verify parameters
If IsNothing(config) Then Throw New ArgumentNullException("config")
If String.IsNullOrEmpty(name) Then name = "TBHSiteMapProvider"
'add default desc to config if empty
If String.IsNullOrEmpty(config("description")) Then
config.Remove("description")
config.Add("description", "SQL site map provider")
End If
'call base's initialize
MyBase.Initialize(name, config)
If config("securityTrimmingEnabled") Is Nothing Then
config.Remove("securityTrimmingEnabled")
End If
If config.Count > 0 Then
Dim attr As String = config.GetKey(0)
If Not String.IsNullOrEmpty(attr) Then
Throw New ProviderException("Unrecognized attribute: " & attr)
End If
End If
End Sub
We also need to add one of the required methods BuildSiteMap. This method is substantially changed from the one in the MSDN article, so I pulled it from the downloaded code. However, this version used two other methods that haven't been created yet. We'll start with those methods and then come back to the BuildSiteMap. Add the following two methods to your class:
Private Function CreateSiteMapNodeFromSiteMapEntity(ByVal node As _
SiteMapInfo) As SiteMapNode
Dim roles As String = If(node.Roles, Nothing)
'if roles were specified, turn the list into a string array
Dim roleList As String() = Nothing
If Not String.IsNullOrEmpty(roles) Then
roleList = roles.Split(New Char() {","c, ";"c}, 512)
End If
'create SiteMapNode
Dim _node As New SiteMapNode(Me, node.SiteMapId.ToString(), _
node.URL, node.Title, node.Description, _
roleList, Nothing, Nothing, Nothing)
'record node in the _nodes directory
_nodes.Add(node.SiteMapId, _node)
Return _node
End Function
Private Sub AddChildNodes(ByVal vParentNode As SiteMapNode, _
ByVal SiteMapId As Integer)
Dim lChildNodes As List(Of SiteMapInfo) = (From lChildren In _
lSiteMapNodes Where lChildren.Parent = SiteMapId).ToList()
For Each lChildNode As SiteMapInfo In lChildNodes
Dim lNode As SiteMapNode = _
CreateSiteMapNodeFromSiteMapEntity(lChildNode)
AddNode(lNode, vParentNode)
AddChildNodes(lNode, lChildNode.SiteMapId)
Next
End Sub
You'll notice if you look at the MSDN article, that they aren't included there so they are newly created for our project from the code download. Now that they're defined, we're ready to add the BuildSiteMap method:
Protected lSiteMapNodes As List(Of SiteMapInfo)
Public Overrides Function BuildSiteMap() As SiteMapNode
SyncLock _lock
If _root IsNot Nothing Then Return _root
Using lSiteMapContext As New SiteMapRepository
lSiteMapNodes = lSiteMapContext.GetSiteMapNodes()
If lSiteMapNodes.Count > 0 Then
Dim node As SiteMapInfo = (From lSiteMapNode In _
lSiteMapNodes Where lSiteMapNode.Parent = 0).FirstOrDefault
If Not IsNothing(node) Then
_root = CreateSiteMapNodeFromSiteMapEntity(node)
AddNode(_root, Nothing)
AddChildNodes(_root, node.SiteMapId)
End If
End If
End Using
Return _root
End SyncLock
End Function
If you look at the errors, you'll see that we still have to define another overridden function, GetRootNodeCore. Again, we'll go to the code download since it isn't in the article or the book. Add the following to your class:
Protected Overloads Overrides Function GetRootNodeCore() As SiteMapNode
SyncLock _lock
BuildSiteMap()
Return _root
End SyncLock
End Function
It's a pretty simple function, but necessary. The provider also needs to be registered in the web.config file. To do this, add the following to your web.config file, it should go somewhere in your <system.web> element:
<siteMap defaultProvider="TBHSiteMapProvider" enabled="true">
<providers>
<add name="TBHSiteMapProvider"
type="TheBeerHouse.TBHSiteMapProvider"
securityTrimmingEnabled="true" />
</providers>
</siteMap>
There are two other methods in the downloaded code, however I'm not sure exactly what they do, I can't that they are called/referenced anywhere else. Since I don't see where they are used (at least at this point), I haven't included them.
A SiteMapHandler Class
This class is a handler that returns the sitemap.xml file used by search engine spiders so that our site can be easily indexed. This class DOES have some code included in the book, specifically it starts on page 126. Before we delve into the code there, let's create our file by adding a new class to the navigation folder and naming it 'SiteMapsHandler.vb' *Gap-Filled*. Following the code in the book starting at the bottom of page 126, we want to make sure that our class implements the iHttpHandler interface (requiring import of System.Web). When you press enter, you'll get stubs for two required elements (IsReusable property and ProcessRequest Method).
The IsReusable property can just return true *Gap-Filled*. ProcessRequest has a few more lines, but only a couple. You can find them at the top of page 127. As we start, your class should look like this:
Imports System.Web
Public Class SiteMapHandler
Implements IHttpHandler
Public ReadOnly Property IsReusable() As Boolean Implements _
System.Web.IHttpHandler.IsReusable
Get
Return True
End Get
End Property
Public Sub ProcessRequest(ByVal context As System.Web.HttpContext) _
Implements System.Web.IHttpHandler.ProcessRequest
BaseContext = context
CreateSiteMap()
Response.Flush()
Response.End()
End Sub
End Class
You'll notice that about every line gives us an error since nearly everything in this method is undefined. Examining the downloaded file, we find that these undefined items are actually properties created in our class (with the exception of CreateSiteMap()) *Gap-Filled*. Add the following properties to your class:
Dim _baseContext As HttpContext
Public Property BaseContext() As HttpContext
Get
Return _baseContext
End Get
Set(ByVal value As HttpContext)
_baseContext = value
End Set
End Property
Public ReadOnly Property Response() As HttpResponse
Get
Return BaseContext.Response
End Get
End Property
Public ReadOnly Property Request() As HttpRequest
Get
Return BaseContext.Request
End Get
End Property
This will take care of all but our CreateSiteMap() method error. So without further ado we'll add our CreateSiteMap method. This can be found on page 128. (Note: if you haven't used XDocument to do XML before, it is very particular about where you put your %> characters so follow the below code exactly so that you don't get errors - I learned by hard experience since I hadn't used it before). Add the following for your CreateSiteMap method:
Private Sub CreateSiteMap()
Response.ContentType = "application/xml"
Dim lSiteMapNodes As List(Of SiteMapInfo)
Using siteMaprpt As New SiteMapRepository
lSiteMapNodes = siteMaprpt.GetSiteMapNodes
End Using
Dim xSiteMap As XDocument = <?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<%= From lSiteMapNode In lSiteMapNodes.AsEnumerable _
Select <url>
<loc><%= lSiteMapNode.URL %></loc>
<lastmod><%= lSiteMapNode.DateUpdated %></lastmod>
<changefreq>weekly</changefreq>
<priority>0</priority>
</url> %>
</urlset>
Response.Write(xSiteMap.ToString())
End Sub
Now we also need to add our handler to the web.config so that our website knows to use it. Add the following to the httpHandlers section of your web.config:
<add verb="GET" path="sitemap.xml" validate="false" type="TBH.SiteMapHandler, TBHBLL, Version=3.5.0.1, Culture=neutral, PublickeyToken=null" />
A URL Rewrite Class
This I feel is one of the less complete modules in the chapter. Originally when I attempted to create the code from this chapter I was only creating code that I found in the solution section. This meant that a couple critical pieces of this module were left out. By looking through the chapter, there was actually much more available.
First we'll need to create a class for our Rewrite code. This should go in a folder called 'Module' in our BLL. Once the folder is created, add a class file and name it URLRewrite.vb. *Gap-Filled*
We get our first code pieces starting on page 124 in the book. Right off, we should make sure that our class implements the IHttpModule interface, so add that to your class definition. Next, we can the code we need for the Init method. We can add that and you class should look something like this:
Public Class URLRewrite
Implements IHttpModule
Public Sub Init(ByVal context As System.Web.HttpApplication) _
Implements System.Web.IHttpModule.Init
AddHandler context.BeginRequest, AddressOf BeginRequest
End Sub
Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
End Sub
End Class
You'll notice that while the book says we should intercept the AuthorizeRequest event that we're using the BeginRequest event. There is a difference between what we see on page 124 and what we read later on pages 135-6, but I went with the one that matched the code download, BeginRequest. *Gap-Filled*
Next, skip over to page 135 and we'll start adding the rest of the module. We will create a variable to use for Regular Expressions. Add the following to your class:
Private Shared wwwRegex As New Regex("https?://www\.", RegexOptions.IgnoreCase Or RegexOptions.Compiled)
Following along on page 136, we'll also add the BeginRequest event handler that we registered in our Init method. This is defined as follows:
Private Sub BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
Dim app As HttpApplication = CType(sender, HttpApplication)
Dim Request As HttpRequest = app.Request
Dim Response As HttpResponse = app.Response
Dim sRequestedURL As String = Request.Url.ToString.ToLower
Dim redirectURL As String = String.Empty
Dim bWWW As Boolean = wwwRegex.IsMatch(sRequestedURL)
If bWWW Then redirectURL = wwwRegex.Replace(sRequestedURL, _
String.Format("{0}://", Request.Url.Scheme))
Rewrite(app)
End Sub
The Rewrite method is next, it is found on page 137 towards the bottom. Unfortunately there is a SIGNIFICANT difference between the code in the book and the code in the downloaded version. The book talks about doing a 301 redirect and looking up the information in the sitemap table, however the book doesn't seem to implement this. The downloaded code seems to to this, so I will be including that code rather than just the book's code. *Gap-Filled* To start, let's create a helper function for our Rewrite method. Add the following to your code:
Private Sub Do301Redirect(ByVal response As HttpResponse, _
ByVal redirectURL As String)
response.RedirectLocation = redirectURL
response.StatusCode = 301
response.End()
End Sub
Now let's create our Rewrite method. Add the following:
Private Sub Rewrite(ByVal app As HttpApplication)
If app.Context.Request.Path.ToLower.EndsWith(".aspx") Then
Using lSiteMapRst As New SiteMapRepository_
(Globals.Settings.DefaultConnectionStringName)
Dim lURLFile As String = _
Helpers.GetURLPath(app.Context.Request.Url.ToString)
Dim lSiteMap As SiteMapInfo = lSiteMapRst.GetSiteMapInfoByURL _
(lURLFile.Replace(Globals.Settings.devSiteName, ""))
If Not IsNothing(lSiteMap) Then
If lSiteMap.RealURL <> lURLFile Then
HttpContext.Current.RewritePath("/" & lSiteMap.RealURL, _
False)
ElseIf lSiteMap.URL <> lSiteMap.RealURL Then
Do301Redirect(app.Response, _
Path.Combine(Globals.Settings.SiteDomainName, _
lSiteMap.URL))
End If
End If
End Using
End If
End Sub
I did make one change from the downloaded code, I removed a settings variable since all we did was get our Globals.Settings object and it wasn't that much more work to add the Globals path to the beginning of the settings object when it was called.
You'll also notice that we created an error with our Helpers.GetURLPath that we haven't defined yet. We'll go define that now so that we can successfully build our application.
Another Method for our Helpers Class
I haven't found where much of the Helper class is defined in the book, so I went to the code download for this method. *Gap-Filled* Go over to your Helpers class and we'll add the following method:
Public Shared Function GetURLPath(ByVal sUrl As String) As String
Dim _Regex As Regex = New Regex("://[^/]+/(?<path>[^?\s<>#""]+)")
If _Regex.Matches(sUrl).Count > 0 Then
Return _Regex.Match(sUrl).Groups(1).ToString()
End If
Return sUrl
End Function
Configuring the ELMAH Error Logging Add-In
One of my little pet peeves is when you download something and then have to search all over the internet for specific instructions on how to install and configure it. This was the case for some information for getting ELMAH started in our project. I think I'm going to have to pull from a couple articles, as well as using the book, to get it all 100% happy. *Gap-Filled*
Starting on page 139, it talks about configuring ELMAH, so we can get some of our direction from the book, however before we do that, we'll need to get it installed. I found a short article that I based my installation on, basically you:
- Download ELMAH.
- Extract the Files.
- Copy the Elmah.dll into the BIN directory of your website.
- Configure ELMAH in your web.config.
We can now go to the book and do a little bit of configuration. First we need to add a Section definition to our web.config, much like we needed to do for our BeerHouseSection for configuration. Add the following to the ConfigSections element of your web.config:
<sectionGroup name="elmah">
<section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah" />
<section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
<section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
<section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah" />
</sectionGroup>
Now we can add the configuration information into an Elmah section in our web.config. Add the following to the Configuration section in your web.config.
<elmah>
<security allowRemoteAccess="0" />
<errorLog type="Elmah.SqlErrorLog, Elmah" connectionStringName="" />
<errorFilter>
<test>
<equal binding="HttpStatusCode" value="404" valueType="Int32" />
</test>
</errorFilter>
<errorMail from="error@thebeerhouse.com" to=dogbert@dogbert.com subject="Exception in the Beerhouse Site." async="true" smptPort="25" smtpServer="mail.thebeerhouse.com" />
</elmah>
You'll notice that I have made a correction in the 'equal' element. The book has incorrectly listed this attribute as valueType *Gap-Filled*, the correct attribute is type. Finally for configuration we need to add handlers so that the modules will actually be used. We need to add the following to our system.web section within the HttpModules subsection:
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" />
<add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" />
<add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" />
and the following into our system.web's httpHandlers section:
<add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
Finally, one last piece that I had a harder time finding, and that is setting up the database. To do this, you need to add the ELMAH table and it's 3 stored procedures (thanks NinethSense). If you downloaded the ELMAH packages, there is a folder in the zip file labeled 'DB'. In this folder, you will find the script to setup your DB. I chose to use SQL server so I loaded the SQLServer.sql file into my SQL Server Management Studio and ran the script. Viola the DB is setup.
Epilogue
Well, there you go, hopefully you'll find this helpful, I certainly would have.
Having finished with this particular chapter, I have started reading further (albeit in pieces here and there) in chapter 5 and I think that actually everything will be much easier to understand in these chapters. If they aren't, then look forward to another post in the series...?