.Net ramblings
# Thursday, 03 February 2005
An ISAPI Filter to have nice URLs for your Asp.Net site

Background

My content management system stores all the pages in a sql database, so if you go to /home.html, there is no document by that name on the server.  Instead, i have one .aspx page called content.aspx that handles every request dynamically and it outputs the content for the page.

You can partially achieve this same effect without using an ISAPI filter (see http://www.codeproject.com/aspnet/URLRewriter.asp) but you don't get the advantage of specifying a folder path as a valid url.  For example, if you request /Departments/HR/ then you will get a 404 on the server, because IIS doesn't know to pass on this request to my /content.aspx page.  Jakob Nielsen (the usability expert) says that hackable urls are important, i.e. users should be allowed to remove parts of the path of the page they are at, and arrive at a level higher up in the site.  The ISAPI filter used here allows this.

Requirements for the Filter

  • Every .html request should be forwarded to /content.aspx
  • Every default document request should be forwarded to /content.aspx
  • A request with a querystring (?) should not be redirected because it is assumed to be a dynamic page.
  • The IIS log should record the original url (i.e. /Home.html) instead of /content.aspx?Url=/home.html

Because the code is in C++ and there is a lot of messy error checking stuff going on all over the place, i won't post it here (mess up my blog). instead you can download the VS 2003 solution (20 k) with source files + executable, and examine it for yourself if you're interested.  It does work, and i have about 15 sites runnning off this filter for a long time now. 

Credits

This solution is 95% taken from David Wang's excellent post on the ISAPI-DEV ms newsgroup. I just added in support for default documents and ignoring dynamic page requests.


Thursday, 03 February 2005 12:14:02 (GMT Standard Time, UTC+00:00)  #    Comments [2]  Asp.Net

# Saturday, 22 January 2005
Howto: Disable windows automatically adding network printers and faxes

i am configuring my new web server, and i noticed that the event log keeps getting 5 or 6 system warning events about printers getting added or deleted. since i pay close attention to my log, and have no print requirements on the web server, those print logs are clutter. i noticed that windows kept adding in printers that are available on my network, even after i deleted them. after hunting around online, i found that microsoft humbly admit that the feature to automatically add network printers may be undesirable, which it is in my case. at least they let you turn it off.

Open windows explorer, Tools > Folder Options > View > Advanced Settings > turn off "Automatically search for network folders and printers", its the first option in the list.

http://support.microsoft.com/?kbid=320138

The KB article says it only applies to XP but it applies to windows server 2003 also.


Saturday, 22 January 2005 22:28:46 (GMT Standard Time, UTC+00:00)  #    Comments [2]  General | Windows Server

# Monday, 17 January 2005
[FIX] Network bridge with Wireless + Wired connections is slow

Many thanks to James Crossley for his post on google which saved me a lot of bother.

"I have a wired network and an ad-hoc wireless network bridged on a Windows XP machine, which is also connected to broadband. The wired side ran fine but the wireless side ran very slowly (around 500kbps).
It ran at full speed when out of the bridge. The fix for this was:

  1. Run cmd (start, run, type cmd)
  2. type : "netsh bridge show adapter". Check which number is your wireless adpater.
  3. type : "netsh bridge set adapter x forcecompatmode=enable", where x is the number of your wireless adapter.

After I ran this, the network ran at full speed in the bridge."

This worked for me too.  both interfaces are running at normal speeds.


Monday, 17 January 2005 12:33:11 (GMT Standard Time, UTC+00:00)  #    Comments [2]  General | Windows Server

# Sunday, 16 January 2005
SQL - change object owner for multiple objects

Tibor Karaszi showed how to do this time saving technique on a newsgroup post. You can use the sp_changeobjectowner stored procedure in the Master database to change the owner of an object one at a time, but there is a semi-automatic way to do it.

Open query analyser, change the selected database to the on you want to work on. The following query generates a list of exec commands for all the sprocs and UDFs in the database:


SELECT 'EXEC sp_changeobjectowner ''dbo.' + ROUTINE_NAME + ''', ''thenewuser'''
FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_SCHEMA = 'dbo'

Execute it, then click the top left corner of the output window, to select all the rows. Copy and paste the rows into a new query window and execute.

To change table owners, modify the query and repeat the proces:

SELECT 'EXEC sp_changeobjectowner ''dbo.' + TABLE_NAME + ''', ''thenewuser'''
FROM INFORMATION_SCHEMA.TABLES

Sunday, 16 January 2005 18:37:40 (GMT Standard Time, UTC+00:00)  #    Comments [2]  Database

# Friday, 17 December 2004
[FIX] aspx validation not working + buttons not clicking

when i deployed my web app to the production server, i found that none of the client-side validation was working. 

the first problem was that there was no mime-type set up on the server for .js files, so i added "application/x-javascript" for the .js extension.

the next problem was that the script references in my aspx pages did not match the script location on the server.   the html source of one of my pages had the webuivalidation.js path set to /aspnet_client/system_web/1_1_4322/WebUIValidation.js

but the server directory was "/aspnet_client/system_web/1_1_4322_0/WebUIValidation.js"

so i tried duplicating the directory so that i would have both paths on the server, but that caused strange problems on the aspx page, such as buttons not clicking (when the event handlers are definitely registered). 

so what i did was run aspnet_regiis -i on the server to make sure all the script maps are registered properly, and it worked. 

if you don't have control over the web server, you can add the following to your web.config

<system.web>
  <webControls clientScriptsLocation="/aspnet_client/system_web/1_1_4322_0/" />
  ...

Friday, 17 December 2004 12:46:46 (GMT Standard Time, UTC+00:00)  #    Comments [1]  Asp.Net

# Wednesday, 15 December 2004
Can't debug web projects with VS 2003 after installing VS 2005

Can't debug web projects with VS 2003 after installing VS 2005

Fortunately Fahad Khalil found the answer to this, he posted this simple solution on his blog.

I am reproducing it here for my own records. Shoot your IIS Console, then click on the Default Website and click on Properties. You will see a Brand new shiny tab [labelled ASP.NET] ... courtesy VS .NET 2005 :). Select the correct version i.e. 1.xx :)

Cheers Fahad. Saved me a lot of work.


Wednesday, 15 December 2004 10:58:09 (GMT Standard Time, UTC+00:00)  #    Comments [1]  Asp.Net

# Sunday, 05 December 2004
An intelligent 404 page, suggest pages with a similar name to the requested page

 

Introduction

Did you ever notice on big sites like microsoft.com, if you reach a page that doesn't exist, they don't just say "Sorry, 404.", they give you a list of pages that are similar to the one you requested.  This is obviously very nice for your users to have, and it's easy enough to integrate into your site.  This article provides source code and explains the algorithm to accomplish this feature. Note: the real benefit of the approach outlined here is the semi-intelligent string comparisons.

Background

The need for this grew out of a client of mine who was changing content management system, and every url in the site changed, so all the search engine results came up with 404 pages. this was obviously a big inconvenience, so i put this together to help users find their way through the new site when arriving from a search engine.

See it in action

Go to the following page http://www.iserc.ie/December15-ISERCWorkshoponTesting.html (which doesn't exist), and the 404 page should give you a list of pages that have quite similar names.

Requirements

  • Your web site must be set up so that 404 pages get redirected to a .net aspx page
  • You must have some way of getting an array of all the page urls in your site that you want to compare 404 requests against. if you have a content management system, there is probably a structure of all the pages stored in xml or a javascript array (for DHTML menus or something), or you could write your own query to get the pages from a database. if don't use a content management system, you could hard-code a string array variable in the 404 page code behind containing the page names, or think up some way of dynamically reading all the .aspx or .html pages from the file system.
  • When the 404 page is accessed, you need to know which page was requested. Using web.config, you can set up 404 errorCodes to go to /404.aspx, where it will tag on the requested page to the querystring. the source code here assumes you have this approach, but you can obviously change it to your own needs, simply change the GetRequestedUrl() function.

Why Regular Expressions are not enough

To compare strings, you can use System.String.IndexOf or you can use regular expressions to match similarities, but all these methods are very unforgiving for slight discrepancies in the string. in the example url above, the page name isDecember15-ISERCWorkshoponTesting.html but under the new content management system, the url is December 15 - ISERC Workshop - Software Testing.html, which is different enough to make traditional string comparison techniques fall down.

So i looked around for a fuzzy string compare routine, and came across an algorithm written by a guy called Levenshtein. His algorithm figures out how different 2 strings are, based on how many character additions, deletions and modifications are necessary to change one string into the other. This is called the 'edit distance', i.e. how far you have to go to make 2 strings match. This is very useful because it takes into account slight differences in spacing, punctuation and spelling. I found this algorithm on http://www.merriampark.com/ld.htm where Lasse Johansen kindly ported it to C#. The algorithm is explained at that site, and it is well worth a read to see how it is done.

Normalising the Scores

I originally had a problem with the algorithm because it gave surprising results for certain situations. If the 404 page request was for 'hello' and there is a valid page called 'hello_new_version' and another valid page called 'abcde', then the 'abcde' page gets a better score, because fewer changes are needed to make it the same as hello (just change the 5 characters in 'abcde' into 'hello'). this is 5 changes, even though the 'hello_new_version' is semantically a better match. Fortunately, a kind newsgroup participant named Patrice suggested that i divide the score by the length of the comparison string, to normalise the results. This worked perfectly, and i found that a score between 0 (perfect match) and 0.6 (a good match) is worth including as a suggested page. You can change this value in the ComputeResults() method if you want to make it more or less flexible.

Code Summary

private void Page_Load(object sender, System.EventArgs e)
{
   GetRequestedUrl();
   SetUpSiteUrls();
   ComputeResults();
   BindList();
} 

The above code shows the 4 key tasks that make up this solution. Each method is explained below.

Using the code

  1. GetRequestedUrl() simply figures out which page was requested. In this example, it is assumed that your web.config contains the following:
    ***In this example, the querystring on the 404.aspx page contains the requested url. SetUpSiteUrls() is where you load in all the pages in your site. In my content management system, i have an xml file with all the names, so i do an XPath query and add in the names one by one to the arraylist.
  2. private void SetUpSiteUrls()
    {
      this.validUrls = new ArrayList(); 
      /*
      * Insert code here to add the pages in your site to this arraylist
      */ 
    }
  3. ComputeResults() iterates through the urls you set up in SetUpSiteUrlsreturns and attaches a score of how close each one is to the requested url. It also sorts the results and discards any that are not a close match.
    private void ComputeResults()
    { 
        ArrayList results = new ArrayList(); // used to store the results 
        // build up an arraylist of the positive results 
        foreach(string s in validUrls) 
        {
            // don't waste time calculating the edit distance of nothing with something 
            if(s == "") continue; 
            double distance = Levenshtein.CalcEditDistance(s, this.requestedUrl); // both in lower case 
            double meanDistancePerLetter = (distance / s.Length); // anything between 0.0 and 0.6 is a good match. the algorithm always returns a value >= 0       
            if(meanDistancePerLetter <= 0.60D) 
            { 
                // add this result to the list. 
                results.Add(new DictionaryEntry(meanDistancePerLetter, "<a href='" + s + ".html'>" + s + "</a>")); // use dictionary entries because we want to store the score and the hyperlink. 
                // can't use sortedlist because they don't allow duplicate keys and we have 2 hyperlinks with the // same edit distance. 
            }
        }
        results.Sort(new ArrayListKeyAscend());
    }
    IMPORTANT NOTE: One thing to definitely look out for is the inner-most line of the above code. results.add(new DictionaryEntry(...). I am adding in a html hyperlink, with the name of the page + ".html". This may not be a correct link in your web site, because you may have removed the folder part of the url while populating the validUrls arraylist. You may need to expand the data structures used in this code to include full url for each page.

  4. BindList() simply binds the arraylist of results to the datagrid, which is configured to display them in a bulleted list.
    private void BindList()
    {
        if(results.Count > 0)
        {
    
            this.lblHeader.Text = "The following pages have similar names to " + this.requestedUrl + "";
            this.DataList1.DataSource = results;
            this.DataList1.DataBind();
        }
        else
        {
            this.lblHeader.Text = "Unable to find any pages in this site that have similar names to " + this.requestedUrl + "";
        }
    }

The 'magic' in the code is all done with the Levenshtein.CalcEditDistance method which returns the distance between 2 strings. It is included in the source.

Winforms Test Application

If you're interested to test out the levenshtein algorithm, i've written a windows forms application that lets you enter a string (e.g. a page url) and also a list of strings to compare it against (e.g. all the page urls in your site), and it gives you the 'edit distance' scores. download here (7k)

Comments

I think this is a great feature because it adds significant value to the user experience for a web site. Please feel free to comment below if you have any questions, find any bugs, improvements, or if you can't get it working, or if you use it in a novel way.

Enjoy!


Sunday, 05 December 2004 15:15:28 (GMT Standard Time, UTC+00:00)  #    Comments [0]  Asp.Net

# Monday, 22 November 2004
How to disable new rows in Windows Forms DataGrid

i found a few work arounds to prevent a datagrid from displaying the * new row. one of them involved using a dataview as the datasource, with AllowNew property set to false. however, someone called Sameers from theAngrycodeR pointed out that a datatable has a DefaultView property which also has this AllowNew property. so you can use the following code (if your datasource is a dataset):

this.dataSet1.Tables[0].DefaultView.AllowNew = false;

and you get to keep the dataset or datatable as the direct datasource.


Monday, 22 November 2004 17:05:24 (GMT Standard Time, UTC+00:00)  #    Comments [0]  .Net Windows Forms

Using custom datatypes in a .Net Dataset

Background

In my content management system, i allow the user to define their own 'objects' (e.g. Staff Member) and then i provide templated data entry forms to let them populate instances of these objects.  It's aimed at non-techies so i have my own datatypes called 'Text' which maps to System.String, 'Number' maps to System.Double etc.  I also have a few custom data types called 'File' and 'Image' to allow the user to add files or images to an instance of the object. 

Problem

This business of doing column-mapping was ok as long as my data types had obvious .Net equivalents, but 'Image' doesn't in my case. i'm only storing a reference to the image, but in my application, it's not to be treated just as a System.String.  When the user is creating a new object with an 'Image' field in it, i want to display a file upload instead of a textbox, and when i go to display the object on the site, I want to display a html IMG tag with the SRC set to the value of the image field. 

Solution

The dataset is serialised into an xml file with the schema embedded. i needed to find some way of encoding my own custom data type information into the dataset that would persist into the xml file.  I looked through the VS intellisense and found the 'ExtendedProperties' data column property.  This property allows you to plug in any number of key/value pairs of information to each column.  This was exactly what i needed, so i added in a pair with something like "MyDataType=Image" for each column.  This persisted nicely into the xml file as follows:

<xs:complexType>
  <xs:sequence>
     <xs:element name="Photo" msprop:MyDataType="Image" type="xs:string" minOccurs="0" />

Note that the official type of the field is "xs:string", because it contains a path to the image. but now it also has the custom data type tagged on to the column definition. in this respect, i'm glad to see that MS have provided a very elegant and flexible framework.


Monday, 22 November 2004 16:53:35 (GMT Standard Time, UTC+00:00)  #    Comments [0]  .Net General

# Thursday, 18 November 2004
System.Net.WebException: The request failed with HTTP status 401: Unauthorized.

I have a WSE2 web service that was working fine until one day i got the above error.  i realised i had changed the permissions on the folder (for the web application), so that only System, Adminstrators and ASPNET had permissions on it.  Previously the 'Users' group had permissions.  By a process of elimination, i found out that IUSR_xx needed to have read/execute permissions aswell as ASPNET, even though the process is running with the identity of ASPNET. 

it's strange, but i thought i'd post my solution here in case anyone else comes across this.


Thursday, 18 November 2004 12:45:53 (GMT Standard Time, UTC+00:00)  #    Comments [5]  Asp.Net