Click here to Skip to main content
15,115,667 members
Articles / Web Development / ASP.NET
Posted 21 Feb 2009


33 bookmarked

SharePoint CAML Query Builder Dialog for your Web Parts

Rate me:
Please Sign up or sign in to vote.
4.65/5 (9 votes)
24 Mar 2009CPOL7 min read
A SharePoint CAML query builder dialog for your Web Parts



I've been writing Web Parts for quite some time now, and I came to realize that I have a lot of Web Parts that use CAML queries to pull information from lists. Because of this, our users were unable to actually configure these Web Parts themselves. Instead, they relied upon the administration group to build their queries for them. It was OK for a little while, but as the popularity for Web Parts grew within the company, we grew tired of having to build these queries for them. Since many of my Web Parts use CAML queries, I figured I'd need to find a way to spread this functionality across all of them. That's why I decided to have this feature work just like the List selector dialog that you see in the Content Query Web Part. I'll explain more below.


I'll first talk about how I figured out how to use SharePoint's framework for creating a dialog page in SharePoint.

First, drop a Content Query Web Part into a web part zone on one of your pages. Then, go ahead and modify its properties. Click the "Show items from the following list" radio button and then hit "Browse".


As you can see, it pops up the PickerTreeView.aspx page with a bunch of query strings appended to the end. If you select a list and click OK, then the values will be returned to the text box in your original window. So, let's find out how this happens.

Here's a little trick I like to do. Mouse over the Browse button, and you'll get a tool tip "Open a new window to select a list".


Right click the web page and go to "View Source". Hit Ctrl+F and then type in the tooltip's text. Click "Find", and boom! We are at our button's HTML.


Now, you'll be able to see, when the users click the button, it fires the JavaScript method mso_launchListSmtPicker(). So, let's hit Ctrl+F and see if we can find this method in the source of this page. And yes, it's in this page, towards the top.


After some analysis of this method, we find that a callback (the callback sets our controls with the returned values from the dialog) method is created and is passed to LaunchPickerTreeDialog, which must be the method that launches the PickertTreeDialog window. So, let's try and find it. First, we can Ctrl+F and see if it's in this page, but I'll save you the time and tell you it's not there. So, what will we do? We'll just launch VS and use the script debugger. If you are using IE 7, click Alt and you'll see the classic menu show up. Then, go to View -> Script Debugger -> Open. Then, start up a new instance of VS. The page's HTML will load up along with all the JavaScript files that are referenced. If we look at the list of JavaScript pages referenced, we'll see the PickerTreeDialog.js and we can safely assume the LaunchPickerTreeDialog method is in that file.


 // _lcid="1033" _version="12.0.6211"
// _localBinding
// Version: "12.0.6211"
// Copyright (c) Microsoft Corporation. All rights reserved.
var PickerTreeDlgUrl="/_layouts/PickerTreeView.aspx";
var PickerTreeDlgDimension="resizable:yes;status:no;location:no;menubar:no;help:no";
var L_WarningFailedOperation_TEXT="Do you wish to continue?";
var L_NullSelectionText_TEXT="Please select a target or click cancel";
var IdSeparator=",";

function LaunchPickerTreeDialog(title, text, filter, anchor, 
         siteUrl, select, featureId, errorString, 
         iconUrl, sourceSmtObjectId, callback)
   var sourceInfo=false;
   var sources=null;
   if(sourceSmtObjectId !=null && 
      sourceSmtObjectId.length > MAX_SOURCEID_LENGTH)
    if(sources.length > 1)
   var sourceObjectIdAppend=(sourceInfo)? sources[0] : sourceSmtObjectId;
   var dialogUrl=TrimLastSlash(siteUrl)+PickerTreeDlgUrl+"?title="+ 
  commonShowModalDialog(dialogUrl, PickerTreeDlgDimension, callback, null);

function HandleOkReturnValues(strDlgReturnValue, strDlgReturnErr)
 if (strDlgReturnValue[0].indexOf("Error:") >=0)
  alert( strDlgReturnValue[0].slice(7));
  if(strDlgReturnErr.indexOf("Error:") >=0)
   var promptUser=strDlgReturnErr.slice(7)+"."+L_WarningFailedOperation_TEXT;
    return false;
 return false;

What does this method do? All it really does is prepare some information to be sent into the commonShowModalDialog method. commonShowModalDialog is a core.js method that contains all the logic for showing dialogs. All you have to do is give it a URL, some parameters such as the info in PickerTreeDlgDimension, and a callback method that will be invoked after the user clicks OK or Cancel from the dialog page. Pretty sweet, eh?

Now that we know how to load up the page, let's figure out how information gets sent back from the dialog to our original window. Let's find the PickerTreeView.aspx page in the layouts directory (C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\TEMPLATE\LAYOUTS) and open it up. After analyzing the page, we can cut out a lot of ASP code and reuse it as a base template for any future dialogs. The main point to get from this is that dialog pages use the Dialog.Master master page instead of Application.Master. This master page provides the consistent look and feel of all dialog pages.

<%@ Page Language="C#" AutoEventWireup="true" 
  MasterPageFile="~/_layouts/dialog.master" CodeBehind="MYCLASS.aspx.cs" 
            Version=, Culture=neutral, PublicKeyToken=c37a514ec27d3057" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" 
   Assembly="Microsoft.SharePoint, Version=, 
             Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 
<asp:Content ID="Content1" 
  <asp:Literal runat="server" ID="dialogTitle" />
<asp:Content ID="Content2" 
 <SharePoint:ScriptLink ID="ScriptLink2" language="javascript" 
     name="core.js" runat="server" />
 <script type="text/javascript" language="javascript">
     function HandleOkClicked() {
             var strDlgReturnValue = new Array(3);
           strDlgReturnValue[0] = "value5";
             strDlgReturnValue[1] = "Value4";
             strDlgReturnValue[2] = "Value3";
             var strDlgReturnErr = "";//Set this if an error occurs
             if(strDlgReturnValue[0] == null || strDlgReturnValue[0].length <= 0)
               return HandleOkReturnValues(strDlgReturnValue, strDlgReturnErr);         
 <SharePoint:FormDigest ID="FormDigest1" runat="server"/>
<asp:Content ID="Content3" 
 <asp:Image ID="imgIcon" width="32" 
    height="32" runat="server" />
<asp:Content ID="Content4" 
 <asp:Literal runat="server" ID="dialogDescription" />
<asp:Content ID="Content5" 
  contentplaceholderid="PlaceHolderHelpLink" runat="server">
<asp:Content ID="Content6" 
<!-- This is the main body of our page. So any controls you want to be in the dial
go here -->

Take a look above and you'll see the method HandleOkReturnValues. This is the method that we saw above in PickerTreeView.js that calls setModalDialogReturnValue. This is a core.js method that invokes the callback method provided by mso_LaunchSmtListPicker. HandleOkClicked is just a simple method that illustrates how values are passed back. To register it to be called by the OK button of the master page, we need to add the following code-behind to our new dialog page.

protected override void OnLoad(EventArgs e)
    ((DialogMaster)base.Master).OkButton.Click += 
                   new EventHandler(OkButton_Click);

If this doesn't make complete sense to you yet, hopefully, when you use the query builder and look at the code, it will help clear things up.


GAC the following DLLs (located in the Build folder):

  • Mullivan.Shared.dll
  • Mullivan.SharePoint.dll
  • Mullivan.SharePoint.Pages.dll
  • Mullivan.SharePoint.Reminders.dll
  • Mullivan.SharePoint.WebParts.dll

Navigate to C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\TEMPLATE\LAYOUTS.

Copy Build\Layouts\QueryBuilder.aspx to this dir
Copy Build\Layouts\FieldValueDialog.aspx to this dir

Navigate to C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\1033.

Copy Build\Layouts\1033\QueryBuilder.js to this dir
Copy Build\Layouts\1033\MullivanUtility.js to this dir
Copy Build\Layouts\1033\FieldValueDialog.js to this dir

Register Web Parts: register Mullivan.SharePoint.WebParts.dll in the SharePoint Web Config. Go to C:\inetpub\wwwroot\wss\VirtualDirectories\<Port Number>\web.config. Add the following in between the SafeControl node:

  Assembly="Mullivan.SharePoint.WebParts, Version=, 
  Culture=neutral, PublicKeyToken=c37a514ec27d3057"
  TypeName="*" Safe="True" />

Go to Site Settings -> Webparts -> click New on the menu.

Scroll to the bottom and check Mullivan.SharePoint.WebParts.ListQueryWebPart and Mullivan.SharePoint.WebParts.ListQueryResultsWebPart, and then scroll back to the top and click Populate Gallery.


Go to Start -> Run and type iisreset and click ok

Using the Query Builder

Let's take a look at the ListQueryWebPart and ListQueryResultsWebPart that I had you install. Go ahead and drop both into a Web Part zone and connect the two Web Parts up. Then, click "Modify SharePoint Web Part" on ListQueryWebPart.


Go ahead and pick a list you want to query by hitting "Browse", and then click the "Build" button for either view or query. You'll see the QueryBuilder window come up. OK.. go ahead and have fun. If you want to test out a query, then make sure you drop a ListQueryResultsWebPart on the page and connect the two Web Parts up.

Clicking the "Build" button for view:


In this dialog, you'll choose all the fields that you want to come back in your list query.

Clicking the "Build" button for query:



Here is the dialog you will use to build your query. The User Input checkbox is a feature that the ListQueryWebPart uses to work like a templated search. Basically, all the conditions that you want to define values for in the ListQueryWebPart need to be set as User Input. Let me show you a screenshot of what the ListQueryWebPart looks like after the above CAML query is saved.


If you define a condition as User Input, then ListQueryWebPart will render an edit control to the page and insert the value into the CAML query when you click "Search". ListQueryWebPart requires at least one User Input condition to work.

If your Web Part uses the User Input feature of the Query Builder, you'll need to parse out the <UserInput> tags that are used as placeholders for the <Value> tag that will need to be placed in there. FYI, here is the CAML query that was passed back to ListQueryWebPart by the Query Builder.

          <FieldRef Name='MultiSelect'  />
          <UserInput />
          <FieldRef Name='Column2'  />
          <UserInput />
          <FieldRef Name='Created'  />
          <UserInput />
          <FieldRef Name='MultiLookup' LookupId='True' />
          <UserInput />
    <FieldRef Name='Modified' Ascending='True' />

If you choose not to allow the User Input, then it forces you to put a value in during the creation of the CAML query.

If you are not sure what kind of data goes into your column, then just click the Edit Dialog button. This will help you find lookups, users, choices, and etc. It's really handy.


Using the code

Let's take a look at how to use the Query Builder dialog. Let's take a look at some source code first. Open up the ListQueryEdit.cs file in the Mullivan.SharePoint.WebParts project. This is the editor control that is used by ListQueryWebPart. It registers our JavaScript files and then loads controls that have associated buttons that pop up the dialog windows. These windows will then return values back and set the textboxes with them.

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.WebControls;
using System.Web.UI;
using Microsoft.SharePoint;
namespace Mullivan.SharePoint.WebParts
    public class ListQueryEditor : EditorPart
        private TextBox _tboxList = null;
        private TextBox _tboxViewFields = null;
        private TextBox _tboxQuery = null;
        private TextBox _tboxPageSize = null;
        private RangeValidator _rvPageSize = null;
        public ListQueryEditor(string webPartId)
            this.ID = webPartId + "_ListQueryEditor";
        protected override void OnInit(EventArgs e)
            _tboxList = new TextBox();
            _tboxViewFields = new TextBox();
            _tboxQuery = new TextBox();
            _tboxPageSize = new TextBox();
            _rvPageSize = new RangeValidator();
            _tboxList.ID = "tboxList";
            _tboxViewFields.ID = "tboxViewFields";
            _tboxQuery.ID = "tboxQuery";
            _tboxPageSize.ID = "tboxPageSize";
            _rvPageSize.ID = "rvPageSize";
            _rvPageSize.ControlToValidate = "tboxPageSize";
            _rvPageSize.ErrorMessage = 
              "Page size must be an integer between 1 and 1000.";
            _rvPageSize.MaximumValue = "1000";
            _rvPageSize.MinimumValue = "1";
            _rvPageSize.Type = ValidationDataType.Integer;
        protected override void CreateChildControls()
            #region "HTML"
            Literal litBeginHtml = new Literal();
            litBeginHtml.Text = @"
<br />
<table cellspacing=""0"" 
       style=""width:0px; width:100%;border-collapse:collapse;"">
            <div class=""UserSectionTitle"">
                <a id=""{0}_MVQueryCategory_IMAGEANCHOR"" 
                  onkeydown='MSOMenu_KeyboardClick(this, 13, 32);' 
                            '{0}_MVQueryCategory_IMAGE', '{0}_MVQueryCategory_ANCHOR', 
                            'Expand category: Query', 'Collapse category: Query',
                  title=""Collapse category: Query"" >
                    &nbsp;<img id=""{0}_MVQueryCategory_IMAGE"" 
                       alt=""Collapse category: Query"" 
                       src=""/_layouts/images/TPMin2.gif"" />&nbsp;
                <a TabIndex=""-1"" 
                    onkeydown='MSOMenu_KeyboardClick(this, 13, 32);' 
                            '{0}_MVQueryCategory_IMAGE', '{0}_MVQueryCategory_ANCHOR', 
                            'Expand category: Query', 'Collapse category: Query',
                    title=""Collapse category: Query"" >&nbsp;Query
<div id=""{0}_MVQueryCategory"">
    <table cellspacing=""0"" border=""0"" 
            Literal litEndHtml = new Literal();
            litEndHtml.Text = @"
            #endregion "HTML"
            string listClientId = this.ClientID + "_tboxList";
            string viewClientId = this.ClientID + "_tboxViewFields";
            string queryClientId = this.ClientID + "_tboxQuery";
            string serverUrl = SPContext.Current.Web.ServerRelativeUrl.Replace(
                                 "/", "\\u002f");
            string editListJS = string.Format(
              "LQWP_LaunchListPicker('{0}','{1}')", listClientId, serverUrl);
            string editViewJS = string.Format(
              listClientId, viewClientId, serverUrl);
            string editQueryJS = string.Format(
              listClientId, queryClientId, serverUrl);
                , "Value that represents the list that is set to be queried."
                , "Browse..."
                , editListJS
                , null);
                , "The fields that you would like to display in the results."
                , _tboxViewFields
                , this.Controls
                , "Build..."
                , editViewJS
                , null);
                , "The CAML query that will be used to search the list."
                , _tboxQuery
                , this.Controls
                , "Build..."
                , editQueryJS
            AppendEditControl("Page Size"
                , "The amount of items that should be displayed per page."
                , _tboxPageSize
                , this.Controls
                , null
                , null
        protected override void OnPreRender(EventArgs e)
            ClientScriptManager csm = this.Page.ClientScript;
            Type lnType = this.GetType();
            //Register our Javascript file
            if (!csm.IsClientScriptIncludeRegistered(
                string url = csm.GetWebResourceUrl(lnType,
            if (!csm.IsClientScriptIncludeRegistered(
                string url = csm.GetWebResourceUrl(lnType, 
                  "Mullivan.SharePoint.WebParts.JS.ListQuery.js", ResolveClientUrl(url));
            if (!csm.IsClientScriptIncludeRegistered(@"PickerTreeDialog"))
                string url = @"/_layouts/1033/PickerTreeDialog.js";
                csm.RegisterClientScriptInclude(lnType, "PickerTreeDialog", url);
            if (!csm.IsClientScriptIncludeRegistered(@"QueryBuilderDialog"))
                string url = @"/_layouts/1033/QueryBuilder.js";
                csm.RegisterClientScriptInclude(lnType, "QueryBuilderDialog", url);
        private void AppendEditControl(string displayName, string description
            , WebControl control, ControlCollection controlCollection
            , string editText, string editJS, Control validationControl)
            Literal litBeginHtml = new Literal();
            Literal litEndHtml = new Literal();
            if (validationControl != null)
            litBeginHtml.Text = string.Format(@"
       <div class=""UserSectionHead"">
           <LABEL FOR=""{0}"" 
       <div class=""UserSectionBody"">
           <div class=""UserControlGroup"">
               <table cellpadding=""0"" 
                 cellspacing=""0"" border=""0"">
                       <tr style=""text-align:left"">
                           <td>", control.ClientID, displayName, description);
            litEndHtml.Text = @"
                       <tr style=""text-align:right;{0}"">
                           <td><input type=""button""  
                                   title=""Click to edit."" 
                                   onclick=""javascript:{2}"" />
       <div style='width:100%' class='UserDottedLine'>
            string display = "";
            if (string.IsNullOrEmpty(editText))
                editText = string.Empty;
                editJS = string.Empty;
                display = "display:none";
            litEndHtml.Text = string.Format(litEndHtml.Text, display, editText, editJS);
            control.CssClass = "UserInput";
            control.Style[HtmlTextWriterStyle.Width] = "176px";
        public override bool ApplyChanges()
            ListQueryWebPart lqwp = this.WebPartToEdit as ListQueryWebPart;
            if (lqwp == null)
                return false;
            lqwp.Query = _tboxQuery.Text;
            lqwp.ListUrl = _tboxList.Text;
            lqwp.ViewFields = _tboxViewFields.Text;
            lqwp.PageSize = uint.Parse(_tboxPageSize.Text);
            return true;
        public override void SyncChanges()
            ListQueryWebPart lqwp = this.WebPartToEdit as ListQueryWebPart;
            if (lqwp == null)
            _tboxQuery.Text = lqwp.Query;
            _tboxList.Text = lqwp.ListUrl;
            _tboxViewFields.Text = lqwp.ViewFields;
            _tboxPageSize.Text = lqwp.PageSize.ToString();

Above, you'll see that we registered Mullivan.SharePoint.WebParts.JS.ListQuery.js, so let's take a look at it. This contains our JavaScript methods for using the PickertTreeView dialog and the query builder dialog.

var lastSelectedListId = null;
function LQWP_LaunchListPicker(clientId, serverUrl) {
    var callback = function(results) {
        LQWP_SetList(clientId, results);
      'listsOnly', '', serverUrl, lastSelectedListId, '', '', 
      '/_layouts/images/smt_icon.gif', '', callback);
function LQWP_SetList(clientId, results) {
    var listTextBox = document.getElementById(clientId);
    if (results == null
        || results[1] == null
        || results[2] == null) return;
    if (results[2] == "") {
        alert("You must select a list!.");
    lastSelectedListId = results[0];
    var listUrl = '';
    if (listUrl.substring(listUrl.length - 1) != '/')
        listUrl = listUrl + '/';
    if (results[1].charAt(0) == '/')
        results[1] = results[1].substring(1);
    listUrl = listUrl + results[1];
    if (listUrl.substring(listUrl.length - 1) != '/')
        listUrl = listUrl + '/';
    if (results[2].charAt(0) == '/')
        results[2] = results[2].substring(1);
    listUrl = listUrl + results[2];
    listTextBox.value = listUrl;

function LQWP_LaunchQueryBuilder(listClientId, queryClientId, serverUrl) {
    var queryTextBox = document.getElementById(queryClientId);
    var listTextBox = document.getElementById(listClientId);
    if (listTextBox.value.replace(/^\s+|\s+$/g, "") == "") {
        alert("A list must be selected before building a query.");
    var callback = function(results) {
    LQWP_SetQuery(queryClientId, results);
    var query = queryTextBox.value;
    query = Mullivan.Utilities.UrlEncode(query);
    QB_LaunchQueryBuilderDialog(serverUrl, listTextBox.value, 
                                null, query, false, true, true, callback);
function LQWP_SetQuery(clientId, results) {
    var queryTextBox = document.getElementById(clientId);
    if (results == null || results.length < 2)
    //Index 1 contains the query and 0 is for the view if it is being used.
    queryTextBox.value = results[1];
function LQWP_LaunchViewBuilder(listClientId, viewClientId, serverUrl) {
    var viewTextBox = document.getElementById(viewClientId);
    var listTextBox = document.getElementById(listClientId);
    if (listTextBox.value.replace(/^\s+|\s+$/g, "") == "") {
        alert("A list must be selected before building a query.");
    var callback = function(results) {
    LQWP_SetView(viewClientId, results);
    var view = viewTextBox.value;
    view = Mullivan.Utilities.UrlEncode(view);
    QB_LaunchQueryBuilderDialog(serverUrl, listTextBox.value, 
                                view, null, true, false, false, callback);
function LQWP_SetView(clientId, results) {
    var viewTextBox = document.getElementById(clientId);
    if (results == null || results.length < 2)
    //Index 1 contains the query and 0 is for the view if it is being used.
    viewTextBox.value = results[0];

And voila!

Wow... that's a long article and I'm really tired. Please give feedback.


This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


About the Author

Sike Mullivan
Software Developer (Senior)
United States United States
No Biography provided

Comments and Discussions

Questionhow to configure in SP2010 Pin
tariquekamal17-Dec-11 19:34
Membertariquekamal17-Dec-11 19:34 
GeneralMy vote of 1 Pin
Clayton W Firth25-Oct-11 15:55
MemberClayton W Firth25-Oct-11 15:55 
GeneralRe: My vote of 1 Pin
Sike Mullivan26-Jan-12 10:31
MemberSike Mullivan26-Jan-12 10:31 
QuestionError on page when clicking Browse button Pin
benoitparis15-Sep-10 5:38
Memberbenoitparis15-Sep-10 5:38 
QuestionHow to get the return value alone Pin
s.c.vinod12-May-10 22:12
Members.c.vinod12-May-10 22:12 
GeneralProblem I used wss 3.0 Pin
robertap3-May-10 7:37
Memberrobertap3-May-10 7:37 
GeneralFailed to get multi select choice fields Pin
m_Consultant20-Apr-10 22:08
Memberm_Consultant20-Apr-10 22:08 
QuestionDownload search results? Pin
Highdesert_nm28-Feb-10 12:21
MemberHighdesert_nm28-Feb-10 12:21 
Questionvalue does not fall within the expected range Pin
hongzeniu20-Dec-09 20:39
Memberhongzeniu20-Dec-09 20:39 
GeneralSike Mullivan Please help on this simple requirement. Pin
Member 46568125-Oct-09 5:36
MemberMember 46568125-Oct-09 5:36 
QuestionGreat article - 1 question Pin
Arkitec20-Sep-09 17:43
professionalArkitec20-Sep-09 17:43 
AnswerRe: Great article - 1 question Pin
Sike Mullivan21-Sep-09 4:49
MemberSike Mullivan21-Sep-09 4:49 
AnswerRe: Great article - 1 question Pin
Arkitec21-Sep-09 16:22
professionalArkitec21-Sep-09 16:22 
GeneralInvalid query??!! Pin
suissa11-Sep-09 2:13
Membersuissa11-Sep-09 2:13 
GeneralRe: Invalid query??!! Pin
suissa11-Sep-09 2:14
Membersuissa11-Sep-09 2:14 
Generaledit dialog Pin
suissa19-Aug-09 4:05
Membersuissa19-Aug-09 4:05 
GeneralRe: edit dialog Pin
Sike Mullivan19-Aug-09 5:32
MemberSike Mullivan19-Aug-09 5:32 
GeneralRe: edit dialog Pin
suissa19-Aug-09 5:56
Membersuissa19-Aug-09 5:56 
GeneralContent Query web part Pin
topgun12313-Jul-09 21:36
Membertopgun12313-Jul-09 21:36 
GeneralNested CAML Queries Pin
s.c.vinod12-Jul-09 20:05
Members.c.vinod12-Jul-09 20:05 
Generalwowoooo.. gr8 post... one question though Pin
umang551-May-09 10:01
Memberumang551-May-09 10:01 
GeneralRe: wowoooo.. gr8 post... one question though Pin
Sike Mullivan1-May-09 10:37
MemberSike Mullivan1-May-09 10:37 
GeneralRe: wowoooo.. gr8 post... one question though Pin
umang551-May-09 11:11
Memberumang551-May-09 11:11 
GeneralMe again - No Data Pin
choggatt30-Apr-09 13:06
Memberchoggatt30-Apr-09 13:06 
GeneralRe: Me again - No Data Pin
Sike Mullivan1-May-09 4:37
MemberSike Mullivan1-May-09 4:37 

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.