Click here to Skip to main content
15,867,453 members
Articles / Web Development / XHTML
Article

AJAX grid with ASP.NET 2005

Rate me:
Please Sign up or sign in to vote.
2.94/5 (25 votes)
6 Mar 200712 min read 93.6K   684   43   24
A great AJAX Grid very customizable. In English & Spanish traduction
Screenshot - LeleGrid.gif

ENGLISH'S TRADUCTION


Introduction:

Designing the structure of an Intranet with multiple modules, i came across the "Dynamic Grid's Problem". I really needed a customizable Grid , easy to apply and to use, and the most important thing, with AJAX's technology .
Looking for one in the Web, codeproject, and so many other pages, i didn't find one that satisfied my expectations.
The only thing I found were tiny things which, properly combined, conform what it is today the "LeleGrid"


Links:

http://www.prototypejs.org


Requirements:

1) Have installed SQL Server Express 2005.
2) As usual, nowadays we need the Prototype JS Framework, because I use it as much for AJAX, as for the DOM handling.
In addition, it has one new method and a modified one. Both here describe :
Added method:

I create this new method so as avoid the "up and down" effect when using the functions (toogle, show or hide).
It is used by:
Element.invisible('nombrecontrol','visible') o Element.invisible('nombrecontrol','hidden')

invisible: function(type) {
$(arguments[0]).style.visibility = arguments[1];<br />},

Modified method:

I modified it because if when using the Grid with MasterPages, the ID's control will be rendered by the form: ctl00_contentPagina + ControlName

function $() {<br /> var results = [], element;<br /> for (var i = 0; i < arguments.length; i++) {<br /> element = arguments[i];<br /> if (typeof element == 'string'){<br /> if (document.getElementById(element) != null)<br /> element = document.getElementById(element);<br /> else<br /> element = document.getElementById("ctl00_contentPagina_"+element);<br /> }<br /> results.push(Element.extend(element));<br /> }<br /> return results.length < 2 ? results[0] : results;<br /> }


Operation:

The Grid uses HttpHandler to obtain XML (by using its information) or to eliminate some element of the grid.
These are two archives: DatosGrilla.ashx and EliminarFilaGrilla.ashx.

The XML's format of the DatosGrilla.ashx file is the following:

<Contenido><br /> <Cantidad></Cantidad> Cantidad de Filas devueltas.<br /> <Filas><br /> <Fila> Fila 1<br /> <Celda></Celda> campo 1..<br /> <Celda></Celda> campo 2..<br /> ...<br /> <Celda></Celda> campo N..<br /> </Fila><br /> <Fila> Fila 2<br /> <Celda></Celda><br /> <Celda></Celda><br /> <Celda></Celda><br /> </Fila><br /> ...<br /> <Fila> Fila N<br /> <Celda></Celda><br /> <Celda></Celda><br /> <Celda></Celda><br /> </Fila><br /> <Filas><br /> </Contenido>

In the different examples you will found it interacts against a data base SQL Express, but it can also interact against other data bases.


The Code:

You will found a great variety of examples with differents configurations.

In the web.config:

<add name="ConnectionString" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=C:\Desarrollo\NET\LeleGrid\App_Data\Database.mdf;Integrated Security=True;User Instance=True" providerName="System.Data.SqlClient"/>

You must modify the path of "AttachDbFilename" according to where you have downloaded the ZIP

In the file .aspx we must have this:


In the head section:

<script type="text/javascript" src="../JavaScripts/protoype.js"></script><br /> <script type="text/javascript" src="../JavaScripts/grillaV2.js"></script>

In the body section or content:

<CNC:Grilla runat="server" ID="grillaCategorias" NombreSector="COT"
NombreGrilla="Categorias" NombreTabla="tblCategorias" />

This control shows the searching fields through which the data will be filtered. It is formed by several "Fields", through which the value will be searched, and another field, that is the value itself.

The mandatory parameters are:
NombreSector: As said in the Introduction of the articule I needed a grid for an Intranet with multiple modules . The, This field is for it.
NombreGrilla: The Grid's name
NombreTabla: The Table's name.

Important:
None of these 3 parameters should be the same.
They ID must be by the form "grilla" + NombreGrilla.

The optional parameters are:

OpcionesAvanzadas: Whether it has advanced options or not. TRUE/FALSE. By default TRUE
OpcionesBasicas: Whether it has basic options or not. TRUE/FALSE. By default TRUE
CantRegistros: Amount of registries shown per page. By default 10
Oculto: Whether to show the search fields or not. TRUE/FALSE. By default FALSE
Paginacion: Whether it has pagination or not. TRUE/FALSE. By default TRUE

In thead of the table which becomes it is to put the head of the Grid. The Width assigned to each column it's your own choice.

In addition, if one would like to sort the table by that particular column the event onclick should be added to every td.

The parameters received by the OrdenarTabla's function receives are: Index of the Column, Name of the Table, and Type of Ordering (0=Numbers, 1=String and 2=$) respectively.


<table id="tblCategorias" class="grilla" cellspacing="0"><br />            <thead><br />                <tr><br />                    <td style="width: 90px; display: block;">Opciones</td><br />
<td style="cursor: pointer; width: 50%;" title="Ordenar por Nombre"
onclick="OrdenarTabla(1,'tblCategorias', 1);">Nombre</td><br />
<td style="cursor: pointer; width: 50%;" title="Ordenar por
Traducci&oacute;n" onclick="OrdenarTabla(2,'tblCategorias',
1);">Traducci&oacute;n</td><br />                    <td style="width: 10px;">Activo</td><br />                </tr><br />            </thead><br />            <tbody id="trFilasCategorias"><br />                <tr><br />                    <td colspan="4">No se han encontrado resultados.</td><br />                </tr><br />            </tbody><br />            <tfoot id="trCargandoCategorias" style="display:none;"><br />                <tr><br />                    <td colspan="4" style="background-color:#fff;"><br />                        <CNC:Cargando ID="CargandoCategorias" runat="server" /><br />                    </td><br />                </tr><br />            </tfoot><br />        </table>

Important:
Id of the TBody must be: "trFilas" + NombreGrilla.
Id of the TFoot must be: "trCargando" + NombreGrilla
Id of the control loading must be: "Cargando" + NombreGrilla.

In the foot a row using the "Loading" control is usually added. This is just a graphical detail but it looks really pretty! You will realize this once the Grid is working. JA!

<asp:HiddenField runat="server" ID="hidVuelveDeEdicion" /><br /> <asp:HiddenField runat="server" ID="hidValorPagina" />

These two hidden fields are used when redirected by the Grid to another page and from this one back to the Grid. Theese fields will assure you the Grid will be loaded with it previous information.

In the file .aspx.cs or similar:

protected void Page_Load(object sender, EventArgs e) {<br /><br />            if(!IsPostBack) {<br /><br />                CargarBusqueda();<br />
Page.ClientScript.RegisterStartupScript(this.GetType(), "Load",
"RecuperarBusqueda('COT','Categorias','tblCategorias', 10 ,true, true,
true);",true);<br />                grillaCategorias.ValoresBusqueda.Items.Add(new ListItem("Nombre", "Nombre"));<br />                grillaCategorias.ValoresBusqueda.Items.Add(new ListItem("Traducción", "Traduccion"));<br />            }<br /><br />            grillaCategorias.ValorBusqueda.Focus();<br />        }<br /><br />        private void CargarBusqueda() {<br /><br />            string valorPagina = Request.QueryString["valorPagina"];<br />            string valorCampo = Request.QueryString["valorCampo"];<br />            string valorBusqueda = Request.QueryString["valorBusqueda"];<br /><br />            if(string.IsNullOrEmpty(valorPagina) || string.IsNullOrEmpty(valorCampo) || string.IsNullOrEmpty(valorBusqueda)) {<br />                hidVuelveDeEdicion.Value = "FALSE";<br />            }<br />            else {<br />                hidVuelveDeEdicion.Value = "TRUE";<br />                hidValorPagina.Value = valorPagina;<br />                grillaCategorias.ValorBusqueda.Text = valorBusqueda;<br />                grillaCategorias.ValoresBusqueda.SelectedValue = valorCampo;<br />            }<br />    }    

In the file grillaV2.js :

It is the Grid's main file. It may have global variables:

var indiceColAnt = 0;<br />        var classAnt;<br /><br />
var _imgPath = "../Imagenes/Grilla/"; This is the only variable that you can modifiy.<br /><br />        var _tipoGrilla = null;<br />        var _sector = null;<br />        var _table = null;<br />        var _opcionesBasicas = null;<br />        var _opcionesAvanzadas = null;<br />        var _paginaActual = null;<br />        var _valorCantReg = null;<br />        var _valorCampo = null;<br />        var _valorBusqueda = null;<br />        var _realizaPaginacion = null;


Apart from containing all the pertinent functions, it contains a EjecutarAccion function.
This function is used for the advanced options of grid.

Styles:

We can have these CSS in the Themes or just in a .css file.
Remind yourself to change the references of the images !

.grilla {<br /> background:Window;<br /> border:Solid 1px #c4bdb0;<br /> color:WindowText;<br /> font:Icon;<br /> width:100%;<br /> }<br /> .grilla thead{<br /> BACKGROUND: url(../../Imagenes/menuBg.gif) repeat-x;<br /> color:black;<br /> }<br /> .grilla td {<br /> padding:2px 5px;<br /> }<br /> .grilla thead td {<br /> border:1px solid; <br /> border-color:ButtonHighlight ButtonShadow ButtonShadow ButtonHighlight;<br /> }<br /> .grilla select {<br /> font-family:Verdana;<br /> font-size:7pt;<br /> }<br /> .descendente {/* columna de ordenamiento descendente */<br /> font-weight:bold;<br /> height:11px;<br /> width:11px;<br /> }<br /> .ascendente{ /* columna de ordenamiento ascendente */<br /> font-weight:bold; <br /> height:11px;<br /> width:11px;<br /> }<br /> .AlternatingItemTemplate {/* filas pares */<br /> background-color:#fff;<br /> background-position:center center;<br /> }<br /> .ItemTemplate{/* filas impares */<br /> background-color:#fff;<br /> background-position:center center;<br /> }<br /> #cmbCantFilas{<br /> font-family:Verdana;<br /> font-size:8pt;<br /> }

Testing

The correct operation oF the LeleGrid was proved in IE 6+ and Mozilla 1.5+


TRADUCCION AL ESPAÑOL


Introduccion:

Diseñando la estructura de una Intranet con multiples modulos, se me presento el problema de las Grillas Dinamicas.
Necesitaba una Grilla altamente customizable, facil de aplicar y de usar, y lo mas importante, con tecnologia AJAX.
Buscando por la web, por codeproject, y por tantas otras paginas, no encontre ninguna que satisfaciera mis espectativas.
Lo unico que encontraba eran pequeñas cosas, que todas juntas, conforman lo que hoy es la "LeleGrid".


Links Utiles:

http://www.prototypejs.org


Requisitos:

1) Tener instalado SQL Server Express 2005.

2) Como es usual en estos tiempos la Grilla necesita de la Libreria Prototype, ya que la uso tanto para AJAX, como para el manejo de DOM

Ademas, tiene 1 metodo nuevo y otro modificado que describo a continuacion:

Metodo agregado:

Lo cree para que no se produzca el efecto de subir y bajar al usar las funciones (toogle, show o hide).
Se usa de la forma:
Element.invisible('nombrecontrol','visible') o Element.invisible('nombrecontrol','hidden')

invisible: function(type) {<br /> $(arguments[0]).style.visibility = arguments[1];<br /> },

Metodo Modificado:

Lo modifique ya que si usamos la Grilla con MasterPages, los ID de los controles se renderean de la forma: ctl00_contentPagina + NombreDelControl

function $() {<br /> var results = [], element;<br /> for (var i = 0; i < arguments.length; i++) {<br /> element = arguments[i];<br /> if (typeof element == 'string'){<br /> if (document.getElementById(element) != null)<br /> element = document.getElementById(element);<br /> else<br /> element = document.getElementById("ctl00_contentPagina_"+element);<br /> }<br /> results.push(Element.extend(element));<br /> }<br /> return results.length < 2 ? results[0] : results;<br /> }


Funcionamiento:

La Grilla usa HttpHandler para obtener el XML con los datos de la misma o para Eliminar algun elemento de la misma.
Estos archivos son 2: DatosGrilla.ashx y EliminarFilaGrilla.ashx.

El formato del XML del archivo DatosGrilla.ashx es de la siguiente forma:
<Contenido><br /> <Cantidad></Cantidad> Cantidad de Filas devueltas.<br /> <Filas><br /> <Fila> Fila 1<br /> <Celda></Celda> campo 1..<br /> <Celda></Celda> campo 2..<br /> ...<br /> <Celda></Celda> campo N..<br /> </Fila><br /> <Fila> Fila 2<br /> <Celda></Celda><br /> <Celda></Celda><br /> <Celda></Celda><br /> </Fila><br /> ...<br /> <Fila> Fila N<br /> <Celda></Celda><br /> <Celda></Celda><br /> <Celda></Celda><br /> </Fila><br /> <Filas><br /> </Contenido>

En los ejemplos encontraran que interactua contra una base de datos SQL Express, pero puede interactuar tmb contra otras bases de datos.


El Codigo:

Encontraran una gran variedad de ejemplos con distintos tipos de configuraciones.

En el web.config:

<add name="ConnectionString" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=C:\Desarrollo\NET\LeleGrid\App_Data\Database.mdf;Integrated Security=True;User Instance=True" providerName="System.Data.SqlClient"/>

Modifiiquen el path de "AttachDbFilename" de acuerdo a donde hayan bajado el ZIP.

En el archivo .aspx deberemos tener esto:


En el head:

<script type="text/javascript" src="../JavaScripts/protoype.js"></script><br /> <script type="text/javascript" src="../JavaScripts/grillaV2.js"></script>

En el body o el content:

<CNC:Grilla runat="server" ID="grillaCategorias" NombreSector="COT" NombreGrilla="Categorias" NombreTabla="tblCategorias" />

Este control lo que hace es Mostrar los Campos de Busqueda por los cuales se van a filtrar los datos en la grilla.
Consta de un combo de "Campos" , que es el campo por el que se va a buscar el valor.
Y Otro de "Valor", que es el valor propiamente dicho.

Los parametros obligatorios que necesita son:

NombreSector: Como dije en la Introduccion del articulo, necesitaba una grilla para una intranet con multiples modulos/sectores. Este campo es para ello.
NombreGrilla: El nombre de la Grilla
NombreTabla: El nombre de la tabla.

Importante: No deben ser iguales ninguno de estos 3 parametros. El ID debe ser de la forma "grilla" + NombreGrilla.

Los parametros opcionales son:

OpcionesAvanzadas: Si tiene opciones avanzadas o no. TRUE/FALSE. Por default TRUE
OpcionesBasicas: Si tiene opciones basicas o no. TRUE/FALSE. Por default TRUE
CantRegistros: Cantidad de registros que se muestran por pagina. Por default 10
Oculto: Si se muestran los campos de busqueda o no. TRUE/FALSE. Por default FALSE
Paginacion: Si tiene paginacion o no. TRUE/FALSE. Por default TRUE

En el thead de la tabla lo que se hace es poner la cabecera de la Grilla. El width que se le asigna a cada columna es a su eleccion.
Si ademas se quiere que se puedan ordenar la tabla por esa columna, se le debe agregar el evento onclick a cada td.

Los parametros que recibe la funcion OrdenarTabla son: Indice de la Columna, Nombre de la Tabla, y Tipo de Ordenamiento (0=Numeros, 1=String y 2=Monedas) respectivamente.

<table id="tblCategorias" class="grilla" cellspacing="0"><br /> <thead><br /> <tr><br /> <td style="width: 90px; display: block;">Opciones</td><br /> <td style="cursor: pointer; width: 50%;" title="Ordenar por Nombre" onclick="OrdenarTabla(1,'tblCategorias', 1);">Nombre</td><br /> <td style="cursor: pointer; width: 50%;" title="Ordenar por Traducci&oacute;n" onclick="OrdenarTabla(2,'tblCategorias', 1);">Traducci&oacute;n</td><br /> <td style="width: 10px;">Activo</td><br /> </tr><br /> </thead><br /> <tbody id="trFilasCategorias"><br /> <tr><br /> <td colspan="4">No se han encontrado resultados.</td><br /> </tr><br /> </tbody><br /> <tfoot id="trCargandoCategorias" style="display:none;"><br /> <tr><br /> <td colspan="4" style="background-color:#fff;"><br /> <CNC:Cargando ID="CargandoCategorias" runat="server" /><br /> </td><br /> </tr><br /> </tfoot><br /> </table>

Importante: El Id del TBody debe ser de la forma "trFilas" + NombreGrilla.
El Id del TFoot debe ser de la forma "trCargando" + NombreGrilla
El Id del control cargando debe ser de la forma "Cargando" + NombreGrilla.

En el foot lo que se hace es poner una fila con el control de Cargando, es un detalle grafico nomas pero que queda muy Lindo !
Cuando vean la Grilla en funcionamiento entenderan mejor..JA !

<asp:HiddenField runat="server" ID="hidVuelveDeEdicion" /><br /> <asp:HiddenField runat="server" ID="hidValorPagina" />

Estos 2 campos ocultos sirven para que cuando la Grilla te redirecciona a otra pagina, y desde esa pagina se vuelva a la Grilla,
la Grilla se cargue devuelta con los valores con los que estaba

En el .aspx.cs o similar:

protected void Page_Load(object sender, EventArgs e) {<br /><br /> if(!IsPostBack) {<br /><br /> CargarBusqueda();<br /> Page.ClientScript.RegisterStartupScript(this.GetType(), "Load", "RecuperarBusqueda('COT','Categorias','tblCategorias', 10 ,true, true, true);",true);<br /> grillaCategorias.ValoresBusqueda.Items.Add(new ListItem("Nombre", "Nombre"));<br /> grillaCategorias.ValoresBusqueda.Items.Add(new ListItem("Traducción", "Traduccion"));<br /> }<br /><br /> grillaCategorias.ValorBusqueda.Focus();<br /> }<br /><br /> private void CargarBusqueda() {<br /><br /> string valorPagina = Request.QueryString["valorPagina"];<br /> string valorCampo = Request.QueryString["valorCampo"];<br /> string valorBusqueda = Request.QueryString["valorBusqueda"];<br /><br /> if(string.IsNullOrEmpty(valorPagina) || string.IsNullOrEmpty(valorCampo) || string.IsNullOrEmpty(valorBusqueda)) {<br /> hidVuelveDeEdicion.Value = "FALSE";<br /> }<br /> else {<br /> hidVuelveDeEdicion.Value = "TRUE";<br /> hidValorPagina.Value = valorPagina;<br /> grillaCategorias.ValorBusqueda.Text = valorBusqueda;<br /> grillaCategorias.ValoresBusqueda.SelectedValue = valorCampo;<br /> }<br /> }

En grillaV2.js :


Es el archivo principal de la Grilla.
Las variables globales que contiene son las siguientes:
var indiceColAnt = 0;<br /> var classAnt;<br /><br /> var _imgPath = "../Imagenes/Grilla/"; Esta es la unica que se debe modificar. Es el path de la carpeta donde se encuentran las imagenes<br /><br /> var _tipoGrilla = null;<br /> var _sector = null;<br /> var _table = null;<br /> var _opcionesBasicas = null;<br /> var _opcionesAvanzadas = null;<br /> var _paginaActual = null;<br /> var _valorCantReg = null;<br /> var _valorCampo = null;<br /> var _valorBusqueda = null;<br /> var _realizaPaginacion = null;

Ademas de contener todas las funciones pertinentes, contiene una llamada EjecutarAccion .
Esta funcion se usa para las opciones avanzadas de la grilla.

Estilos:

Podemos tener estos CSS en los Themes o simplemente en un .css
Acordarse de cambiar las referencias a las imagenes.

.grilla {<br /> background:Window;<br /> border:Solid 1px #c4bdb0;<br /> color:WindowText;<br /> font:Icon;<br /> width:100%;<br /> }<br /> .grilla thead{<br /> BACKGROUND: url(../../Imagenes/menuBg.gif) repeat-x;<br /> color:black;<br /> }<br /> .grilla td {<br /> padding:2px 5px;<br /> }<br /> .grilla thead td {<br /> border:1px solid; <br /> border-color:ButtonHighlight ButtonShadow ButtonShadow ButtonHighlight;<br /> }<br /> .grilla select {<br /> font-family:Verdana;<br /> font-size:7pt;<br /> }<br /> .descendente {/* columna de ordenamiento descendente */<br /> font-weight:bold;<br /> height:11px;<br /> width:11px;<br /> }<br /> .ascendente{ /* columna de ordenamiento ascendente */<br /> font-weight:bold; <br /> height:11px;<br /> width:11px;<br /> }<br /> .AlternatingItemTemplate {/* filas pares */<br /> background-color:#fff;<br /> background-position:center center;<br /> }<br /> .ItemTemplate{/* filas impares */<br /> background-color:#fff;<br /> background-position:center center;<br /> }<br /> #cmbCantFilas{<br /> font-family:Verdana;<br /> font-size:8pt;<br /> }

Testing

Se comprobo su correcto funcionamiento es IE 6+ y Mozilla 1.5+

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Argentina Argentina
Soy un analista de Sistemas y Desarrollador Web de Argentina.
Trabajo desde los 19 años desarrollando Intranets , sitios web y aplicaciones Desktop de Alta Calidad y Gran Performance.

Comments and Discussions

 
GeneralMy vote of 3 Pin
RusselSSC27-Jan-11 12:21
RusselSSC27-Jan-11 12:21 
GeneralMy vote of 5 Pin
shanawazway14-Nov-10 2:32
shanawazway14-Nov-10 2:32 
GeneralMy vote of 1 Pin
Arlen Navasartian1-Nov-10 7:09
Arlen Navasartian1-Nov-10 7:09 
GeneralDownload Link Not Work Pin
markx00717-Jun-08 3:07
markx00717-Jun-08 3:07 
GeneralRe: Download Link Not Work Pin
LeleHalfon17-Jun-08 3:10
LeleHalfon17-Jun-08 3:10 
GeneralExcelente!!!! Pin
Edward Ceballos24-May-08 4:49
Edward Ceballos24-May-08 4:49 
Generalsuggestion Pin
Zakaria Bin Abdur Rouf7-Mar-07 22:28
Zakaria Bin Abdur Rouf7-Mar-07 22:28 
GeneralFelicitaciones! Pin
ajlopez1-Mar-07 22:52
ajlopez1-Mar-07 22:52 
GeneralGreat work Pin
Libin Chen1-Mar-07 14:50
Libin Chen1-Mar-07 14:50 
GeneralRe: Great work Pin
LeleHalfon5-Mar-07 11:56
LeleHalfon5-Mar-07 11:56 
GeneralExcelente Pin
Selecters1-Mar-07 8:16
Selecters1-Mar-07 8:16 
GeneralBuen Trabajo ! Pin
mr. ajax1-Mar-07 6:44
mr. ajax1-Mar-07 6:44 
GeneralEsta Super!! Pin
babalao1-Mar-07 6:28
babalao1-Mar-07 6:28 
GeneralEnglish Pin
Jeffrey Deflers1-Mar-07 2:54
Jeffrey Deflers1-Mar-07 2:54 
GeneralRe: English Pin
Zakaria Bin Abdur Rouf1-Mar-07 3:31
Zakaria Bin Abdur Rouf1-Mar-07 3:31 
GeneralRe: English Pin
vodzurk1-Mar-07 5:12
vodzurk1-Mar-07 5:12 
GeneralRe: English Pin
Zakaria Bin Abdur Rouf1-Mar-07 5:42
Zakaria Bin Abdur Rouf1-Mar-07 5:42 
AnswerRe: English Pin
LeleHalfon1-Mar-07 5:53
LeleHalfon1-Mar-07 5:53 
GeneralRe: English Pin
Colin Angus Mackay1-Mar-07 6:18
Colin Angus Mackay1-Mar-07 6:18 
GeneralRe: English Pin
Mike Ellison2-Mar-07 10:36
Mike Ellison2-Mar-07 10:36 
GeneralRe: English Pin
bigals4-Mar-07 11:58
bigals4-Mar-07 11:58 
GeneralRe: English Pin
vodzurk23-Sep-09 23:52
vodzurk23-Sep-09 23:52 
GeneralRe: English Pin
LeleHalfon1-Mar-07 5:54
LeleHalfon1-Mar-07 5:54 
GeneralRe: English Pin
ajlopez1-Mar-07 22:50
ajlopez1-Mar-07 22:50 

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.