Click here to Skip to main content
13,252,933 members (50,661 online)
Click here to Skip to main content
Add your own
alternative version

Stats

24.1K views
53 bookmarked
Posted 8 Sep 2016

Skip logfiles - Try automated exception handling!

, 8 Sep 2016
Rate this:
Please Sign up or sign in to vote.
codeRR is an open source error handling service. It includes the context information that you forgot to include when you logged/reported the exception.

Introduction

This article is from a usage perspective. It will guide you trough all the features and show how you can get started with your own application. 

codeRR is an open source service used to detect and analyze exceptions. It works over unstable network connections and even behind corporate proxies. It attaches context information to each reported exception, which makes it significantly easier to identify and correct the cause of the exception.

Background

I've been programming for over 20 years. During that time, error management has been a recurring problem. Especially if your application is a windows/desktop application. User error reports tend to be sparse with not enough details to be able to reproduce the error. You either have to make educated guesses or just do monkey testing to identify the root cause.

The worst problem is when think that you have found the cause and correct it. But later you receive a new error report for the same problem. What you have done then is to actually worsen the code base.

The de facto standard for error reporting have been logging. But you know how it is, we try to be good at logging and include enough details. But haven't we missed that important field that would have told us why the exception was thrown?

Or look at it from the user perspective. If you get an error in somebody elses application, how often to try to analyze the error? Do you create a detailed error report and contact the application manufacturers support department? Guess not. Thus once you get an error report you can be sure that several other users have already experienced the same error.

codeRR is an attempt to solve these problems. codeRR is not reliant on bug reports from users or on log files. codeRR contact you when a user experience a new unique exception and you typically get all information you need to be able to start working on a bug fix without having to do any analysis first.

Feature tour

Let's start by going through all the features. The last chapter in the article contains a step-by-step guide to get started.

codeRR consists of a client (nuget packages) and a server (IIS web application). The client detects exceptions in your application, collect context information and uploads everything to the server.

Let's report an excpetion through a simple console program to see what kind of bells and whistles we get.

class Program
{
    static void Main(string[] args)
    {
        // Initialization
        var url = new Uri("http://localhost/coderr/");
        Err.Configuration.Credentials(url, "yourAppKey", "yourSharedSecret");

        try
        {
            throw new InvalidOperationException("Hello world");
        }
        catch (Exception ex)
        {
            Err.Report(ex);
        }
    }
}

When that code is run the exception will be uploaded to the server. When you go to your codeRR website you will see something like this:

Our dashboard

The dashboard gives you an overview over all applications that have been configured in codeRR.

The information presented:

Name Description
Incident An aggregation of all reported exceptions that have been identified as the same error.
Active incidents Incidents which have not been solved or ignored.
Report An uploaded error report (corresponds to a logged exception)
Users Waiting Users that have entered their email address when they got the error page. i.e. they want to get status updates and can be contacted for further analysis.
Feedback  Number of users that have written error descriptions (what they did when the error ocurred).

Let's click on the incident to get more information about that specific error.

Incidents

Incidents are used to group error reports together. Unlike log libraries, codeRR will not generate 5000 different errors if you receive the same exception 5000 times. Instead codeRR identifies that the received report is for the same exception as a previous report. Both reports are grouped together under an incident. ISO 20000 defines an incident as;

Quote:

unplanned interruption to a service, a reduction in the quality of a service or an event that has not yet impacted the service to the customer.

.. which fits well with what your user(s) experience when an exception is thrown.

incident page

The incident page contains information about a specific error. From here you can browse all reports that we've received for the error. In the above screenshot I've run my sample application one more time get a total of two reports listed under the incident.

There are two actions that you can take on incidents:

  • Close incident - Mark it as solved/corrected (i.e. it should not happen again)
  • Ignore incident - Do not store any more reports or generate notifications for this incident

When you ignore an incident, all new reports will be thrown away. The report counter will continue to grow to allow you to see that reports are still being received. The new reports will however not be analyzed or stored.

When you close an incident you can also write a message which will be distributed to all users that are waiting on a new version. It's all done through codeRR and only to users that signed up to status notifications when the exception was thrown.

Below are some of the analysis features that have been implemented in codeRR to date.

Tags

If you look at the screen shot above you'll notice the console-application tag. codeRR identifies a number of StackOverflow tags for incoming reports. If you click on a tag you'll be directed to a search for the exception message filtered for that tag only.

Showcase of a StackOverflow.com search

Error origins

Error origins shows where error reports are received from. The information is presented as a heat map. Thus you can easily identify if the error is more frequently occuring in a certain region. That indicates that the error is related to localization or cultural issues.
 

A map displaying all error origins

The pins will be replaced with heatmaps once a larger number of reports have arrived.

Context data

The context data under the incident is a combination of aggregation and specialization. Collections are first specialized, for instance the web browser "User-Agent" string is extracted to it's own collection. Once done the 
information is aggregated and then compared. codeRR can therefore tell you for instance if 99% of the error reports for an incident is for the culture "sv-SE", or if there was less then 100Mb memory available in the operating system for all reports.
 

context data example

Feedback / Error descriptions

codeRR provides a way for users to leave an error description when an exception is caught. i.e. they can describe what they did when the exception occurred. They can also leave their email address to get a notification when the incident is closed.

Here is the built in error form for WinForms-projects:

Feedback form

You can also collect the information yourself like this:

//last param is email address
var feedback = new UserSuppliedInformation("I pressed the 'any' key.", null); 
Err.LeaveFeedback(errorIdFromReport, feedback);

Once uploaded the feedback will be available under the incident:
Screenshot from feedback in the website

The more feedback your users provide, the easier to identify and correct the issue.
 

Error reports

Incidents are a great way to get an overview of which different errors your application have, including information like how often they occur and what they have in common. However, it's sometimes better to get into the details of each specific error report to understand why the exception was thrown.

Therefore you can click of any of the reports in the list under the incident to take a peek.

Screenshot from a specific error report (exception occurrence)

Notifications

Ever wanted to get informed directly when an exception is thrown in your application?

codeRR supports multiple types of notifications. You can either get a text to your cell phone or an email message.

These are the notifications that codeRR supports:
 

user notification options

Application versions

codeRR can track application versions to see if the same error have existed in multiple versions or if it has resurfaced a couple of versions later.

To activate the feature you need to go to the administration area and select with Assembly you bump the version number in.

Selects correct assembly in admin area

You also need to make sure that you have specified a version in your project:

assemblyversion

So once an exception have been reported, the incident will be tagged with the application version:

Incident got version tag

If you now bump the assembly version:

changing the assembly version

.. and report the same exception, you'll see that the incident will contain both application versions:

Incident got two version tags.

Client libraries

While the codeRR server analyzes and presents the information, the client libraries are used to detect exceptions and collect context information.

The client library injects itself into your favorite .NET library/framework's pipeline, to be able to collect exceptions and information automatically. However, you can also report exceptions yourself if you have try/catch blocks.

The libraries that we have built so far is:

Name Description
Coderr.Client Base library. This library is enough if you want to report exceptions by yourself.
Core.Client.NetStd Library for .NET standard (v1.6 and v2.0).
Coderr.Client.AspNet

Generic ASP.NET library. Catches all unhandled exceptions and report them.

Collects information about the HTTP request, session data etc. Allows you to easily create custom error pages for different HTTP error codes.

Coderr.Client.AspNet.Mvc5

ASP.NET MVC5 specific library.

Does the same as the ASP.NET library, but do also collect specific MVC5 information like route data, ViewBag etc. Also allows you to customize your error pages by just creating razor views in the error view folder.

Coderr.Client.AspNet.WebApi2

Library for ASP.NET WebApi 2.

Tracks exceptions, invalid model states, failed authorization attempts, missing APIs. Collects all information available, both in the ASP.NET pipeline and in WebApi. For instance models, request, routedata and more.

Coderr.Client.AspNetCore.Mvc

Library for ASP.NET Core MVC.

Tracks exceptions, invalid model states, failed authorization attempts, missing pages. Collects all information available, both in the OWIN pipeline and in MVC. For instance viewbag, request, viewdata, viewbag, routedata and more.

Coderr.Client.Wcf

Catches unhandled exceptions in the WCF pipeline.

Collects WCF specific information like the inbound WCF message that failed to be processed.

Coderr.Client.Log4Net Reports all exceptions that you log, including the error message that you wrote.
Coderr.Client.WinForms

Reports all unhandled exceptions.

Can take screen shots and collect the state of all open forms.

Coderr.Client.WPF

Reports all unhandled exceptions.

Can take screen shots and collect the state of all open forms.

It's also possible to report exceptions directly by your own library/application. The client specification (HTTP/JSON) is available in the online documentation.

Below is more about what some of our libraries can do.

Core library

This library is the foundation of all exception reporting done with codeRR. All other client libraries are based upon this one.

With the core library you have to report exceptions manually like this:

try
{
    doSomething();
}
catch (Exception ex)
{
    Err.Report(ex);
}

You can also attach different kinds of context information which we'll get back to in the 'Getting started' chapter.

Context collections

A context collection is a set of properties which is fetched from a specific source. A context collection can be HTTP Headers, a view model, Route data etc.

There are few built in context collections in the core library. Some are added to the pipeline per default, which means that they are included every time an exception is reported. Other collections require that you to add them manually when configuring the client library.

If you like, you can also create and include your own easily.

Application information

This collection includes information about your process. It contains information like used memory, thread count and amount of used CPU.

Assemblies

All assemblies that have been loaded into the AppDomain and their versions.

Exception information

Information from the exception, including all properties. If you have ever used EntityFramework and got the DbEntityValidationException?

It's exception message just says:

Quote:

System.Data.Entity.Validation.DbEntityValidationException: Validation failed for one or more entities. See 'EntityValidationErrors' property for more details

When you see that in a logfile, do you know the reason to why you got the exception?

Since codeRR includes all exception properties, you get the validation information directly.

Below is a subset of all information from the DbEntityValidationException exception. As you can see, you both the values of all properties in your EF entity, and you also get all validation failures.

Entity Framework properties

 

File versions

File versions for all assemblies, as the assembly version might be the same while the file version is higher (GAC hell anyone?).

Operating system

Does the error happen on all windows editions or just some of them?

System information

Is the amount of available RAM memory a show stopper?

Thread information

Thread information like current UI culture.

Which thread crashed? Hint: Name threads if you start them yourself

ASP.NET

This library is intended for ASP.NET projects which are not using MVC. So it works great for libraries based on ASP.NET such as Nancy.

Context collections

These collections are generated by the ASP.NET library.

HTTP headers

All HTTP headers for the request that failed.

Uploaded files

Name, size and context type of all files that was uploaded in the request.

Form

All items in the HTTP form (name and value).

QueryString

Query string variables (key and value).

Session

All session items (support for complex objects).

Error pages

The library contains a custom error page which can be activated (and shown) when an exception is caught.

To activate it add the following code:

var provider = new VirtualPathProviderBasedGenerator("~/Errors/");
Err.Configuration.SetErrorPageGenerator(provider);

The code says that the library should look after error pages (either .aspx or .html) in the specified folder:

The library tries to find pages based on the HTTP code. If the code is 404 the library tries to find the following error pages:

  1. FileNotFound.aspx
  2. FileNotFound.html
  3. Error.aspx
  4. Error.html

That is, it tries to find the most specific file first. If a specific file do not exist it tries to load a generic one.

Error information

To get information into your view, simply declare one of the following properties in your code behind.

Property Type Description
ErrorContext HttpErrorReporterContext Look in the Client API specification for more information
Exception Exception The caught exception

 

Example

public partial class NotFound : System.Web.UI.Page
{
	protected void Page_Load(object sender, EventArgs e)
	{

	}

	public Exception Exception { get; set; }

	public HttpErrorReporterContext ErrorContext { get; set; }
}

Then simply display the error information in your HTML:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="NotFound.aspx.cs" Inherits="codeRR.Client.AspNet.Demo.Errors.NotFound" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Failed to find <%= Exception.Message %></title>
</head>
<body>
	<form method="post" action="$URL$">
		<input type="hidden" value="$reportId$" name="reportId" />
        <div>
            Page is not found
        </div>
        <div>
            <%= ErrorContext.HttpStatusCode  %>
        </div>
		<div>
			<p>Could you please let us know how to reproduce it? Any information you	 give us will help us solve it faster.</p>
			<textarea rows="10" cols="40" name="Description"></textarea>
		</div>
    </form>
</body>
</html>

If you are just using HTML you can use the following template strings:

Template text Description
$ExceptionMessage Exception message
$reportId$ Generated report id
$URL$ codeRR's own url to submit error description, email etc.
$AllowReportUploading$ If you want to allow the user to decide whether the report should be uploaded

 

Example:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>An error occurred</title>
    <meta name="ROBOTS" content="NOINDEX, NOFOLLOW" />
    <meta name="X-powered-with" content="https://coderrapp.com" />
    <style type="text/css">
        /*CssStyles*/
    </style>
</head>
<body>
    <div style="" class="container">
        <div style="width: 100%; text-align: center">
            <h1>Looks like something went wrong!</h1>
        </div>
        <form method="post" action="$URL$">
            <div class="img">
                <img src="/images/Error.jpg" />
            </div>
            <div class="content">
                <p>
                    Thanks for taking the time and letting us know about the issue.
                </p>
                <p>
                    We’re working to fix it for you as fast as we can, apologies for the inconvenience.
                </p>
                <input type="hidden" value="$reportId$" name="reportId" />
                <div class="AllowSubmissionStyle">
                    <p>
                        However, If you allow us to collect additional error information we'll be able to analyze this error much faster.
                    </p>
                    <input type="checkbox" name="Allowed" value="true" checked="$AllowReportUploading$" />
                    I allow thy to collect the additional information.
                </div>
                <div class="AllowFeedbackStyle">
                    <p>Could you please let us know how to reproduce it? Any information you give us will help us solve it faster.</p>
                    <textarea rows="10" cols="40" name="Description"></textarea>
                </div>

                <div class="AskForEmailAddress">
                    <p>Enter your email address if you would like to receive status updates about this error.</p>
                    <input type="text" name="email" placeholder="email address" />
                </div>
                <hr />
                <input type="submit" value="Send report" />
                <a href="/">Back to homepage</a>
            </div>
            <div style="clear: both;"></div>
        </form>
    </div>
</body>
</html>

ASP.NET MVC

This client library will provide both ASP.NET and MVC specific context information for codeRR.

Apart from detecting and uploading uncaught exceptions the library also provide the following features.

Error pages

The library have build in support for error pages.

To use the ones included in the library, add the following in global.asax (after the Err.Configuration.Credentials() line):

Err.Configuration.DisplayErrorPages();

Custom error pages

If our built in pages are not preferable you can include your own views.

Example

@model codeRR.Client.AspNet.Mvc5.CoderrViewModel

<h1>Internal Server Error</h1>
<p>
    We've experienced a malfunction in the core crystal cooling. The ship will explode within five seconds.
</p>

<h3>Reason</h3>
<p>
    @Model.Exception.Message
</p>

Views should be named as the HTTP codes are defined in the HttpStatusCode enum in .NET and be placed in the Views/Errors folder.

The Error.cshtml view is displayed if no other view matches.

ErrorController

If it's not enough to control the error handling through views only you can create your own ErrorController. Create it like any other controller in the Controllers folder.

The action methods should be named like the views. i.e. public ActionResult InternalServer().

The information provided by codeRR is represented as CoderrViewModel. Take it as a parameter to your action methods.

Sample

public class ErrorController : Controller
{
    public ActionResult Index(CoderrViewModel model)
    {
        return View("Error", model);
    }

    public ActionResult NotFound(CoderrViewModel model)
    {
        return View(model);
    }

    public ActionResult InternalServerError(CoderrViewModel model)
    {
        return View(model);
    }

}

Custom formats

An error object will be returned if XML or JSON is requested by the HTTP client. Great for ASP.NET WebApi.

json

{
  "error": { 
	"msg": "The error message", 
	"reportId": "Unique error id"
  }, 
  hint: "Use the report id when contacting us if you need further assistance." 
}

xml

<Error ReportId="Unique error id" hint="Use the report id when contacting us if you need further assistance">
	Error message
</Error>

Context collections

The following collections are provided by the ASP.NET MVC library.

Controller

The controller name is collected.

RouteData

Information about the route that MVC took is collected.

TempData

TempData is collected if set.

Example

TempData["DemoKey"] = new {
		Amount = 20000,
		Expires = DateTime.UtcNow.AddMinutes(5)
};

Result

ViewData / ViewBag

The Viewbag and/or ViewData is collected if specified.

Example

ViewBag.Title = "Hello";
ViewBag.Model = new
{
	state = "Running",
	Collected = true
};

Result

log4net client

The log4net library injects codeRR into the logging pipeline of log4net. Each time you log something and include an exception it will be reported to codeRR . This is by far the easiest way to use the power of codeRR in legacy applications (which use log4net).

Usage

If you have code like this:

try
{
	methodThatWillThrowAnException();
}
catch (Exception ex)
{
	_logger.Warn("Failed doing some crazy stuff.", ex);
}

.. the exception will be picked up in codeRR.

As a bonus you will also see information about the log entry in codeRR:

Winforms

The WinForms client library can help you display error pages and collect information about the open forms.

Error pages

An error page is displayed when an exception is detected. It looks like this per default:

You configure it by using the following properties:

Err.Configuration.UserInteraction.AskUserForDetails = true;
Err.Configuration.UserInteraction.AskUserForPermission = true;
Err.Configuration.UserInteraction.AskForEmailAddress = true;

Examples:

all flags set

only ask for permission

only details

only email

Context information

WinForms have two built in context collections.

OpenForms

codeRR collects information from all open forms using reflection. The information includes all controls and their configuration (position, content, visibility etc)

Let's say that you got this form open:

..which will give you this information:

Screenshots

Screnshots can be activated by one of the following configuration lines:

//only of the active form
Err.Configuration.TakeScreenshotOfActiveFormOnly();

// of all forms            
Err.Configuration.TakeScreenshots();

The context collection will be shown as:

The codeRR pipeline

Here is a small diagram showing what steps codeRR takes to make sure that your exceptions are detected, caught, wrapped with context information and finally uploaded to the service for analysis.

It also identifies significant method calls if you would like to browse the code and get a hang of how things work together. Tip: Right-click in Visual Studio on the code line and use "Find usages" / "Find references" to see where the method call comes from.

(CodeProject have a width limit on images, see the full size image here)

Getting started

A guide to help you report the first error.

Server installation

You can either download the source code for the server, compile and install it, or use the precompiled version. Either way the installation is a xcopy deployment.

  1. Download or compile
  2. Copy the binaries to a new webserver folder, typically c:\inetpub\wwwroot\coderr
  3. Go to IIS and use the context menu choice "Add application.." or "Add website.."
  4. Name it "Coderr" and point on the folder that you created.
  5. Create a new empty database in your SQL server and modify the connection string in web.config.
  6. Browse to the website (using your web browser)
  7. Follow the setup wizard
  8. One the wizard is completed, change the appKey Configured to true in web.config.

The server is now installed.

Creating a new application

Once you have logged in to codeRR server with the account that you created in the previous step, you should be greeted by the application wizard:

Appwizard

Enter an application name and go to the next step.

Installing a client

Our client libraries are used to detect exceptions and collect context information. The purpose is to give you enough information to really understand why the exception happened. We don't want to make assumptions or write a workaround, do we?

select nuget package

Select the correct nuget package for your application and click "Configure".

Configuring your application

To be able to report exceptions you need to tell your application that codeRR should detect exceptions and collect context information. Once that's done codeRR also needs to know where to upload the error reports.

To be able to do that, codeRR needs to be able to identify your application so that the errors are stored correctly in your account.

The next step in the application wizard will help you with that. Below is an example.

configure nuget

The instruction contains the correct appKey/SharedSecret, all you need to do is to copy/paste the information to your application.

The application wizard is only started for the first application, however, you can start it yourself or applications where no errors have been reported yet:

start appwizard manually

Reporting exceptions manually

The easiest way to report an exception is like this:

try
{
    somelogic();
}
catch(SomeException ex)
{
	Err.Report(ex);
}

The exception should appear in your server installation shortly after being reported.

Attaching context information

Usually an exception is not enough information alone to be able to understand why the exception was thrown. codeRR will always collect a large number of parameters for you. You might however have information that directly allows you to understand why the exception was thrown.

That information can be attached when reporting:

try
{
    //some stuff that generates an exception
}
catch (Exception ex)
{
    Err.Report(ex, yourContextData);
}

Using anonymous object

If you need to attach multiple values you can use an anonymous object:

try
{
    //some stuff that generates an exception
}
catch (Exception ex)
{
    Err.Report(ex, new { UserId = userId, UserState = state });
}

Result

Custom collections

We also have an object extension method which can transform any object into a context collection (one of the groups in the "Context Data" menu in our web site).

Below we are using the .ToContextCollection() extension method.

try
{
    <span spellcheck="true">//some stuff that generates an exception</span>
}
catch (Exception ex)
{
    var modelCollection = viewModel.ToContextCollection("ViewModel");
    var loggedInUser = User.ToContextCollection("User");
    Err.Report(ex, new[]{modelCollection, loggedInUser});
}

Result

Hence you can easily attach and group your information just as you like.

Categorize exceptions using tags

We automatically identify common StackOverflow.com tags when analyzing exceptions (to help you find answers by searching StackOverflow.com). You can also add your own tags by adding a special property, named "ErrTags", to any context collection:

try
{
    //some stuff that generates an exception
}
catch (Exception ex)
{
    Err.Report(ex, new { ErrTags = "important,backend" });
}

When that report arrives in the codeRR server you'll see the tags directly on the incident page:

tags

This is a great way to categorize errors.

Summary

codeRR is under constant development. New features arrive weekly. If you want see what's happening, follow us on twitter or facebook.

The server is built using messaging, command/queries, repository pattern, typescript and other goodies. Check it out at github.

Thank you for reading and it would be really awesome to hear what you think in any of the channels mentioned above.

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0

Share

About the Author

jgauffin
Founder 1TCompany AB
Sweden Sweden
Skip logfiles, try automated error handling!

I'm one of the founders of codeRR, a .NET service which takes care of everything related to exceptions, so that you can focus on writing code.

blog | twitter

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 5 Pin
_Asif_3-Nov-17 3:17
professional_Asif_3-Nov-17 3:17 
QuestionSuch a great thing! Pin
Rahman Mahmoodi17-Jun-17 1:05
memberRahman Mahmoodi17-Jun-17 1:05 
AnswerRe: Such a great thing! Pin
Ehsan Sajjad28-Sep-17 5:27
professionalEhsan Sajjad28-Sep-17 5:27 
AnswerRe: Such a great thing! Pin
jgauffin29-Oct-17 10:58
memberjgauffin29-Oct-17 10:58 
QuestionMy Vote Of 5 Pin
vinod.jangle7-Apr-17 6:59
membervinod.jangle7-Apr-17 6:59 
AnswerRe: My Vote Of 5 Pin
jgauffin29-Oct-17 10:59
memberjgauffin29-Oct-17 10:59 
QuestionHiding the Error detection window Pin
Member 1073548412-Mar-17 21:20
professionalMember 1073548412-Mar-17 21:20 
AnswerRe: Hiding the Error detection window Pin
jgauffin29-Oct-17 10:59
memberjgauffin29-Oct-17 10:59 
GeneralRe: Hiding the Error detection window Pin
Member 1073548430-Oct-17 22:10
professionalMember 1073548430-Oct-17 22:10 
QuestionMy Vote of 5 Pin
Sir Zeppa'Man7-Nov-16 0:16
professionalSir Zeppa'Man7-Nov-16 0:16 
GeneralMy vote of 5 Pin
Manuel A Lombardi T25-Oct-16 7:16
memberManuel A Lombardi T25-Oct-16 7:16 
Very nice article, thank you
QuestionWebAPI Pin
TimCirrus13-Sep-16 3:32
memberTimCirrus13-Sep-16 3:32 
AnswerRe: WebAPI Pin
TimCirrus13-Sep-16 3:40
memberTimCirrus13-Sep-16 3:40 
Questionpretty cool Pin
Sacha Barber12-Sep-16 3:53
mvpSacha Barber12-Sep-16 3:53 
QuestionGreat work Jonas Pin
Garth J Lancaster8-Sep-16 15:35
professionalGarth J Lancaster8-Sep-16 15:35 
AnswerRe: Great work Jonas Pin
jgauffin9-Sep-16 9:28
memberjgauffin9-Sep-16 9:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.171114.1 | Last Updated 8 Sep 2016
Article Copyright 2016 by jgauffin
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid