Smart404: Handle 404 smartly in ASP.NET

Introduction
The 404 or Not Found error message is an HTTP standard response code, there are many reasons for a 404 to show up like,

  • Mistyped URL
  • Pages have been moved.
  • Pages have been deleted.

The default error page for 404 may look something similar to this:

The purpose of this blog/article is to discuss ways to handle 404 smartly in ASP.NET.

So the big question is how? On an event of 404, the first thing I would try do is to catch the event and redirect to somewhere where I can handle it in a more nicer way. In ASP.NET its just a matter of second, simply add the following node in your web.config thats it.


.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #006080; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

    <customErrors mode="On">
      <error statusCode="404" redirect="~/error404.aspx" />
    </customErrors>

What we have done here is on an event of 404 we are redirecting to a custom “error404.aspx” page.

Now common practice is to display some decent error messages or links on this page, but we can really do lot more than that, one thing to note here is we are redirecting to an ASPX page and we can do smart coding in the code behind file.

  • In an event of mistyped url we can suggest valid pages (Intelligent 404).
  • In case of deleted / moved page we can redirect smartly to the correct page.

Lets look at how we can implement this.

Intelligent 404

To be able to suggest potential valid pages intelligently,

  • First, we need to know what we are searching for.
  • Second, we need to know the available pages in the website.
  • Third, we need to create uri list from the available file list. 
  • Forth, we need to match the page that we are looking for, in the urilist and suggest the potential uris.

Lets look at all this in code:

This is how we can find out what we are looking for:



.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #006080; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }


    string from = Request.QueryString["aspxerrorpath"];
    int index = from.LastIndexOf("/") + 1;
    string search = from.Substring(index).Replace(".aspx", string.Empty).Replace("-", " ");
 

Then we need the list of available files, the following method filters by file extension and return all the list of files (including files in subfolder). 



.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #006080; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }


    private List<string> GetAllFiles(string directory, string filter, bool getFilesInSubDirs)
    {        
        List<string> filesList = new List<string>();
        
        string[] files = null;
        if (filter == null)
        {
            files = Directory.GetFiles(directory);
        }
        else
        {
            files = Directory.GetFiles(directory, filter);
        }
        filesList.AddRange(files);//add all files in that current folder.
 
        if (getFilesInSubDirs)
        {
            //Check if the current directory has sub-directories
            string[] subDirs = Directory.GetDirectories(directory);
            //if yes, then call recursive functions..
            if (subDirs.Length > 0)
            {
                //now look for all files in current folder's sub-dir's.
                foreach (string subDir in subDirs)
                {
                    List<string> tempList = GetAllFiles(subDir, filter, getFilesInSubDirs);
                    filesList.AddRange(tempList);
                }
            }
        }
        return filesList;
    }  

After getting the files we need to turn them into proper uris. 



.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #006080; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }


    public List<string> GenerateUriList(string directory, string rootUri)
    {
        List<string> filelist = GetAllFiles(directory, "*.aspx", true);
        List<string> uriList = new List<string>(filelist.Count);
        foreach (string str in filelist)
        {
            string uri = str.Replace(directory, rootUri);
            uri = uri.Replace(@"\", "/");            
            uriList.Add(uri);
        }
        return uriList;
    }

We also need to be able to match and find the potential uris, like this,


    bool GetMatchingUri(string search, string uri)
    {
        //case in sensitive match
        string term = search.ToLowerInvariant().Trim();
        string[] terms = term.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
        string regex = string.Format(System.Globalization.CultureInfo.InvariantCulture, "({0})", string.Join("|", terms));
        int result = (Regex.Matches(uri.ToLowerInvariant(), regex).Count);
        return result > 0;     
    }

And finally the Suggest() method which uses all the above codes and makes the 404 more intelligent, and suggests potential Uris. You can call this during OnPage_Load event of your error404.aspx.


    public partial class error404 : System.Web.UI.Page
    {
      protected void Page_Load(object sender, EventArgs e)
      {
        if (Request.QueryString["aspxerrorpath"] != null)
        {
            Suggest();
        }
      }
    }


    private void Suggest( )
    {
        string from = Request.QueryString["aspxerrorpath"];
        int index = from.LastIndexOf("/") + 1;
        string search = from.Substring(index).Replace(".aspx", string.Empty).Replace("-", " ");
        //
        string rootDirectory = Request.PhysicalApplicationPath;
        string rawurl = Request.RawUrl;        
        int index1 = rawurl.LastIndexOf('/');
        //Get the virtual root directory.
        string virtualDirectory = rawurl.Substring(0, index1 + 1);
        
        //Get UriList
        List<string> uriList = GenerateUriList(rootDirectory, "");
        foreach (string uri in uriList)
        {   
            if (GetMatchingUri(search, uri))
            {
                LiteralControl result = new LiteralControl(string.Format("<li><a href=\"{0}\">{1}</a></li>", uri, uri));
                phSearchResult.Controls.Add(result);
            }
        }
    }

here phSearchResult is a ASP.NET Placeholder control.

The end result may look something like this, where I intentionally typed an url (localhost/Albums.aspx) which does not exist, notice how the error404.aspx intelligently suggests the correct urls instead. 

 

Redirecting smartly to the correct Page

Another common responsibility of a Web Admin is to be able to point to the right pages when there is a change/move/rename/delete of existing pages. The referers/search engines may still cache the old url and use it, but the website need to handle this situation and properly redirect to the correct page/pages. In ASP.NET this is again pretty easy, by using the same technique mentioned above we can redirect the page to error404.aspx and handle this in the code behind file.

To achieve this, first thing we need is some sort of database/xml where we can define old and new url mappings.


<?xml version="1.0"?>
<ArrayOfUrlMap xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <UrlMap>
    <OldUrl>?404;http://www.mysite.com/albums.htm</OldUrl>
    <NewUrl>~/Albums.aspx</NewUrl>
  </UrlMap>
  <UrlMap>
    <OldUrl>?aspxerrorpath=/albums.html</OldUrl>
    <NewUrl>~/Albums.aspx</NewUrl>
  </UrlMap>  
</ArrayOfUrlMap>

Note, I have put two types of url, the reason is, IIS and ASP.NET development Server generates different urls. 


 
    string queryString = Request.Url.Query.ToString();
    //result of the above line will be different based on IIS of ASP.NET
    //?404;http://www.mysite.com/albums.htm (generated by IIS)
    //?aspxerrorpath=/resume.html (generated by ASP.NET development Server)

Secondly we need to match the oldUrl with the list in the database/xml and redirect to the right location.


    protected void Page_Load(object sender, EventArgs e)
    {
        string queryString = Request.Url.Query.ToString();
        string match = FindMatchingPage(queryString);
        if (match != null)
        {
          //Set the Response status code to 301 (Moved Permanently)
          //redirect to the new Url
        }
        else
        {
            //Do Something else ie Suggest()
        }
        Response.End();
    } 
    private string FindMatchingPage(string queryString)
    {
      //search in your db/xml
      //if (queryString == oldurl)
      //return matching new url
    }
    
And thats it we are done.
 

Conclusion


In ASP.NET its very easy to handle 404, I have just demonstrated 2 smart ways of handling 404.
I hope this helps and thank you for being with me so far.

Duplicate items are not supported by the "Sources" parameter (VS2005 IDE error)

Problem:
In one of my new project after generating some codes with .NetTiers, when I tried to compile the project in VS2005 IDE it started showing this error:

The item “Audit.cs” was specified more than once in the “Sources” parameter. Duplicate items are not supported by the “Sources” parameter. MyProject.Net.Entities

As you can guess there is not enough information or any direction on how to resolve this issue.


Solution
:
As usual started googling and found some hints that in the csproj file there might be same entry twice. I opened up MyProject.Net.Entities.csproj via Notepad I found the following nodes defined twice (in Nettiers Entities) (if you are not using Nettiers you might have only one item).

<Compile Include=”Audit.cs”>
<SubType>Code</SubType>
</Compile>
<Compile Include=”AuditBase.generated.cs”>
<DependentUpon>Audit.cs</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include=”IAudit.cs”>
<DependentUpon>Audit.cs</DependentUpon>
<SubType>Code</SubType>
</Compile>

I deleted these nodes and made sure only one node exists for each item, and that solved the problem.

Hope this helps.

Databinding using ObservableCollection<T>

Someone asked me today:
I have a text field and a drop down menu, based on the values of these 2 when i click on a button an api call is made to get the results. Now i want to display these results in a ListView using GridView. How can i use ObservableCollection to read the data when the search button is hit.

My Short Reply:

Creating an ObservableCollection is pretty straight forward.

1. For example say we have class Customer { id, name, address }

2. Now lets create a DataSrc that returns an ObservableCollection of Customer
public class CustomerDataSrc
{
 private ObservableCollection<Customer> _results = new
ObservableCollection<Customer>();
 public ObservableCollection<Customer> Customers {get { return _results; }}
 private void LoadCustomers()
 {
   IList<Customer> customers = YourDAL.FindAll();
   foreach (Customer customer in customers)
   {
     _results.Add(customer);
   }
 }
}

3. Declare an ObjectDataProvider in your XAML Page.Resources
<Page.Resources>
 <ObjectDataProvider x:Key=”CustomerDataSrc”
                   d:IsDataSource=”True”
                   ObjectType=”{x:Type Client_DataSources:CustomerDataSrc}”/>
</Page.Resources>

4. Bind the ListView like this:
<ListView x:Name=”dataGrid”
              ItemsSource=”{Binding Path=Customers,
              Mode=Default,
              Source={StaticResource CustomerDataSrc}}”>

5. In the code behind do this:

(change according to the on button click event)


private void Page_Loaded(object sender, RoutedEventArgs e)
{
  ObjectDataProvider odp = this.FindResource(“CustomerDataSrc”) as ObjectDataProvider;
  _customerSrc = odp.ObjectInstance as CustomerDataSrc;
  _customerSrc.IsDesignTime = System.ComponentModel.DesignerProperties.GetIsInDesignMode(this);
  _customerSrc.LoadCustomers();
}




Hope this helps

Releasing the Source Code for the .NET Framework Libraries

Check this out at Scotts Blog:

source: http://weblogs.asp.net/scottgu/archive/2007/10/03/releasing-the-source-code-for-the-net-framework-libraries.aspx

Releasing the Source Code for the .NET Framework Libraries

One of the things my team has been working to enable has been the ability for .NET developers to download and browse the source code of the .NET Framework libraries, and to easily enable debugging support in them.

Today I’m excited to announce that we’ll be providing this with the .NET 3.5 and VS 2008 release later this year.

We’ll begin by offering the source code (with source file comments included) for the .NET Base Class Libraries (System, System.IO, System.Collections, System.Configuration, System.Threading, System.Net, System.Security, System.Runtime, System.Text, etc), ASP.NET (System.Web), Windows Forms (System.Windows.Forms), ADO.NET (System.Data), XML (System.Xml), and WPF (System.Windows).  We’ll then be adding more libraries in the months ahead (including WCF, Workflow, and LINQ).  The source code will be released under the Microsoft Reference License (MS-RL).

You’ll be able to download the .NET Framework source libraries via a standalone install (allowing you to use any text editor to browse it locally).  We will also provide integrated debugging support of it within VS 2008.