Click here to Skip to main content
15,881,413 members
Articles / Web Development / HTML

A Book Store Application Using AngularJS and ASP.NET Web API

Rate me:
Please Sign up or sign in to vote.
4.88/5 (30 votes)
22 Sep 2013MIT5 min read 171.4K   8.7K   81   27
How to build a book store application using AngularJS and ASP.NET Web API
In this article, I will be demonstrating how AngularJS and Web API can be used together using ASP.NET web forms.

Introduction

There are many examples out there demonstrating how AngularJS and Web API can be used together, but almost all of them are in MVC, so I tried to implement this using ASP.NET web forms and this is what I came up with.

For those who are new to AngularJS and Web API, please refer to the following links:

What to Expect

This application demonstrates how to use ASP.NET Web API RESTful services to send and receive data with angularJS. Apart from that you can see how angularJS is very effective in creating applications where we need to do real-time DOM manipulation. Also this is a single page application which utilizes angular views to navigate to different pages.

This application does not contain any kind of user management or any payment processing system as this is outside the boundary of the scope of this article, but you can implement them easily enough if you want to by modifying the existing code.

This application uses a modified version of ToolTipJS which is a small JavaScript library to implement tooltips for web page elements. I have already covered that here.

Image 1

Using the Code

This application uses SQL Server database file to retrieve the books data. I have added controller and data access layers to make the database calls so you can either use them or you can implement your own custom code to connect with your custom data source and you won't need to modify the UI code when doing that.

To get started, you need to create an empty ASP.NET web application and add a new Web API Controller Class. Name that class BookController and add the following code to it:

Image 2

C#
using Controller;
using Shared;
using Shared.Models; 
C#
/// <summary>
/// Get all books
/// </summary>
/// <returns></returns>
public List<BookModel> GetBooks()
{
    CommonController commonController =
          HttpContext.Current.Session["CommonController"] as CommonController;
    List<BookModel> returnData =
    commonController.ExecuteOperation(OperationType.Read, null) as List<BookModel>;

    return returnData;
}

/// <summary>
/// Get book by its id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public List<BookModel> GetBookById(Int32 id)
{
    CommonController commonController =
    HttpContext.Current.Session["CommonController"] as CommonController;
    Dictionary<String, Object> data = new Dictionary<String, Object>();
    data.Add("id", id);
    List<BookModel> returnData = commonController.ExecuteOperation
                       (OperationType.ReadById, data) as List<BookModel>;

    return returnData;
}

/// <summary>
/// Get all books of the required category.
/// </summary>
/// <param name="category"></param>
/// <returns></returns>
public List<BookModel> Post([FromBody] String category)
{
    CommonController commonController =
    HttpContext.Current.Session["CommonController"] as CommonController;
    Dictionary<String, Object> data = new Dictionary<String, Object>();
    data.Add("category", category);
    List<BookModel> returnData = commonController.ExecuteOperation
    (OperationType.ReadByCategory, data) as List<BookModel>;

    return returnData;
}

Image 3

The methods in the above code are:

  • GetBooks(): Retrieves all the books from the database
  • GetBookById(Int32 id): Retrieves the book information belonging to the Id provided
  • Post([FromBody] String category): Retrieves all the books belonging to the category provided

When we call the Web API through its url, then ASP.NET does semantic analysis of the request to resolve the action, this has both its advantages and disadvantages. The disadvantage is obviously the fact that we cannot freely set method names in our controller class, but I am assuming they want us to have one controller per entity so I think that makes some sense.

Next, we need to set the route in the Global.asax file to utilize our Web API controller class. In this application, I have used session in the Web API controller, although this should not be done as it will defeat the whole purpose of having RESTful services but I did that just to add an extra feature in the code.

Add the following code to the Global.asax file (you need to add one if it is not already in your solution):

C#
#region Classes To Provide Session Support For WebApi
       public class MyHttpControllerHandler : HttpControllerHandler, IRequiresSessionState
       {
           public MyHttpControllerHandler(RouteData routeData)
               : base(routeData)
           { }
       }

       public class MyHttpControllerRouteHandler : HttpControllerRouteHandler
       {
           protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
           {
               return new MyHttpControllerHandler(requestContext.RouteData);
           }
       }
       #endregion Classes To Provide Session Support For WebApi

       protected void Application_Start(object sender, EventArgs e)
       {
           RouteTable.Routes.MapHttpRoute(
               name: "DefaultApi",
               routeTemplate: "api/{controller}/{id}",
               defaults: new { id = RouteParameter.Optional }
           ).RouteHandler = new MyHttpControllerRouteHandler();
       }

Let's now move to the AngularJS part. First, let's implement angular $routeProvider to configure routes for our views. Add a new JavaScript file and name it app.js, then add the following code to this file:

JavaScript
;var bookApp = angular.module('bookStore', []);
bookApp.config(['$routeProvider', function ($routeProvider) {
    $routeProvider.when('/books', {
        templateUrl: 'books.html',
        controller: MainCtrl
    });
    $routeProvider.when('/book-detail/:bookId', {
        templateUrl: 'book-detail.html',
        controller: BookCtrl
    });
    $routeProvider.otherwise({ redirectTo: '/books' });
}]);

Next, we need to add angularJS controllers. There are two routes and views in this application so I have used two controllers, one is MainCtrl (for all the book data) and another is BookCtrl (for individual books).

Add a new JavaScript file Controllers.js and add the following code to this file:

JavaScript
;function MainCtrl($scope, $http, $templateCache) {
    $scope.books = [];
    var tooltipJS = new ToolTipJS();
    //Set the tooltip html content
    var tooltipContent = "<div style='text-align:center'><table>"
    tooltipContent += "<span style='font:bold;font-family:Arial;
                       font-weight:800;font-size:large'>{{Name}}</span><br />";
    tooltipContent += "<tr><td>Author</td><td>{{AuthorName}}</td></tr>";
    tooltipContent += "<tr><td>Publisher</td><td>{{PublisherName}}</td></tr>";
    tooltipContent += "<tr><td>Price</td><td>{{Price}}</td></tr>";
    tooltipContent += "<tr><td>Discount</td><td>{{Discount}}</td></tr>";
    tooltipContent += "<tr><td>Language</td><td>{{Language}}</td></tr>";
    tooltipContent += "<tr><td>Publication Year</td><td>{{PublicationYear}}</td></tr>";
    tooltipContent += "<tr><td>ISBN-13</td><td>{{ISBN13}}</td></tr>";
    tooltipContent += "<tr><td>ISBN-10</td><td>{{ISBN10}}</td></tr></table></div>";
    
    //set the tooltip location preference, these can be reordered as required
    tooltipJS.addLocationPreference(new tooltipJS.tooltipLocation
                         (tooltipJS.LocationConstants.Top, "tooltip-Css"));
    tooltipJS.addLocationPreference(new tooltipJS.tooltipLocation
                         (tooltipJS.LocationConstants.Right, "tooltip-Css"));
    tooltipJS.addLocationPreference(new tooltipJS.tooltipLocation
                         (tooltipJS.LocationConstants.Left, "tooltip-Css"));
    tooltipJS.addLocationPreference(new tooltipJS.tooltipLocation
                         (tooltipJS.LocationConstants.Bottom, "tooltip-Css"));
 
    //first let's get all the books
    $http({
        method: 'GET',
        url: 'api/book/',
        cache: $templateCache
    }).
    success(function (data, status, headers, config) {
        $scope.books = data;
    }).
    error(function (data, status) {
        console.log("Request Failed");
    });
       
 
    //Get Books by their category
    $scope.GetBooksByCategory = function (category) {
        $http({
            method: 'POST',
            url: 'api/book/',
            data: JSON.stringify(category),
            headers: { 'Content-Type': 'application/json; charset=utf-8', 'dataType': 'json' },
            cache: $templateCache
        }).
        success(function (data, status, headers, config) {
            $scope.books = data;
        }).
        error(function (data, status) {
            console.log("Request Failed");
        });
    };
 
    //set the tooltips for all the book images
    $scope.SetToolTip = function (id, name, author, publisher, price, 
                                  discount, language, year, isbn13, isbn10) {
        var content = tooltipContent;
        content = content.replace("{{Name}}", name);
        content = content.replace("{{AuthorName}}", author);
        content = content.replace("{{PublisherName}}", publisher);
        content = content.replace("{{Price}}", price);
        content = content.replace("{{Discount}}", Math.round(discount * 100) + "%");
        content = content.replace("{{Language}}", language);
        content = content.replace("{{PublicationYear}}", year);
        content = content.replace("{{ISBN13}}", isbn13);
        content = content.replace("{{ISBN10}}", isbn10);
 
        tooltipJS.applyTooltip("imgBook" + id, content, 5, true);
    };
 
    //Get our helper methods
    $scope.GetRatingImage = GetRatingImage;
    $scope.GetActualPrice = GetActualPrice;
    $scope.HasDiscount = HasDiscount;
}
 
function BookCtrl($scope, $http, $templateCache, $routeParams) {
    $scope.bookId = $routeParams.bookId;
    $scope.book = {};
 
    $http({
        method: 'GET',
        url: 'api/book/' + $scope.bookId,
        cache: $templateCache
    }).
    success(function (data, status, headers, config) {
        $scope.book = data[0];
    }).
    error(function (data, status) {
        console.log("Request Failed");
    });
 
    //Get our helper methods
    $scope.GetRatingImage = GetRatingImage;
    $scope.GetActualPrice = GetActualPrice;
    $scope.HasDiscount = HasDiscount;
}
 
//Gets rating image based on the rating value passed
function GetRatingImage(rating) {
    switch (rating) {
        case 0:
            return "0star.png";
            break;
        case 1:
            return "1star.png";
            break;
        case 2:
            return "2star.png";
            break;
        case 3:
            return "3star.png";
            break;
        case 4:
            return "4star.png";
            break;
        case 5:
            return "5star.png";
            break;
    }
}
 
//Gets the actual price after deducting the discount
function GetActualPrice(price, discount) {
    var discountString = Math.round(discount * 100) + "%";
    var finalPrice = price - (price * discount)
    if (discount > 0) {
        return "Rs. " + Math.round(finalPrice) + "(" + discountString + ")";
    }
    else {
        return "";
    }
};
 
//Determines if there is any discount for the book or not
function HasDiscount(discount) {
    return (discount > 0);
}; 

Next, we need to create the HTML pages for our views. There are two views in this application and we will add separate HTML files for them.

Add a new HTML file and name it books.html. This will contain the HTML markup to display the list of available books in the database. Initially, the Angular controller loads all the books data from our database, we can later filter that data according to different categories. Add the following code to this file:

HTML
<div id="divButtonPane" class="divButtonPane">
    <a href ="" class="blueButton" ng-click="GetBooksByCategory('romance')">ROMANCE</a>
    <a href ="" class="redButton" ng-click="GetBooksByCategory('thriller')">THRILLER</a>
    <a href ="" class="purpleButton" ng-click="GetBooksByCategory('classics')">CLASSICS</a>
    <a href ="" class="greenButton" ng-click="GetBooksByCategory('fantasy')">FANTASY</a>
    <a href ="" class="yellowButton" ng-click="GetBooksByCategory('mystery')">MYSTERY</a>
    <a href ="" class="pinkButton" ng-click="GetBooksByCategory('science fiction')">SCI-FI</a>
</div>
<div id="divBody" class="divBody">
    <div style="width:80%">
        <ul style="list-style-type:none;">
            <li ng-repeat="book in books">
                <table>
                    <tr>
                        <td style = "width:20%;padding:20px;text-align:center;">
                            <img ng-src="Resources/Images/{{book.Image}}" width="200px" 
                             height="300px" tooltipid= "imgBook{{book.Id}}"/>
                            {{SetToolTip(book.Id, book.Name, book.AuthorName, 
                              book.PublisherName, book.Price, book.Discount, 
                              book.Language, book.PublicationYear, book.ISBN13, book.ISBN10)}}
                        </td>
                        <td style = "width:80%;padding:20px;text-align:left;">
                            <a ng-href = '#/book-detail/{{book.Id}}'>
                                <span style="font:bold;font-family:Arial;font-weight:800;
                                 font-size:xx-large">{{book.Name}}</span> 
                            </a><br />
                            <span style="font:bold;font-family:Arial;font-weight:400;
                                         font-size:small;color:gray;">
                                By: {{book.AuthorName}}
                            </span><br />
                            <img ng-src="Resources/Images/{{GetRatingImage(book.Rating)}}" 
                                 height="17px" width="90px"/>                            
                            <hr />
                            <table>
                                <tr>
                                    <td>
                                        <span class="boldFont800" 
                                        class="discount-{{HasDiscount(book.Discount)}}">Rs. 
                                        {{book.Price}}</span>
                                        <span class="boldFont800">{{GetActualPrice
                                        (book.Price, book.Discount)}}</span><br />
                                        <span style="font:bold;font-family:Arial;color:green;
                                        font-weight:800;">In Stock</span><br />
                                        <span style="font:bold;font-family:Arial;color:gray;
                                        font-size:small">
                                            Delivered in 2-3 business days.
                                        </span><br />
                                        <a href ="#" class="buyNowButton">BUY NOW</a>
                                    </td>
                                    <td style="vertical-align:top;">
                                        <span class="boldFont800">Publisher: </span>
                                        <span class="boldFontGray800">{{book.PublisherName}}
                                        </span><br />
                                        <span class="boldFont800">Released: </span>
                                        <span class="boldFontGray800">{{book.PublicationYear}}
                                        </span><br />
                                    </td>
                                </tr>
                            </table>                                    
                        </td>
                    </tr>
                </table>                                             
            </li>                      
        </ul>                   
    </div>            
</div> 

Add another HTML file and name it book-detail.html. This will contain the HTML markup to show individual books that we select. Add the following code to this file:

HTML
<div id="divBookBody" class="divBookBody">
    <table>
        <tr>
            <td style="padding:5px;">
                <img src="Resources/Images/{{book.Image}}"/>
            </td>
            <td>
                <span style="font:bold;font-family:Arial;font-weight:800;
                font-size:xx-large">{{book.Name}}</span><br />
                <img src="Resources/Images/{{GetRatingImage(book.Rating)}}" 
                height="17px" width="90px"/>
                <hr />
                <span class="boldFont800">Author: </span>
                <span class="boldFontGray800">{{book.AuthorName}}</span><br />
                <span class="boldFont800">Publisher: </span>
                <span class="boldFontGray800">{{book.PublisherName}}</span><br />
                <hr />
                <span style="font:bold;font-family:Arial;font-weight:800;" 
                class="discount-{{HasDiscount(book.Discount)}}">Rs. {{book.Price}}</span>
                <span class="boldFont800">{{GetActualPrice(book.Price, book.Discount)}}
                </span><br />
                <span class="boldSmallFontGray300">
                    Inclusive of all taxes.
                </span><br /><br />
                <span class="boldSmallFontGray300">
                    Free home delivery if total order amount is Rs. 1000 or above. 
                    Add Rs. 100 otherwise.
                </span><br />
                <table>
                    <tr>
                        <td>
                            <a href ="#" class="buyNowButton">BUY NOW</a>
                        </td>
                        <td>
                            <span style="font:bold;font-family:Arial;font-weight:800;
                             color:green;font-size:large">
                                In Stock
                            </span><br />
                            <span class="boldSmallFontGray300">
                                Delivered in 2-3 business days.
                            </span>
                        </td>
                    </tr>
                </table>                
            </td>
        </tr>
    </table>
    <hr />
    <span style="font:bold;font-family:Arial;font-weight:800;">
     Summary Of The Book:</span><br />
    <span class="boldSmallFontGray300"">
        {{book.Details}}
    </span>
    <hr />
    <span style="font:bold;font-family:Arial;font-weight:800;">Specifications Of The Book:
    </span><br />
    <table style="border:1px solid gray;">
        <tr>
            <td style="padding:5px;">
                <span class="boldSmallFontGray300">Author</span>
            </td>
            <td style="padding:5px;">
                <span class="boldSmallFontGray300">
                    {{book.AuthorName}}
                </span>
            </td>
        </tr> 
        <tr>
            <td style="padding:5px;">
                <span class="boldSmallFontGray300">Publisher</span>
            </td>
            <td style="padding:5px;">
                <span class="boldSmallFontGray300">
                    {{book.PublisherName}}
                </span>
            </td>
        </tr>
        <tr>
            <td style="padding:5px;">
                <span class="boldSmallFontGray300">Publication Year</span>
            </td>
            <td style="padding:5px;">
                <span class="boldSmallFontGray300">
                    {{book.PublicationYear}}
                </span>
            </td>
        </tr>   
        <tr>
            <td style="padding:5px;">
                <span class="boldSmallFontGray300">ISBN-13</span>
            </td>
            <td style="padding:5px;">
                <span class="boldSmallFontGray300">
                    {{book.ISBN13}}
                </span>
            </td>
        </tr>   
        <tr>
            <td style="padding:5px;">
                <span class="boldSmallFontGray300">ISBN-10</span>
            </td>
            <td style="padding:5px;">
                <span class="boldSmallFontGray300">
                    {{book.ISBN10}}
                </span>
            </td>
        </tr>   
        <tr>
            <td style="padding:5px;">
                <span class="boldSmallFontGray300">Language</span>
            </td>
            <td style="padding:5px;">
                <span class="boldSmallFontGray300">
                    {{book.Language}}
                </span>
            </td>
        </tr>            
    </table>
    <hr />
</div> 

Now, we just need to add the master and the default.aspx pages for our application. Add a new master page to the solution and add the following code to it:

HTML
<%@ Master Language="C#" AutoEventWireup="true" 
    CodeBehind="Site1.master.cs" Inherits="BookStore.Site1" %>
 
<!DOCTYPE html>
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <script src ="http://code.jquery.com/jquery-latest.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js">
    </script>
    <script type="text/javascript" src = "Scripts/App.js"></script>
    <script type="text/javascript" src="Scripts/Controllers.js"></script>  
    <script type="text/javascript" src="Scripts/tooltip.js"></script> 
    <link rel="stylesheet" href="Style/StyleSheet.css" />
    <title></title>
    <asp:ContentPlaceHolder ID="head" runat="server">
    </asp:ContentPlaceHolder>
</head>
<body ng-app="bookStore">
    <form id="form1" runat="server">
    <div id="divHeader" class="divHeader">
        <span class="logoText">
            Online Book Store
        </span><br />
        <span style="font-family:Arial;font-size:small;font-weight:400;color:whitesmoke">
            Made by using AngularJS and Asp.Net Web API
        </span>
    </div>
    <div style="height:5px;background-color:white;width:100%;"></div>
 
    <asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
        
    </asp:ContentPlaceHolder>
    <div style="height:5px;background-color:white;width:100%;"></div>
    <div id="divFooter" class="divFooter">
        <span style="font-family:Arial;font-size:small;font-weight:400;color:whitesmoke">
            Add some stuff here...
        </span>
    </div>
    </form>
</body>
</html>

Next add a new web form using the master page we just added. Add the following code to the Default.aspx file:

HTML
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" 
     Inherits="BookStore.Default" MasterPageFile="~/Site1.Master" %>
<asp:Content ID="content1" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
<div ng-view>
    
</div>    
</asp:Content>      

And lastly, add this code to Site1.Master.cs file:

C#
protected void Page_Load(object sender, EventArgs e)
        {
            #region Initialize Controller
 
            CommonController commonController;
            if (Session.IsNewSession)
            {
                commonController = new CommonController();
                Session.Add("CommonController", commonController);
            }
 
            #endregion Initialize Controller
        } 

In the above code, I am adding an instance of the controller object to the session so that it can be later used anywhere we want.

Image 4

That was all about the UI using angularJS, ASP.NET web forms and Web API. On the server side, I have created a small scalable architecture to facilitate data access decoupled from the user interface. I am providing an overview of the different layers of this application and leaving you guys to explore the code in the sample provided here.

Image 5

  • Controller: The UI fetches an instance of the controller object from the session and then uses that controller to retrieve data.
  • DataAccess: This is used to communicate with the database.
  • Shared: This contains all the common data that is shared between different code layers.

Points of Interest

This code can be further improved by injecting a common data service into the Angular controllers. That data service can then be used to share data between different controllers and also to send or retrieve data from the server.

Conclusion

Use this code only as a reference or as a starter kit for your applications if you ever need to, because this code requires a lot of tweaks for production use. Feel free to provide your suggestions or bugs/errors that you encounter while using this code.

History

  • 22nd September, 2013: Initial version

License

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


Written By
Software Developer (Senior)
India India
Just a regular guy interesting in programming, gaming and a lot of other stuff Smile | :)

Please take a moment to visit my YouTube Channel and subscribe to it if you like its contents!
My YouTube Channel

Don't be a stranger! Say Hi!!

Cheers!

Comments and Discussions

 
QuestionOnline Book store using Anjular JS with Spring 4, Hibernate 4 Pin
Member 133615479-Oct-17 20:26
Member 133615479-Oct-17 20:26 
SuggestionAjax MVC Application Pin
Jaypalsinh11-Aug-15 3:07
Jaypalsinh11-Aug-15 3:07 
GeneralRe: Ajax MVC Application Pin
Nitij11-Aug-15 3:47
professionalNitij11-Aug-15 3:47 
QuestionNot able to attach database Pin
Member 1162116429-Apr-15 1:54
Member 1162116429-Apr-15 1:54 
GeneralMy vote of 5 Pin
Rocky Nandu6-Feb-15 2:39
Rocky Nandu6-Feb-15 2:39 
QuestionQuestion Pin
vijay r17-Jun-14 16:10
vijay r17-Jun-14 16:10 
SuggestionMy vote of 2 Pin
Triann21-May-14 4:03
Triann21-May-14 4:03 
GeneralRe: My vote of 2 Pin
Nitij28-May-14 19:06
professionalNitij28-May-14 19:06 
QuestionGetting blank space Pin
Isha Agarwal20-May-14 0:37
Isha Agarwal20-May-14 0:37 
AnswerRe: Getting blank space Pin
Nitij28-May-14 19:10
professionalNitij28-May-14 19:10 
GeneralRe: Getting blank space Pin
Emrah Zengin11-Jun-14 14:31
Emrah Zengin11-Jun-14 14:31 
The reason you are getting blank space is because of a typo in app.js.

You should change:
$scope.GetRatingImage = GetRatingImage; ==> to ==> $scope.GetRatingImage = getRatingImage;

Because the function name starts with "g" not capital "G".
GeneralRe: Getting blank space Pin
Nitij17-Jun-14 19:26
professionalNitij17-Jun-14 19:26 
QuestionSQL Server 2008 Pin
robbyram6-Mar-14 8:33
robbyram6-Mar-14 8:33 
AnswerRe: SQL Server 2008 Pin
Nitij11-Mar-14 6:14
professionalNitij11-Mar-14 6:14 
SuggestionNeeds cleaning Pin
Thorhallur Kristjansson6-Feb-14 17:26
Thorhallur Kristjansson6-Feb-14 17:26 
GeneralRe: Needs cleaning Pin
Nitij14-Feb-14 5:24
professionalNitij14-Feb-14 5:24 
QuestionBug Pin
dswersky24-Jan-14 4:52
dswersky24-Jan-14 4:52 
AnswerRe: Bug Pin
Nitij25-Jan-14 17:42
professionalNitij25-Jan-14 17:42 
GeneralMy vote of 3 Pin
tyaramis22-Nov-13 2:56
tyaramis22-Nov-13 2:56 
GeneralRe: My vote of 3 Pin
Nitij24-Nov-13 20:49
professionalNitij24-Nov-13 20:49 
Question[My vote of 2] Could have been better Pin
msdevtech5-Nov-13 23:12
msdevtech5-Nov-13 23:12 
AnswerRe: [My vote of 2] Could have been better Pin
Nitij6-Nov-13 3:02
professionalNitij6-Nov-13 3:02 
QuestionVery Nice - 5 Pin
rhubka5-Nov-13 8:38
professionalrhubka5-Nov-13 8:38 
GeneralRe: Very Nice - 5 Pin
Nitij5-Nov-13 16:23
professionalNitij5-Nov-13 16:23 
GeneralMy vote of 5 Pin
Anurag Gandhi24-Sep-13 10:23
professionalAnurag Gandhi24-Sep-13 10:23 

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.