Click here to Skip to main content
15,886,035 members
Articles / Programming Languages / C#
Article

Creating MMC Snapin using C# (Part I)

Rate me:
Please Sign up or sign in to vote.
3.47/5 (15 votes)
12 Nov 20033 min read 374.2K   2.2K   61   22
This article explains how to create an MMC Snapin using C#.

Image 1

Introduction

The article/code snippet helps in creation of MMC Snapin using C#.

Background

The framework for building management consoles for Windows NT/2000 is developed by Jim. Jim is the author for ironringsoftware. In our article we will be using this framework to derive the UI for the management console. Detailed step for generating the snapin console is explained below.

Using the code

Here are the steps to use the MMC library created by the author at ironringsoftware to create management consoles for Windows NT/2000

Step 1: Downloading framework

You need to download the latest source of MMC Snapin Framework from ironringsoftware.

The zip file downloaded contains three projects.

  • First is <MMCFormsShim> which is the implementation of the framework in C++.
  • Second is <MCLib> which mainly is the Runtime Callable Wrapper (RCW) for the underlying C++ code and also contains the implementation for the Snapin objects like nodes, menus etc., with features like event handlers etc. defined.
  • Finally <MCTest> is the project which shows the usage of the framework to generate MMC Snapin.

Step 2: How to view the sample Snapin provided

  • Set <MMCTest> project as the startup project.
  • When executed the project, it displays Microsoft Management Console.
  • Click on menu Console -> Add/Remove Snap-in..
  • Click Add button
  • Select "MMC Library Test Snapin" and click Add button and then the Close button
  • Finally you shall be viewing the Snapin sample in your Console.

Step 3: Initial setup

  • Create a directory "C:\FileActDemo" with few sample files and sub directories in it
  • View the properties of project <MMCTest>. Set the value for "Working Directory" property under "Configuration Properties -> Debugging" section appropriately.
  • Open <dlldatax.c> file in project <MMCFormsShim> and replace the value for "#define _WIN32_WINNT" from "0x0400" to the appropriate value. for e.g. "0x0586"

Step 4: How to create new nodes in the Snapin

Let us take an example of creating a node which contains all the directory information on the left pane and the file information in the result pane.

  • Open <BaseNode.cs> file in project <MMCLib> and add a new protected class variable as shown in the code below
C#
//This is the variable which holds the data passed from the framework.
protected Hashtable m_Properties;    
  • Initialize the variable in the constructor of the class as shown in the code below
C#
m_Properties = new Hashtable();    
  • This is the public property used to pass the values from the framework to the form. Add public property for the class to access the protected variable just defined as shown in the code below
C#
public Hashtable Properties {
  get { return m_Properties; }
  set { m_Properties = value; }
}    
  • Add new .cs file into project <MMCLib> and name it as <DirectoryNode.cs>. Replace the code of the class file with the code below
C#
// 
//Code block for DirectoryNode.cs
//
using System;
using System.IO;
using System.Data;
using System.Runtime.InteropServices;

namespace Ironring.Management.MMC {
  /// <summary>
  /// Any node which we create should be derived from the BaseNode.
  /// This node will get displayed in the left pane of the MMC. 
  /// 
  /// </summary>
  public class DirectoryNode : BaseNode {
    protected DataTable m_table = null;

    public DirectoryNode(SnapinBase snapin) : base(snapin) {
    }

    /// <summary>
    /// This is the public property to get/set the datatable value.
    /// This datatable values are the ones which gets displayed in the right pane
    /// of the MMC for the respective node item selected in the left pane.
    /// </summary>
    public DataTable Table {
      get { return m_table; }
      set { m_table = value; }
    }

    /// <summary>
    /// This method is called by the MMC snap to populate the data for listview 
    /// in the right pane, for the respective node item selected in the left pane.
    /// </summary>
    public override void OnShow() {
      IConsole2 console = Snapin.ResultViewConsole;

      IHeaderCtrl2 header = console as IHeaderCtrl2;
      OnShowHeader(header);

      IResultData rdata = console as IResultData;
      OnShowData(rdata);

      rdata.SetViewMode((int)ViewMode.Report);
    }

    /// <summary>
    /// This method is called by the OnShow() method to 
    /// display header for the listview 
    /// in the right pane, for the respective node item selected in the left pane.
    /// </summary>
    public virtual void OnShowHeader(IHeaderCtrl2 header) {
      try {
        if (Table != null) {

          foreach (DataColumn col in Table.Columns) {
            string name = col.ColumnName;
            int ord = col.Ordinal;
            header.InsertColumn(ord, name, (int)ColumnHeaderFormat.LEFT, 140);
          }
        }
      }
      catch(COMException e) {
        System.Diagnostics.Debug.WriteLine(e.Message);
        throw e;
      }
      catch(Exception e) {
        System.Diagnostics.Debug.WriteLine(e.Message);
        throw e;
      }
    }

    /// <summary>
    /// This method is called by the OnShow() method to 
    /// display data for the listview 
    /// in the right pane, for the respective node item selected in the left pane.
    /// </summary>
    public virtual void OnShowData(IResultData ResultData) {
      if (Table != null) {
        int nRow = 1;

        RESULTDATAITEM rdi = new RESULTDATAITEM();
        foreach (DataRow row in Table.Rows) {
          row.ToString();
          rdi.mask=(uint)RDI.STR | (uint)RDI.IMAGE | (uint)RDI.PARAM;     
          rdi.nImage=-1; 
          rdi.str=  (IntPtr)(-1);
          rdi.nCol=0;
          rdi.lParam=m_iCookie|(nRow <<  16);
          ResultData.InsertItem(ref rdi);
          nRow++;
        }
      }
    }

    /// <summary>
    /// This method is called by the MMC snap to display
    /// each listview item's value 
    /// in the right pane.
    /// </summary>
    public override void GetDisplayInfo(ref RESULTDATAITEM ResultDataItem) {
      bool bCallbase = true;

      if (Table != null) {
        int nRow = (ResultDataItem.lParam >> 16) - 1;
        int nCol = ResultDataItem.nCol;

        if ((ResultDataItem.mask & (uint)RDI.STR) > 0) {
          string data = DisplayName;
          if (nRow >= 0 && nRow < Table.Rows.Count && 
                nCol >= 0 && nCol < Table.Columns.Count) {
            data = Table.Rows[nRow].ItemArray[nCol].ToString();
            bCallbase = false;
          }

          ResultDataItem.str = Marshal.StringToCoTaskMemUni(data);
        }
      }

            if ((ResultDataItem.mask & (uint)RDI.IMAGE) > 0) {
                int offset = 1;
                if (IsUSeSmallIcons())
                    offset = 0;

                ResultDataItem.nImage = (Cookie << 16) + offset;
            }

      if (bCallbase)
        base.GetDisplayInfo(ref ResultDataItem);
    }
    
    /// <summary>
    /// This method creates the datatable with  appropriate data which servers 
    /// as the datasource for the listview displayed in the right pane, for the 
    /// respective node item selected in the left pane.
    /// </summary>
    public void CreateDataTableStructure() {
      DataTable dataTable = new DataTable();
      DataColumn dataColumn = new DataColumn();
      dataColumn.DataType = System.Type.GetType("System.String");
      dataColumn.ColumnName = "Name";
      dataColumn.ReadOnly = true;
      dataColumn.Unique = true;
      // Add the Column to the DataColumnCollection.
      dataTable.Columns.Add(dataColumn);

      dataColumn = new DataColumn();
      dataColumn.DataType = System.Type.GetType("System.String");
      dataColumn.ColumnName = "Last Accessed";
      dataColumn.ReadOnly = true;
      dataColumn.Unique = false;
      // Add the Column to the DataColumnCollection.
      dataTable.Columns.Add(dataColumn);

      dataColumn = new DataColumn();
      dataColumn.DataType = System.Type.GetType("System.String");
      dataColumn.ColumnName = "Last Written";
      dataColumn.ReadOnly = true;
      dataColumn.Unique = false;
      // Add the Column to the DataColumnCollection.
      dataTable.Columns.Add(dataColumn);

      // Make the ID column the primary key column.
      DataColumn[] PrimaryKeyColumns = new DataColumn[1];
      PrimaryKeyColumns[0] = dataTable.Columns["Name"];
      dataTable.PrimaryKey = PrimaryKeyColumns;
      this.Table = dataTable;
    }

    public void FillDataTableValues() {
      DataRow dataRow;
      DirectoryInfo directoryInfo = new DirectoryInfo(
           this.Properties["NodePath"].ToString());

      foreach(FileInfo fileInfo in directoryInfo.GetFiles()) {
        if(!fileInfo.Name.ToString().EndsWith(".transfer") && 
              !fileInfo.Name.ToString().EndsWith(".config")) {
          dataRow = this.Table.NewRow();
          dataRow["Name"] = fileInfo.Name;
          dataRow["Last Accessed"] = fileInfo.LastAccessTime.ToString();
          dataRow["Last Written"] = fileInfo.LastWriteTime.ToString();
          this.Table.Rows.Add(dataRow);
        }
      }
    }
  }
}
  • Add new windows form to project <MMCTest> and name it as <FormDirectoryProperty>. Replace the code of the form with the code below
C#
//
//Code block for FormDirectoryProperty//
// 
using System; 
using System.Drawing; 
using System.Collections; 
using System.ComponentModel; 
using System.Windows.Forms; 

namespace MMCTest
  { 
  /// <summary>
  /// This is a sample form which opens up when menu button "FileAct Properties" 
  /// is clicked over the selected node.
  /// </summary>
  public class FormDirectoryProperty: System.Windows.Forms.Form {
    private System.ComponentModel.Container components=null;
    private Hashtable nodeProperties;
    
    public FormDirectoryProperty() {
      //
      // Required for Windows Form Designer support
      //
      InitializeComponent();

      //
      // TODO: Add any constructor code after InitializeComponent call
      //
      this.Icon=null;
      this.Width=384;
      this.Height=440;
      this.MaximizeBox=false;
      this.MinimizeBox=false;

      this.Text="Directory Properties";

      this.ShowInTaskbar=false;
      this.StartPosition=FormStartPosition.CenterParent;
      this.FormBorderStyle=FormBorderStyle.FixedDialog;
    }

    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    protected override void Dispose( bool disposing ) {
      if( disposing ) {
        if(components != null) {
          components.Dispose();
        }
      }
      base.Dispose( disposing );
    }

    #region Windows Form Designer generated code
    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
      this.components = new System.ComponentModel.Container();
      this.Size = new System.Drawing.Size(300,300);
    }
    #endregion

    public Hashtable NodeProperties {
      set  {
        nodeProperties=value;
      }
    }

    private void FormFileProperty_Load(object sender, System.EventArgs e) {
      FetchGeneralProperty();
    }

    private void FetchGeneralProperty()  {
        MessageBox.Show(nodeProperties["NodePath"].ToString());
    }

  }
}
  • Add new windows form to project <MMCTest> and name it as <FormFileProperty>. Replace the code of the form with the code below
C#
//
//Code block for FormFileProperty//
//
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;

namespace MMCTest 
{
  /// <summary>
  /// This is a sample form which opens up when menu button "FileAct Properties" 
  /// is clicked over the selected node's list/result item.
  /// </summary>
  public class FormFileProperty : System.Windows.Forms.Form {
    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.Container components=null;
    private Hashtable nodeProperties;
    
    public FormFileProperty () {
      //
      // Required for Windows Form Designer support
      //
      InitializeComponent();

      //
      // TODO: Add any constructor code after InitializeComponent call
      //
      this.Icon=null;
      this.Width=384;
      this.Height=440;
      this.MaximizeBox=false;
      this.MinimizeBox=false;

      this.Text="File Properties";

      this.ShowInTaskbar=false;
      this.StartPosition=FormStartPosition.CenterParent;
      this.FormBorderStyle=FormBorderStyle.FixedDialog;
    }

    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    protected override void Dispose( bool disposing ) {
      if( disposing ) {
        if(components != null) {
          components.Dispose();
        }
      }
      base.Dispose( disposing );
    }

    #region Windows Form Designer generated code
    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
      this.components = new System.ComponentModel.Container();
      this.Size = new System.Drawing.Size(300,300);
    }
    #endregion

    public Hashtable NodeProperties {
      set  {
        nodeProperties=value;
      }
    }

    private void FormFileProperty_Load(object sender, System.EventArgs e) {
      FetchGeneralProperty();
    }

    private void FetchGeneralProperty()  {
        MessageBox.Show(nodeProperties["NodePath"].ToString() + 
           "\\"+nodeProperties["NodeResultName"].ToString());
    }

  }
}
  • Add new .cs file into project <MMCTest> and name it as <FileActSnapinElement.cs>. Replace the code of the class file with the code below
C#
//
//Code block for FileActSnapinElement.cs
// 
using System;
using System.IO;
using System.Data;
using System.Drawing;
using System.Collections;
using System.Configuration;
using System.Runtime.InteropServices;
using MMCTest;
using Ironring.Management.MMC;


namespace MMCTest {
  /// <summary>
  /// This is the class which uses the DirecotryNode created above 
  /// to populate it on to the MMC.
  /// </summary>
  public class FileActSnapinElement {
    public FileActSnapinElement() {
    }
    public void CreateDirectoryNode(SnapinBase snapinBase, 
          string parentDirectory, BaseNode parentNode) {
      DirectoryInfo directoryInfo=new DirectoryInfo(parentDirectory);

      //Create DirectoryNode with all the default properties set
      DirectoryNode directoryNode=new DirectoryNode(snapinBase);
      directoryNode.DisplayName=directoryInfo.Name;
      directoryNode.Properties.Add("NodePath", directoryInfo.Parent.FullName 
            + "\\" + directoryInfo.Name);
      directoryNode.Properties.Add("NodeResultName","");
      directoryNode.Properties.Add("EventTarget","");
      directoryNode.CreateDataTableStructure();
      directoryNode.FillDataTableValues();
      parentNode.AddChild(directoryNode);

      //Add Menu and EventHandler for Menu for DirectoryNode
      MenuItem topMenuItemFileActProperties=new MenuItem("FileAct Properties", 
          "FileAct Properties for selected the item", 
          new MenuCommandHandler(OnMenuFileActPropertiesClick));
      directoryNode.AddTopMenuItem(topMenuItemFileActProperties);
      MenuItem topMenuItemRefresh=new MenuItem("Refresh", "Refresh the contents",  
          new MenuCommandHandler(OnMenuRefreshClick));
      directoryNode.AddTopMenuItem(topMenuItemRefresh);

      //Add other Event Handlers for DirectoryNode
      directoryNode.OnSelectScopeEvent += 
          new NodeNotificationHandler(OnSelectScopeEvent);
      directoryNode.OnSelectResultEvent += 
          new NodeNotificationHandler(OnSelectResultEvent);

      foreach(DirectoryInfo subDirectoryInfo in directoryInfo.GetDirectories()) {
        CreateDirectoryNode(snapinBase, subDirectoryInfo.Parent.FullName + 
             "\\" + subDirectoryInfo.Name, directoryNode);
      }
    }

    //**********************************************************************//
    //Event Handlers
    //Handles events generated by nodes and the result pane
    //
    //**********************************************************************//

    //**********************************************************************//
    //MMC Event Handlers
    //**********************************************************************//
    /// <summary>
    /// This method is called when the node element is selected.
    /// </summary>
    protected void OnSelectScopeEvent(object sender, NodeEventArgs args) {
      DirectoryNode directoryNode=(DirectoryNode) sender;
      directoryNode.Properties["EventTarget"]="Node";
    }

    /// <summary>
    /// This method is called when the node's list/result item is selected.
    /// </summary>
    protected void OnSelectResultEvent(object sender, NodeEventArgs args) {
      DirectoryNode directoryNode=(DirectoryNode) sender;
      directoryNode.Properties["EventTarget"]="NodeResult";
    }

    //**********************************************************************//
    //Menu Event Handlers
    //**********************************************************************//
    /// <summary>
    /// This method is called when menu item "FileAct Properties" is clicked.
    /// </summary>
    public void OnMenuFileActPropertiesClick(object sender, BaseNode arg) {
      DirectoryNode directoryNode=(DirectoryNode)arg;
      uint pItemID;
      IConsole2 console=directoryNode.Snapin.ResultViewConsole;
      IResultData resultData=console as IResultData;
      RESULTDATAITEM resultDataItem=new RESULTDATAITEM();

      try  {
        if(directoryNode.Properties["EventTarget"].ToString().Equals("Node")) {
          FormDirectoryProperty formDirectoryProperty=new FormDirectoryProperty();
          formDirectoryProperty.NodeProperties=directoryNode.Properties;
          formDirectoryProperty.ShowDialog();
        }
        else {
          for(int loopCounter=0; loopCounter<directoryNode.Table.Rows.Count; 
               loopCounter++) {
            resultData.FindItemByLParam(directoryNode.Cookie | 
               (loopCounter+1 << 16), out pItemID);
            resultDataItem.nCol=0;
            resultDataItem.mask=(uint)RDI.STATE | (uint)RDI.PARAM;
            resultDataItem.itemID=(int)pItemID;
            resultData.GetItem(ref resultDataItem);
            if(resultDataItem.nState==3) {
              directoryNode.Properties["NodeResultName"] =
                directoryNode.Table.Rows[loopCounter][0];
              break;
            }
          }

          FormFileProperty formFileProperty=new FormFileProperty();
          formFileProperty.NodeProperties=directoryNode.Properties;
          formFileProperty.ShowDialog();
        }
      }
      catch(Exception ex)  {
        throw ex;
      }
    }

    /// <summary>
    /// This method is called when menu item "Refresh" is clicked.
    /// </summary>
    public void OnMenuRefreshClick(object sender, BaseNode arg) {
      string selectedFileName=null;
      DirectoryNode directoryNode=(DirectoryNode)arg;
      uint pItemID;
      IConsole2 console=directoryNode.Snapin.ResultViewConsole;
      IResultData resultData=console as IResultData;
      RESULTDATAITEM resultDataItem=new RESULTDATAITEM();

      try  {
        if(!directoryNode.Properties["EventTarget"].ToString().Equals("Node")) {
          for(int loopCounter=0; loopCounter<directoryNode.Table.Rows.Count; 
               loopCounter++) {
            resultData.FindItemByLParam(directoryNode.Cookie | 
                 (loopCounter+1 << 16), out pItemID);
            resultDataItem.nCol=0;
            resultDataItem.mask=(uint)RDI.STATE | (uint)RDI.PARAM;
            resultDataItem.itemID=(int)pItemID;
            resultData.GetItem(ref resultDataItem);
            if(resultDataItem.nState==3) {
              selectedFileName =
                 directoryNode.Table.Rows[loopCounter][0].ToString();
              break;
            }
          }
        }

        resultData.DeleteAllRsltItems();
        directoryNode.Table.Rows.Clear();
        directoryNode.Table.AcceptChanges();
        directoryNode.FillDataTableValues();
        directoryNode.OnShowData(resultData);

        if(!directoryNode.Properties["EventTarget"].ToString().Equals("Node")) {
          for(int loopCounter=0; loopCounter<directoryNode.Table.Rows.Count; 
                loopCounter++) {
            if(directoryNode.Table.Rows[loopCounter][0].ToString().Equals(
                  selectedFileName))
            {
              resultData.FindItemByLParam(directoryNode.Cookie | 
                      (loopCounter+1 << 16), out pItemID);
              resultDataItem.mask=(uint)RDI.STATE;
              resultDataItem.itemID=(int)pItemID;
              resultDataItem.nState=3;
              resultData.SetItem(ref resultDataItem);
              break;
            }
          }
        }
      }
      catch(Exception ex)  {
        throw ex;
      }
    }

  }
}
  • Open <TestSnapin.cs> file in project <MMCTest>
  • Add the below piece of code at the end of constructor <TestSnapin>
C#
FileActSnapinElement fileActSnapinElement=new FileActSnapinElement();
fileActSnapinElement.CreateDirectoryNode(this,@"C:\FileActDemo",rootnode);
  • Build the solution
  • Execute the project to see the addition of new node just created

Points of Interest

There is lot more work to be done to the Framework to include functionalities to display help, to incorporate multiselection option, task pads etc. You can join the team at ironringsoftware to incorporate more functionalities to the framework and help the product to be more efficient.

History

  • Initial version - 12-Nov-2003

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
India India
I am software engineer from Bangalore, India, I have worked in .net technologies for small companies to the MNC like DELL. I like to share my knowledge with pals and gain from you all. Thanks!!!. My Contact - lhaish4k@yahoo.com

Comments and Discussions

 
QuestionAdd context menu to existing snap in Pin
Adnan Shaheen5-Aug-14 9:06
Adnan Shaheen5-Aug-14 9:06 
Newswin7 debug ok! Pin
pophelix6-Nov-09 16:47
pophelix6-Nov-09 16:47 
GeneralNew Task Pad Pin
Member 444242030-Mar-09 4:48
Member 444242030-Mar-09 4:48 
QuestionMMC Library as Remote client [modified] Pin
ArsenMkrt20-Sep-07 0:38
ArsenMkrt20-Sep-07 0:38 
QuestionHow to install dll manually Pin
gayane_darbinyan12-Sep-07 20:02
gayane_darbinyan12-Sep-07 20:02 
AnswerRe: How to install dll manually Pin
Levon Levonian29-Oct-07 13:12
Levon Levonian29-Oct-07 13:12 
GeneralI have some questions about .net (C#) MMC snap-in programming. Pin
Byung-Jin Han1-Jun-07 18:53
Byung-Jin Han1-Jun-07 18:53 
GeneralPlease fix those links. Pin
Sean Ewington1-Jun-07 8:39
staffSean Ewington1-Jun-07 8:39 
Generalnot able to download MMC stuff from Jim Pin
Wenjie Wang19-Apr-07 15:34
Wenjie Wang19-Apr-07 15:34 
QuestionAre you going to write more parts Pin
li_robert14-Mar-07 4:57
li_robert14-Mar-07 4:57 
QuestionTab index is totally ignored and is backwards? Pin
Nikolaj Rasmussen14-Jun-05 22:25
sussNikolaj Rasmussen14-Jun-05 22:25 
GeneralSnap-in failed to initialize Pin
TheGuy2-Dec-03 8:27
TheGuy2-Dec-03 8:27 
GeneralRe: Snap-in failed to initialize Pin
Harish Kumar L11-Dec-03 17:54
Harish Kumar L11-Dec-03 17:54 
GeneralRe: Snap-in failed to initialize Pin
garnold26-Jan-04 21:23
garnold26-Jan-04 21:23 
GeneralRe: Snap-in failed to initialize Pin
Stephen Viswaraj14-Jun-04 23:31
Stephen Viswaraj14-Jun-04 23:31 
Through MMC, When i try to open my application, it gives me the error,
"Snap-in failed to initialize.
Name : **********
CLSID : #####-######-######".

i removed the key from Registry and restarted, rebuilded the project.
But, Still it gives the error.

Thanks in advance. Rose | [Rose]

Stephen Viswa Raj.
GeneralRe: Snap-in failed to initialize Pin
sandipkshinde20-Sep-05 21:03
sandipkshinde20-Sep-05 21:03 
QuestionAdding nodes dynamically? Pin
alexn1-Dec-03 11:06
alexn1-Dec-03 11:06 
AnswerRe: Adding nodes dynamically? Pin
alexn1-Dec-03 11:30
alexn1-Dec-03 11:30 
GeneralReportNode Selected Item Pin
ASA00680618-Nov-03 11:58
ASA00680618-Nov-03 11:58 
GeneralOpenServer PropertySheet Pin
ASA00680617-Nov-03 11:48
ASA00680617-Nov-03 11:48 
GeneralRe: OpenServer PropertySheet Pin
Harish Kumar L18-Nov-03 1:04
Harish Kumar L18-Nov-03 1:04 
GeneralSourceForge.NET MMC Library Project launched!! Pin
Kocha14-Nov-03 2:23
Kocha14-Nov-03 2: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.