Allowing special characters (forward slash, hash, asterisk etc) in ASP.Net MVC URL parameters

I’ve been getting into ASP.Net MVC a lot lately and there is plenty that is good about it.

One thing that is not good is the problems that MVC has when you have a special character (*,/,& etc) in your querystring.

Eg: Say you want to pass the param

url=http://google.com

MVC won’t like it - ‘/’ is a special character reserved as a separator for routes. But standard URL encoding it won’t work either:

url=http%3A%2F%2Fgoogle.com

MVC will still give a ‘HTTP Error 400 - Bad Request error’.

After looking around, it seems this error has been coming up a bit here and here and Phil Haack explains why standard encoding won’t work here.

One answer is to use base 64 encoding on any parameters that might contain the special characters.


    Public Function MyURLEncode(ByVal sInString As String) As String
        Dim sInStringNoSpaces As String = sInString.Replace(" ", "")
        Dim sURLEncoded As String = HttpUtility.UrlEncode(sInString)

        If sURLEncoded.Replace("+", "") = sInString.Replace(" ", "") Then
            Return sURLEncoded
        Else
            Return "=" & ToBase64(sInString)
        End If
    End Function

    Public Function MyURLDecode(ByVal sEncodedString As String) As String
        If sEncodedString.StartsWith("=") = True Then
            MyURLDecode = FromBase64(Mid$(sEncodedString, 2))
        Else
            MyURLDecode = sEncodedString.Replace("+", " ")
        End If
    End Function

    Private Function ToBase64(ByVal sInString As String) As String
        Dim btByteArray As Byte()
        Dim a As New System.Text.ASCIIEncoding()

        btByteArray = a.GetBytes(sInString)

        ToBase64 = System.Convert.ToBase64String(btByteArray, 0, btByteArray.Length)
    End Function

    Private Function FromBase64(ByVal sBase64String As String) As String
        Dim objUTF8 As New UTF8Encoding()
        FromBase64 = objUTF8.GetString(Convert.FromBase64String(sBase64String))
    End Function

Because one of the useful things about MVC is the SEO-friendly URLs, the code doesn’t convert to Base 64 unless it is necessary.

To use these functions, here is a brief code snippet:


Public Class HotelsController
    Inherits System.Web.Mvc.Controller

    Function SelectHotelList(ByVal City As String, _
    ByVal Location As String)

        oSearch = New With {.City = MyURLEncode(City), .Location = MyURLEncode(Location)}
        Dim oDO As New RouteValueDictionary(oSearch)
        Return RedirectToAction("List", "Hotels", oDO)
    End Function

    Function List(ByVal City As String, _
                  ByVal Location As String)

        City = MyURLDecode(City)
        Location = MyURLDecode(Location)

	...

    End Function
End Class

Copying cookies across domains in ASP.Net

If you logging in to remote sites using HttpWebRequest then you are probably used to supplying a CookieContainer object to keep track of login sessions etc. However, sometimes a login will do a redirect, leaving your cookies referring to the login url and not usable on the next url.

Eg:
Post login details to
http://yourdomain/login
Site redirects (302) to:
http://yourdomain/admin

The login cookie is stuck on http://yourdomain/login and is not used at http://yourdomain/admin, so the login fails.

The key is to set:
WebRequestObject.AllowAutoRedirect = false
in the HttpWebRequest for http://yourdomain/login

Code to copy cookies:


Dim oOldCookies As New CookieContainer
Dim oNewCookies As New CookieContainer
Dim oOldCookiesCol As New CookieCollection

'Do HttpRequest on http://yourdomain/login, place cookies in oOldCookies
'Remember to set WebRequestObject.AllowAutoRedirect = false

oOldCookiesCollection = oOldCookies.GetCookies(New System.Uri("http://www.olddomain.com"))
For iC = 0 To oOldCookiesCol.Count - 1
   oNewCookies.Add(New Cookie(oOldCookiesCol(iC).Name, oOldCookiesCol(iC).Value, "", "http://www.newdomain.com"))
Next

'Now do HttpRequest on http://yourdomain/admin, using oNewCookies

Getting Flights info from the Kayak API in ASP.Net

Kayak is one of the top travel meta-search engines on the web, and they have a free API which allows you to get flight and hotel information.

To use the Kayak API, first get a developer key

Then you can read the full specifications to get the info. Or, just use the code below :)

This gets the flights given an Origin, Destination, Departure date and the number of travellers (note: Origin & Destination must be in the form of a 3 letter Airport code, eg: MEL = Melbourne, SYD = Sydney, LAX = Los Angeles).

You can also Download the full source code


    Private Function Flights(ByVal dtDepartDate As Date, _
        ByVal sOriginCode As String, _
        ByVal sDestinationCode As String, _
        ByVal iTravellers As Integer) As DataTable

        Dim sResult As String
        Dim oCookies As New CookieContainer
        Dim sURL As String
        Dim oDoc As New System.Xml.XmlDocument
        Dim sSID As String
        Dim sSearchID As String
        Dim bMorePending As Boolean = True
        Dim oTripList As System.Xml.XmlNodeList
        Dim oTrip As System.Xml.XmlNode
        Dim oFlights As DataTable
        Dim oFlight As DataRow
        Dim iFlightsFound As Integer
        Dim sDeveloperKey As String = "***Your Key Here****"

        'Full specs at: http://www.kayak.com/labs/api/search/spec.html

        'Get the SessionID
        sURL = "http://www.kayak.com/k/ident/apisession?version=1&token=" & sDeveloperKey
        sResult = sGetData(sURL, oCookies)
        oDoc.LoadXml(sResult)
        sSID = oDoc("ident")("sid").InnerText

        'Start the Flight Search & get the SearchID
        sURL = "http://www.kayak.com/s/apisearch?" & _
        "basicmode=true&oneway=y" & _
        "&origin=" & sOriginCode & _
        "&destination=" & sDestinationCode & _
        "&depart_date=" & Format(dtDepartDate, "MM/dd/yyyy") & _
        "&depart_time=a&" & _
        "return_date=" & _
        "&return_time=a" & _
        "&travelers=" & iTravellers & _
        "&cabin=b&action=doflights&apimode=1&_sid_=" & sSID
        sResult = sGetData(sURL, oCookies)
        oDoc.LoadXml(sResult)
        sSearchID = oDoc("search")("searchid").InnerText

        oFlights = dtFlightsTableInit()

        Do While bMorePending = True
            'Get the first lot of results
            sURL = "http://www.kayak.com/s/basic/flight?" & _
            "searchid=" & sSearchID & _
            "&c=10&apimode=1&_sid_=" & sSID
            sResult = sGetData(sURL, oCookies)
            oDoc.LoadXml(sResult)
            bMorePending = IIf(oDoc("searchresult")("morepending").InnerText = "true", True, False)
            If bMorePending = True Then
                'Wait for 5 seconds, then poll again
                System.Threading.Thread.Sleep(5 * 1000)
            End If
        Loop
        iFlightsFound = Val(oDoc("searchresult")("count").InnerText)

        'Build the final request, asking for the total number of flights found by Kayak.com
        sURL = "http://www.kayak.com/s/basic/flight?" & _
        "searchid=" & sSearchID & _
        "&c=" & iFlightsFound & _
        "&apimode=1&_sid_=" & sSID
        sResult = sGetData(sURL, oCookies)
        oDoc.LoadXml(sResult)

        oTripList = oDoc.SelectNodes("//trip")
        For Each oTrip In oTripList
            oFlight = oFlights.NewRow()
            oFlight("DepartDate") = oTrip("legs")("leg")("depart").InnerText
            oFlight("ArriveDate") = oTrip("legs")("leg")("arrive").InnerText
            oFlight("Airline") = oTrip("legs")("leg")("airline_display").InnerText
            oFlight("Price") = oTrip("price").InnerText
            oFlight("BookURL") = oTrip("price").Attributes("url").Value
            oFlights.Rows.Add(oFlight)
        Next

        Flights = oFlights
    End Function

    Public Function dtFlightsTableInit() As DataTable
        Dim dtFlights As New DataTable

        dtFlights.Columns.Add(New DataColumn("DepartDate", GetType(String)))
        dtFlights.Columns.Add(New DataColumn("ArriveDate", GetType(String)))
        dtFlights.Columns.Add(New DataColumn("Airline", GetType(String)))
        dtFlights.Columns.Add(New DataColumn("Price", GetType(String)))
        dtFlights.Columns.Add(New DataColumn("BookURL", GetType(String)))

        dtFlightsTableInit = dtFlights
    End Function

    Public Function sGetData(ByVal sURL As String, _
Optional ByRef oCookies As CookieContainer = Nothing) As String

        Dim Writer As StreamWriter = Nothing
        Dim WebRequestObject As HttpWebRequest
        Dim sr As StreamReader
        Dim WebResponseObject As HttpWebResponse
        Dim sbResultsBuilder As New StringBuilder
        Dim sBuffer(8192) As Char
        Dim iRetChars As Integer

        WebRequestObject = CType(WebRequest.Create(sURL), HttpWebRequest)
        WebRequestObject.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)"
        WebRequestObject.Method = "GET"
        WebRequestObject.Timeout = 55000
        WebRequestObject.ReadWriteTimeout = 55000
        WebRequestObject.AllowAutoRedirect = True

        If Not (oCookies Is Nothing) Then
            WebRequestObject.CookieContainer = oCookies
        End If

        WebResponseObject = CType(WebRequestObject.GetResponse(), HttpWebResponse)

        sr = New StreamReader(WebResponseObject.GetResponseStream)

        Do
            iRetChars = sr.Read(sBuffer, 0, sBuffer.Length)
            If iRetChars > 0 Then
                sbResultsBuilder.Append(sBuffer, 0, iRetChars)
            End If
        Loop While iRetChars > 0
        sGetData = sbResultsBuilder.ToString

    End Function

The Kayak API is pretty good, but does have some limitations, so I think is works best when you use it for stats rather than bookings. Anyone else know of some good travel APIs?

Automate FireFox with ASP.net

Usually, if you are writing in c# or vb.net then when you want to automate a browser you would use the MS WebBrowser control (AxWebBrowser). However, that control is missing some attributes (eg - you can’t set the referrer), but Firefox gives you the lot.

All the hard work is done by the SWAT (Simple Web Automation Toolkit) library (http://ulti-swat.wiki.sourceforge.net/). Download the SWAT.dll and add a reference. You also have to add the file Interop.SHDocVw.dll to the same directory and also add a reference.

A quick demo - get the first result from Google for a search term:


        Dim sResult As String
        sResult = GoogleFirstResult("web hosting")

    Private Function GoogleFirstResult(ByVal sSearchTerm As String) As String
        Dim oFireFoxBrowser As SWAT.WebBrowser

        oFireFoxBrowser = New SWAT.WebBrowser(BrowserType.FireFox)
        oFireFoxBrowser.OpenBrowser()

        oFireFoxBrowser.NavigateBrowser("http://google.com")

        oFireFoxBrowser.SetElementAttribute(IdentifierType.Name, "q", "value", sSearchTerm)

        oFireFoxBrowser.StimulateElement(IdentifierType.Name, "btnI", "onclick")

        'Give the browser a chance to get to the site.
        oFireFoxBrowser.Sleep(1000)

        GoogleFirstResult = oFireFoxBrowser.CurrentLocation

    End Function

How to access HttpServerUtility.MapPath in a Thread

Usually, when you want the root directory of your website, it is easy to call:

HttpContext.Current.Server.MapPath

or just

Server.MapPath

from a page.

But if you are running your code in a thread or timer, then HttpContext.Current is null (giving you a NullReferenceException), and you are out of luck. :(

However, as long as you are not using virtual directories, there is an easy fix:

AppDomain.CurrentDomain.BaseDirectory

will give you the same result, and doesn’t depend on a current HTTP context.

Integrating JQuery with ASP.net - A Cool Client-side Alert Box

This code shows how to display a client-side Alert Box from within server-side code. Of course, being built on JQuery, the Alert Box is cooler than usual.

ASP.Net Alert Box

The Alert Box is built on the Impromtu Plugin for JQuery. It also uses the Corners Plugin for the rounded corners.

Here is a demo of the ASP.Net Alert Box. Clicking the button will submit the page, and the Alert Box will be displayed once the page has been returned.

Here is the code behind the Submit Button:


Protected Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
clsJQuery.AlertBox(Me.Page, "this is a demo of the Alert Box using JQuery")
End Sub

And here is the code in the clsJQuery Class:


Public Class clsJQuery
    Public Shared Sub AlertBox(ByRef oPage As Page, ByVal sMsg As String)

        Call IncludeJQuery(oPage)
        Call IncludeImpromptu(oPage)
        Call IncludeCorners(oPage)

        'Add the script after all the HTML
        Dim sb As StringBuilder
        sb = New StringBuilder
        sb.Append("<script language=javascript>$.prompt('" & sMsg & "').children('#jqi').corner();</script>")
        oPage.Controls.AddAt(oPage.Controls.Count, New LiteralControl(sb.ToString()))
        sb = Nothing

    End Sub

    Private Shared Sub IncludeJQuery(ByVal oPage As Page)
        Call IncludeJSLibrary(oPage, "jquery-1.2.6.min.js")
    End Sub

    Private Shared Sub IncludeCorners(ByVal oPage As Page)
        Call IncludeJSLibrary(oPage, "jquery.corner.js")
    End Sub

    Private Shared Sub IncludeImpromptu(ByVal oPage As Page)
        Call IncludeJSLibrary(oPage, "jquery-impromptu.1.5.js")

        If oPage.ClientScript.IsClientScriptBlockRegistered("jquery.alert.css") = False Then
            oPage.ClientScript.RegisterClientScriptBlock(oPage.GetType, "jquery.alert.css", "<LINK href='jquery.alert.css' type=text/css rel=stylesheet >")
        End If
    End Sub

    Private Shared Sub IncludeJSLibrary(ByRef oPage As Page, ByVal sJSFileName As String)
        Dim sRegKey As String = "jquery-1.2.6.min.js"

        If oPage.ClientScript.IsClientScriptIncludeRegistered(sJSFileName) = False Then
            oPage.ClientScript.RegisterClientScriptInclude(oPage.GetType, sJSFileName, sJSFileName)
        End If

    End Sub
End Class

All pretty simple :) The only things to really note are the different subs which make sure that the jquery scripts are only loaded once on the page (the subs IncludeJQuery, IncludeImpromptu etc can should be called when using other jquery functions). Also, the adding of the jquery call as a new page element at the foot of the page ensures the libraries are loaded before the function is called.