|
 Sunday, April 01, 2007

Handling unexpected exceptions and tracing

I was browsing a site earlier and I came upon the dreaded Yellow Screen of Death (YSOD). It reminded me of when I had to implement some diagnostic tracing for an application that happened to be built prior to my time at the said company.

The Problem

The application was written in ASP.NET 1.1 and was recently upgraded to 2.0. There was a TON of code in the code behind, heck, we could go as far as to say that the majority of the business logic was in the code behind. The validation and error checking routines were nearly non-existant and as of lately the system was encountering a lot of YSOD screens. The users were confused and on top of that the YSOD screen was presenting TOO MUCH info, enabling a malicious user to take that info and use it for evil purposes. I HAD to get rid of these errors, and quick, and I wasnt allowed to turn off the application so I could fix it. I was allowed to take down the app for a few seconds to implement a change and thats it. I had the error report but could not

I had to implement some way of tracking these bugs without taking down the application for good. I did this through the Application_Error event in the Global.asax file.

Background Info on Page Processing

When a ASP.NET Page is processing and it encounters an error, and if it is not handled, it will present the YSOD with all the pertinent information to the user (that is, if no error handling or default error page is defined in the web.Config). In this project, that was the case. No error handling was present.

An ASP.NET page can also catch its own errors if you implement the Page_Error method. Inside of this method you can get the last error that was thrown and do something with it and then move on. Like this:

public void Page_Error(object sender, EventArgs e)
{
   Exception ex = Server.GetLastError();
   /// Do some stuff with the exception, such as logging, etc 

   // Clean  up 
   Server.ClearError();
}

The problem with this method is that you must implement this on every page. You could do this if had a base page class you inherited from, but what if you forgot to inherit from your base page? Your page would still blow up when something went wrong and was not handled.

Enter Global.asax and Application_Error ...

Implementing Application Level Error Handling & Logging

You can implement application level error handling by implementing some handling at the Application level.

Here's some code, explanation is at the bottom.


Global.asax File

<%@ Application Language="C#" %>
<%@ Import Namespace="System.Web.Configuration" %>

<script runat="server">

void Application_Start(object sender, EventArgs e)
{
}

void Application_End(object sender, EventArgs e)
{
}

void Application_Error(object sender, EventArgs e)
{
   // Perform some handling here if necessary, and then redirect to error handling page as defined in the web.Config file.
   CustomErrorsSection section = (CustomErrorsSection)ConfigurationManager.GetSection("system.web/customErrors");
   Server.Transfer(section.DefaultRedirect);
}

void Session_Start(object sender, EventArgs e)
{
}

void Session_End(object sender, EventArgs e)
{
}

</script>


web.Config File

<?xml version="1.0"?>
<configuration>
    <appSettings/>
    <connectionStrings/>
    <system.web>
         <compilation debug="true"/>
         <authentication mode="Windows"/>
         <customErrors defaultRedirect="Oops.aspx" />
         <trace writeToDiagnosticsTrace="true" enabled="true"/>
    </system.web>
    <system.diagnostics>
        <trace autoflush="true">
            <listeners>
               <add name="TraceSiteEventLogListener" type="System.Diagnostics.EventLogTraceListener,System,version=2.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089" initializeData="Test123"/>
            </listeners>
        </trace>
    </system.diagnostics>
</configuration>


Oops.aspx Page

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Web.Configuration;

public partial class Oops : System.Web.UI.Page
{
   protected void Page_Load(object sender, EventArgs e)
   {
      Exception ex = Server.GetLastError();
      string errorInfo = GetFormattedException(ex); 
      Trace.Write(errorInfo);
      Server.ClearError(); 
   }

   /// <summary>
   /// Gets the exception information, recursively. 
   /// </summary>
   /// <param name="ex">The exception that occurred.</param>
   /// <returns>A formatted string that contains the message, exception type, source and stack trace for each exception
   /// that exists within the exception hierarchy.</returns>
   private string GetFormattedException(Exception ex)
   {
      string exceptionString = string.Empty;
      // If we have an inner exception, we need to get that information first. 
      if (ex.InnerException != null)
      {
         exceptionString = String.Format("Inner Exception: {0}{1}", GetFormattedException(ex.InnerException), Environment.NewLine);
      }
      string exceptionFormat = "[{0}] - Message: {1} - Exception Type: {2} - Source: {3} - Stack Trace - {4}";
   
      exceptionString += String.Format(exceptionFormat,
            DateTime.Now,
            ex.Message,
            ex.GetType().FullName,
            ex.Source,
            ex.StackTrace);

      return exceptionString;
   }


}



Oops.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Oops.aspx.cs" Inherits="Oops" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
   <head runat="server">
      <title>Error Page</title>
   </head>
<body>
   <form id="form1" runat="server">
   <div>
      Oops! An error occurred!
   </div>
   </form>
</body>
</html>



Explanation

If an error is thrown, on any page, the Global.asax's Application_Error event catches the error. At that time I redirect the user to the default error page that is defined in the web.Config's customErrors section (Oops.aspx).

In that file (Oops.aspx) I grab the last exception by making a call to Server.GetLastError(). Then I recursively loop through each child exception that the exception has and format the data for viewing purposes.

After the data is formatted I then write it to the Page.Trace, which is set up to write it to the Trace Listeners that are set up in the web.config. I have decided to use the Event Log as my location of writing my trace information to, but this could easily be set up to use the TextWriterTraceListener (or any other listener) as well.

Then at the end, I clean up the exception queue by calling Server.ClearError().

Then, the page is displayed. "Oops! An error occurred!".

Now, open up the event viewer and look inside of the application log. You'll see a source by the name of "Test123" and you'll see the error information inside. You'll also find a plethora of other great event information too.  

For this to all work, we have to set the Trace enabled to true in the web.Config and set the writeToDiagnosticsTrace equal to true as well. Otherwise the Page's Trace will not redirect its trace information to the listeners defined in the web.Config. To turn this off, just set the enabled flag to "false".

Conclusion

The best thing to do is to always capture the error, give the user a meaningfull message such as "First name cannot be blank." But at times there are errors that we don't catch as developers, things slip through the cracks and we miss something, that's life, it happens. In event of such case, we can enable global exception handling, just like we did here. Just in case something "exceptional" happens, we can catch it, log it in the event viewer and not have to worry about displaying vital information to a user through the YSOD.

 

#    Comments [0] |
 Thursday, March 29, 2007

ASP.NET HOWTO: Enable Default Enter Button in ASP.NET 2.0

In certain forms, such as search forms, users enter a value into the search query box, then press the enter button. If you are develolping in ASP.NET, the page will refresh, but nothing will happen. The buttons click event wont fire... UNLESS, you explicity enable a default enter button on the form.

A problem that existed in ASP.NET 1.x was that you could not specify a default enter button unless you provided some custom code. In ASP.NET 2.0 you can do this very easily, unfortunately its not that widely used, much less known.

HOW TO:

The HtmlForm object has a property DefaultButton. This property gets or sets the control that that causes the post back when the ENTER key is pressed.

Example

<body>
   <form id="form1" runat="server" defaultbutton="processRequestButton">
      <div>
         <asp:TextBox AccessKey="c" runat="server" ID="somevalueTextbox"></asp:TextBox>
         <asp:Button runat="server" ID="processRequestButton" Text="Process" OnClick="processRequestButton_Click" />
      </div>
   </form>
</body>

Result

When the user presses the ENTER key, the form will be posted back and the processRequestButton_Click event will be fired.

Simple!

kick it on DotNetKicks.com
#    Comments [4] |
 Tuesday, March 27, 2007

Overcomplicating simple things - ASP.NET AJAX Confirmation Button Extender

Web 2.0, the big hit. Its everywhere, everyone is doing it. Microsoft has made it very easy for us developers to implment some very cool features of AJAX through the ASP.NET Ajax.NET framework. Simply install, drag and drop and deploy, blammo, you're done.

But there in lies the problem. What about developers who take this for granted? This turns our profession into a melting pot of people who think they can code but actually can't do much of anything other than drag and drop. I'm talking about the developers who implement this type of solution and run with it. They have no concern about how it works, why it works the way it does, it just solves the problem. Little did they know, they went through more work getting this done than is required. Continue on...

Introducing the ASP.NET AJAX Confirmation Button Extender...

This extender enhances usability by giving the user a choice of what to do. For example...

I'm all for ease of use, but sometimes even the smartest people can complicate something VERY SIMPLE. We've all suffered from Analysis Paralysis at one point in our career and well, I think the Confirmation Button Extender is a product of that symptom.

Why would I want to use the AJAX>NET framework to implement a simple confirmation box when I could use the OnClientClick with a fraction of the code?

Here's what I'm saying.

AJAX.NET Implementation

<asp:LinkButton runat="server" id="LinkButton1" Text="test" />

<ajaxToolkit:ConfirmButtonExtender ID="cbe" runat="server"
TargetControlID="LinkButton1"
ConfirmText="Are you sure you want to click this?" />

ASP.NET Implementation with JavaScript:

<asp:LinkButton runat="server" ID="testButton" OnClientClick="return confirm('Are you sure you want to click this?');" Text="test" />

These two methods DO THE EXACT same thing. Yes, the markup output is different, but honeslty, the latter of the two is much easier to implement. To implement this in AJAX.NET you have include the dll and js files, blah blah blah.

The problem I have with the AJAX.NET implementation is that its not helping me gain anything. I have to type more code to get the same thing accomplished. I see this as teaching developers its ok to lean heavily on the design time . This is a problem that we've been solving for years in web dev. How to send a confirmation to a user, and it seems, just now, a lot of people are just figuring it out due to AJAX.NET and they think its "the only way", mainly because ... well ... it worked from a drag/drop perspective. Unfortunately it takes more code than necessary and when the developer finally figures out what it does, they usually say "well why did Microsoft implement it that way? This is soo much eaiser doing by using OnClientClick".

My answer: "So you could get it to work, quickly. If developers cant figure it out on their own, then its time abstract the problem out into another level, and thats exactly what they did. Removed the knowledge of how and why, and replaced it with an easy to implement solution."

We call them R.A.D. Tools and sometimes in this industry its a love/hate relationship with them. :)

 

#    Comments [0] |
 Monday, March 26, 2007

error CS0030: Cannot convert type (Login.master)

I ran into an interesting error today and for a minute I didnt understand why it occurred.

Exception
c:\Windows\Microsoft.NET\Framework64\v2.0.50727\Temporary ASP.NET Files\exampleapp\571e5472\336672e2\App_Web_login.master.cdcab7d2.gh4qw4cs.0.cs(115): error CS0030: Cannot convert type 'ASP.login_master' to 'System.Web.UI.WebControls.Login'
http://localhost/exampleapp/login.aspx?ReturnUrl=/exampleapp/default.aspx
/exampleapp/login.aspx


The site ran fine when running under the local cassini web web server but when we pre-compiled the website through our continuous integration system and then dropped it on our test server, everything went hay-wire. This error started popping up.

After a few moments of reviewing the error I figured it out.

The problem

The master page code behind class was called "Login". ASP.NET 2.0 also has a class by the name of Login. The .NET Framework was trying to convert one type (the master page type) to the actual Login class type.

The Fix

You can fix this one of two ways.

1. Give the full type name in the inherits attribute of the master page.

      e.g.: NameSpace.Type

2. Change the code behind class name from "Login" to something like "LoginMaster".

 

Conclusion

As I found (after the fact of solving this) Rahul Soni to say ... "dont give a class the name of 'Login'".

Please note, I've also see this problem with class names that are of the following:

  • Error
  • View
  • Wizard

I'm sure there are more, but these are the ones I've see cause this problem before.

 

 

#    Comments [1] |

IIS 7 - This configuration section cannot be used at this path.

If you're new to IIS 7 (you probably are) you might receive this nice little gem when you first start working with it:

HTTP Error 500.19 - Internal Server Error
Description: The requested page cannot be accessed because the related configuration data for the page is invalid.
Error Code: 0x80070021
Notification: BeginRequest
Module: IIS Web Core
Requested URL: http://localhost:80/ExampleApplication/
Physical Path: C:\inetpub\wwwroot\ExampleApplication\
Logon User: Not yet determined
Logon Method: Not yet determined
Handler: Not yet determined
Config Error: This configuration section cannot be used at this path. This happens when the section is locked at a parent level. Locking is either by default (overrideModeDefault="Deny"), or set explicitly by a location tag with overrideMode="Deny" or the legacy allowOverride="false".
FONT color=#a52a2a>
Config File: \\?\C:\inetpub\wwwroot\ExampleApplication\web.config
Config Source:
  184: 		</modules>
  185: 		<handlers>
  186: 			<remove name="WebServiceHandlerFactory-Integrated"/>

IIS 7 implements "Configuration Locking". This is to help with IIS administration. The IIS Administrator can lock down Configuration Sections, Section Elements and Attributes at the IIS level. This allows the administrator to have a more granular level of control on the system. Read this link to see more about it.

Important Note:  In order make these changes, you must be an administrator on the machine where the config file resides. If its on your local machine, you must be an administrator on your machine. 

Since I'm on my own development machine, and since this is a development machine, I have decided to change the global setting (the config section itselft). To fix this error I had to go to the applicaitonHost.config file and set the overrideModeDefault to "Allow".

Here's how:

Open the applicationHost.config file, located here: %windir%\system32\inetsrv\config\applicationHost.config

In my instance, I need to edit the "handlers" section.

Change this line:

<section name="handlers" overrideModeDefault="Deny" />

To:

<section name="handlers" overrideModeDefault="Allow" />

Thats it. :)


You might receive more errors after you enable this, such as the same error for modules, but just follow the same steps above, and you should be good.

Note: You can accomplish this same thing through the command line by using the appcmd.exe application (%windir%\system32\inetsrv\appcmd.exe). Like this:

%windir%\system32\inetsrv\appcmd unlock config -section:system.webServer/handlers

One more note...

This is not something you'd want to do on a production server unless you're sure you want to enable all appliations to override the section contents. Be sure to read up on some of the docs (explanation of the configuration system) before you make this change on production.

#    Comments [6] |