Click here to Skip to main content
15,881,803 members
Articles / Programming Languages / C#

A Visual Studio Solution Cloner

Rate me:
Please Sign up or sign in to vote.
4.93/5 (15 votes)
24 Oct 2013MIT4 min read 53.6K   757   21   27
Creates copies of a Visual Studio C++ or C# solution

Introduction

The CloneSolution program was written for Visual Studio 2008 to create entire copies of a directory tree of either a Visual Studio C++ solution or a Visual Studio C# solution.

I wanted to experiment with several versions of a program. I found that copying the entire project directory and loading the copied project in Visual Studio caused problems, because, although in a different folder, a copied solution file contained the same Globally Unique Identifiers (GUIDs) as the original solution file.

I believe it will work with later versions of Visual Studio, although I have not tested this.

This project allows copying the files in a solution and projects as long as they are all under the same folder and making the necessary changes to the copied solution and project file GUIDs.

Using the Code

Usage

For any project created with the Visual Studio Wizard, the solution name is the name of the solution (.sln) file with the extension removed.

The command to generate another solution is in the form:

CloneSolution <NewSolutionName> <OriginalSolutionName> [PathToOldSolution]

will create a new solution in a folder named NewSolutionName that is at the same level in the directory tree as the path to the original solution.

If the program is run in the original folder, then the last parameter, PathToOldSolution, can be omitted.

Every occurrence of "OriginalSolutionName" in every file name and in every file's text will be changed to "NewSolutionName". Every occurrence of ORIGINALSOLUTIONNAME will be changed to "NEWSOLUTIONNAME".

The CloneSolution program will change the GUIDs to be new values, but GUIDs that were equal to each other before will still be equal, just different values.

GUIDs for external services, COM objects, and perhaps other GUIDs, should not be changed. If you identify any GUID that should not be changed, it can be added to a file named exclusion.txt. Each GUID must be on a new line in the file. The GUIDs must be in the form:

{5903BC26-D583-362A-619C-DA5CB9C74321}

I used file exclusion.txt years ago, but I haven't needed it for a long time. I forget whether file exclusion.txt must be copied to the original solution folder, but I think that is necessary. There should only be a need for one exclusion.txt file, so the code could be modified to load file exclusion.txt from a known location. I have not done that.

Example:

I cloned a copy of the CloneSolution program with the following command: (I modified the path for security reasons).

CloneSolution BizarreSolution CloneSolution C:\Users\MyUserName\Projects\Tools\CloneSolution\

That command line created a folder C:\Users\MyUserName\Projects\Tools\BizarreSolution\ with the BizarreSolution files that are either identical, or modified, copies of the CloneSolution project files. This solution could be loaded and built successfully.

The SolutionCloner Class

The SolutionCloner class in file SolutionCloner.cs does all the heavy lifting. The parse arguments from the 'main' function in file Program.cs call the CloneSolution method of the SolutionCloner class instance. This recursively traverses the original solutions directories and files and creates identical directories and corresponding files for the new solution.

The SolutionCloner class is show here:

C#
//=======================================================================
// Copyright (C) 2013 William Hallahan
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software,
// and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//=======================================================================

using System;
using System.IO;
using System.Collections.Generic;

namespace CloneSolution
{
    /// <summary>
    /// This class clones an entire solution.
    /// </summary>
    public class SolutionCloner
    {
        protected string m_newSolutionName;
        protected string m_newSolutionNameUpperCase;
        protected string m_newSolutionNameLowerCase;

        protected string m_oldSolutionName;
        protected string m_oldSolutionNameUpperCase;
        protected string m_oldSolutionNameLowerCase;

        protected GuidReplacer m_guidReplacer;

        protected string m_sourceFileName;

        protected List<string> m_listOfFileExtensionsToSkip;
        protected List<string> m_listOfTextFileExtensions;

        #region constructor
        /// <summary>
        /// This constructor takes a new solution name and an old solution name.
        /// </summary>
        /// <param name="newSolutionName">The new name of the solution.</param>
        /// <param name="oldSolutionName">The old name of a solution.</param>
        public SolutionCloner(string newSolutionName, string oldSolutionName)
        {
            m_newSolutionName = newSolutionName;
            m_newSolutionNameUpperCase = newSolutionName.ToUpper();
            m_newSolutionNameLowerCase = newSolutionName.ToLower();

            m_oldSolutionName = oldSolutionName;
            m_oldSolutionNameUpperCase = oldSolutionName.ToUpper();
            m_oldSolutionNameLowerCase = oldSolutionName.ToLower();

            m_guidReplacer = new GuidReplacer();

            // Create a list of file extensions to skip.
            m_listOfFileExtensionsToSkip = new List<String>();
            m_listOfFileExtensionsToSkip.Add(".suo");
            m_listOfFileExtensionsToSkip.Add(".pdb");
            m_listOfFileExtensionsToSkip.Add(".tlb");
            m_listOfFileExtensionsToSkip.Add(".cache");
            m_listOfFileExtensionsToSkip.Add(".resource");
            m_listOfFileExtensionsToSkip.Add(".ilk");
            m_listOfFileExtensionsToSkip.Add(".ncb");
            m_listOfFileExtensionsToSkip.Add(".plg");
            m_listOfFileExtensionsToSkip.Add(".exe");
            m_listOfFileExtensionsToSkip.Add(".obj");
            m_listOfFileExtensionsToSkip.Add(".sbr");
            m_listOfFileExtensionsToSkip.Add(".aps");
            m_listOfFileExtensionsToSkip.Add(".res");
            m_listOfFileExtensionsToSkip.Add(".pch");
            m_listOfFileExtensionsToSkip.Add(".idb");
            m_listOfFileExtensionsToSkip.Add(".bsc");
            m_listOfFileExtensionsToSkip.Add(".dep");
            m_listOfFileExtensionsToSkip.Add(".intermediate");
            m_listOfFileExtensionsToSkip.Add(".user");
            m_listOfFileExtensionsToSkip.Add(".embed");
            m_listOfFileExtensionsToSkip.Add(".zip");

            // Create a list of file extensions to be copied as text files.
            m_listOfTextFileExtensions = new List<String>();
            m_listOfTextFileExtensions.Add(".sln");
            m_listOfTextFileExtensions.Add(".csproj");
            m_listOfTextFileExtensions.Add(".vcproj");
            m_listOfTextFileExtensions.Add(".c");
            m_listOfTextFileExtensions.Add(".cs");
            m_listOfTextFileExtensions.Add(".c");
            m_listOfTextFileExtensions.Add(".cpp");
            m_listOfTextFileExtensions.Add(".cc");
            m_listOfTextFileExtensions.Add(".h");
            m_listOfTextFileExtensions.Add(".hh");
            m_listOfTextFileExtensions.Add(".hpp");
            m_listOfTextFileExtensions.Add(".resx");
            m_listOfTextFileExtensions.Add(".rc");
            m_listOfTextFileExtensions.Add(".rc2");
            m_listOfTextFileExtensions.Add(".def");
            m_listOfTextFileExtensions.Add(".idl");
            m_listOfTextFileExtensions.Add(".odl");
            m_listOfTextFileExtensions.Add(".rgs");
            m_listOfTextFileExtensions.Add(".reg");
            m_listOfTextFileExtensions.Add(".txt");
            m_listOfTextFileExtensions.Add(".log");
            m_listOfTextFileExtensions.Add(".fml");
            m_listOfTextFileExtensions.Add(".xml");
            m_listOfTextFileExtensions.Add(".settings");
            m_listOfTextFileExtensions.Add(".config");
        }
        #endregion

        /// <summary>
        /// This is the main method to clone a solution.
        /// </summary>
        /// <param name="destinationPath">
        /// The path to where the new solution will be located.</param>
        /// <param name="sourcePath">
        /// The path to where the old solution to be copied is located.</param>
        public void CloneSolution(string destinationPath, string sourcePath)
        {
            DirectoryInfo sourceDirectoryInfo = new DirectoryInfo(sourcePath);
            DirectoryInfo destDirectoryInfo = new DirectoryInfo(destinationPath);

            CopyDirectory(destDirectoryInfo, sourceDirectoryInfo);
        }

        /// <summary>
        /// This method copies a directory and recursively copies all subdirectories.
        /// </summary>
        /// <param name="destDirectoryInfo">
        /// Specifies the current directory where items are to be coped to.</param>
        /// <param name="sourceDirectoryInfo">
        /// Specified the current directory where items are to be copied from.</param>
        protected void CopyDirectory(DirectoryInfo destDirectoryInfo, DirectoryInfo sourceDirectoryInfo)
        {
            // If the destination directory does not exist, then create it.
            string destDirectoryFullName = destDirectoryInfo.FullName;
            // If the old solution name is in the path, then replace it with the new name.
            destDirectoryFullName = destDirectoryFullName.Replace(m_oldSolutionName, m_newSolutionName);

            if (!Directory.Exists(destDirectoryFullName))
            {
                Directory.CreateDirectory(destDirectoryFullName);
            }

            // Clone all the files in the directory.
            foreach (FileInfo fileInfo in sourceDirectoryInfo.GetFiles())
            {
                m_sourceFileName = fileInfo.Name;
                // Replace the old solution name string with the new solution name string to get the destination file name.
                string destinationFileName = m_sourceFileName;
                int position = destinationFileName.IndexOf(m_oldSolutionName);
                if (position > -1)
                {
                    destinationFileName = destinationFileName.Replace(m_oldSolutionName, m_newSolutionName);
                }

                string sourcePathFileName = Path.Combine(sourceDirectoryInfo.FullName, m_sourceFileName);
                string destinationPathFileName = Path.Combine(destDirectoryFullName, destinationFileName);
                CopyFile(destinationPathFileName, sourcePathFileName);
            }

            // Traverse each subdirectory and recursively descend to clone each subdirectory.
            foreach (DirectoryInfo sourceSubDirectoryInfo in sourceDirectoryInfo.GetDirectories())
            {
                DirectoryInfo destinationSubDirectoryInfo = 
                    destDirectoryInfo.CreateSubdirectory(sourceSubDirectoryInfo.Name);
                CopyDirectory(destinationSubDirectoryInfo, sourceSubDirectoryInfo);
            }
        }

        /// <summary>
        /// This method copies a file. Certain file types are skipped, and not copied.
        /// </summary>
        /// <param name="destinationPathFileName">
        /// The destination path and name for the copied file.</param>
        /// <param name="sourcePathFileName">
        /// The path and name of the file to be copied.</param>
        protected void CopyFile(string destinationPathFileName, string sourcePathFileName)
        {
            string sourceFileExtension = Path.GetExtension(sourcePathFileName).ToLower();

            // Skip copying files that have certain file extensions.
            if (!m_listOfFileExtensionsToSkip.Contains(sourceFileExtension))
            {
                string sourceFileName = Path.GetFileName(sourcePathFileName);

                // Check the file extension to test whether to do a binary copy,
                // or to copy the file as a text file.
                if (m_listOfTextFileExtensions.Contains(sourceFileExtension))
                {
                    CopyFileAndChangeGuids(destinationPathFileName, sourcePathFileName);
                }
                else
                {
                    CopyBinaryFile(destinationPathFileName, sourcePathFileName);
                }
            }
        }

        /// <summary>
        /// This method copies special text files, such as project files, solution files, and code.
        /// </summary>
        /// <param name="destinationPathFileName">
        /// The destination path and name for the copied file.</param>
        /// <param name="sourcePathFileName">The path and name of the file to be copied.</param>
        protected void CopyFileAndChangeGuids(string destinationPathFileName, string sourcePathFileName)
        {
            // Open the input file and the output file.
            using (FileStream sourceStream = new FileStream(sourcePathFileName,
                                                            FileMode.Open,
                                                            FileAccess.Read,
                                                            System.IO.FileShare.ReadWrite))
            using (FileStream destinationStream = new FileStream(destinationPathFileName,
                                                                 FileMode.Create,
                                                                 FileAccess.Write,
                                                                 System.IO.FileShare.ReadWrite))
            using (StreamReader sourceStreamReader = new StreamReader(sourceStream))
            using (StreamWriter destStreamWriter = new StreamWriter(destinationStream))
            //using (StreamReader sourceStreamReader = new StreamReader(sourcePathFileName))
            //using (StreamWriter destStreamWriter = new StreamWriter(destinationPathFileName))
            {
                string lineOfText = string.Empty;

                while ((lineOfText = sourceStreamReader.ReadLine()) != null)
                {
                    lineOfText = lineOfText.Replace(m_oldSolutionName, m_newSolutionName);
                    lineOfText = lineOfText.Replace(m_oldSolutionNameUpperCase, m_newSolutionNameUpperCase);
                    lineOfText = lineOfText.Replace(m_oldSolutionNameLowerCase, m_newSolutionNameLowerCase);
                    // Don't change certain GUIDs.
                    if (!lineOfText.Contains("<Service Include=\"{"))
                    {
                        lineOfText = m_guidReplacer.ChangeGuids(lineOfText);
                    }
                    destStreamWriter.WriteLine(lineOfText);
                }
            }
        }

        /// <summary>
        /// This method copies binary files. 
        /// Binary files are just copied by the system, and only the title can be modified.
        /// </summary>
        /// <param name="destinationPathFileName">
        /// The destination path and name for the copied file.</param>
        /// <param name="sourcePathFileName">The path and name of the file to be copied.</param>
        protected void CopyBinaryFile(string destinationPathFileName, string sourcePathFileName)
        {
            using (FileStream sourceStream = new FileStream(sourcePathFileName,
                                                            FileMode.Open,
                                                            FileAccess.Read,
                                                            System.IO.FileShare.ReadWrite))
            using (FileStream destinationStream = new FileStream(destinationPathFileName,
                                                                 FileMode.Create,
                                                                 FileAccess.Write,
                                                                 System.IO.FileShare.ReadWrite))
            {
                BinaryReader reader = new BinaryReader(sourceStream);
                BinaryWriter writer = new BinaryWriter(destinationStream);

                // Create a buffer for the file data.
                byte[] buffer = new Byte[1024];
                int bytesRead = 0;

                // Read bytes from the source stream and write the bytes to the destination stream.
                while ((bytesRead = sourceStream.Read(buffer, 0, 1024)) > 0)
                {
                    destinationStream.Write(buffer, 0, bytesRead);
                }
            }
        }
    }
}

It would be trivial to modify this program to handle other .NET languages merely by adding the file extension for the project file to the SolutionCloner.cs file.

The Main Program

The 'main' function is in the file Program.cs. This does a simple argument parsing and calls the CloneSolution method of the SolutionCloner class.

C#
//=======================================================================
// Copyright (C) 2013 William Hallahan
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software,
// and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//=======================================================================

using System;
using System.IO;

namespace CloneSolution
{
    class Program
    {
        static void Main(string[] args)
        {
            // args[0] contains the new solution name.
            if (args.Length < 2)
            {
                if (args.Length < 1)
                {
                    Console.WriteLine("Usage:");
                    Console.WriteLine("");
                    Console.WriteLine("    CloneSolution NewSolutionName OldSolutionName <path>");
                    Console.WriteLine("");
                    Console.WriteLine("where <path> is an optional path to the old solution directory,");
                    Console.WriteLine("e.g. C:\\Projects\\SolutionName.  If <path> is omitted, then the");
                    Console.WriteLine("current directory is used for the path to the solution.");
                    Console.WriteLine("The new solution directory is always created in the parent directory");
                    Console.WriteLine("of the existing solution directory, i.e. the new solution has the");
                    Console.WriteLine("same parent folder as the old solution.");
                    Console.WriteLine("");
                    Console.WriteLine("All project folders and files are duplicated, and the solution name");
                    Console.WriteLine("is replaced with the new name, both for file names, and for text files.");
                    Console.WriteLine("contents. Solution, project, and assembly GUIDS, are all updated in a");
                    Console.WriteLine("correct and consistent fashion.  DLLs are copied, but not modified.");
                    Console.WriteLine("");
                    Console.WriteLine("");
                }
                else
                {
                    Console.WriteLine("No new solution name was specified.");
                }
            }
            else
            {
                string newSolutionName = args[0];
                string oldSolutionName = args[1];

                string sourcePath = Directory.GetCurrentDirectory();

                // If a third argument is not specified, then use the current directory for the source path.
                if (args.Length > 2)
                {
                    sourcePath = args[2];
                }
                else
                {
                    sourcePath = Directory.GetCurrentDirectory();
                }

                // Make sure the source path exists.
                if (!Directory.Exists(sourcePath))
                {
                    Console.WriteLine("The source path does not exist.");
                }
                else
                {
                    // Use the source path, and the new solution name, to calculate the destination path.
                    DirectoryInfo sourceDirectoryInfo = new DirectoryInfo(sourcePath);
                    DirectoryInfo parentDirectoryInfo = sourceDirectoryInfo.Parent;
                    string parentPath = parentDirectoryInfo.FullName;
                    string destinationPath = Path.Combine(parentPath, newSolutionName);

                    // Clone the solution.
                    SolutionCloner solutionCloner = new SolutionCloner(newSolutionName, oldSolutionName);
                    solutionCloner.CloneSolution(destinationPath, sourcePath);
                }
            }
        }
    }
}

Points of Interest

Warning: This program creates the NewSolutionName folder and overwrites the files in that folder. If you specify a folder that already has files, you might wipe out code. If you put the first two arguments to this program backwards, and the NewSolutionName folder already exists, you will overwrite the files you want to clone. Always be careful when you specify the arguments to the program. If you are not used to using this program, you might want to make a backup until you understand how to use it.

History

  • Initial article creation
  • Made extension lists and removed long if-statement for file extension checks. Fixed bad error message that stated the new solution name was missing, when it was actually the old solution name. Changed data members to start with 'm_' instead of just '_'. Other minor article text changes.
  • Minor update to fix the help prompt.

License

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


Written By
Software Developer (Senior)
United States United States
I'm an electrical engineer who has spend most of my career writing software. My background includes Digital Signal Processing, Multimedia programming, Robotics, Text-To-Speech, and Storage products. Most of the code that I've written is in C, C++ and Python. I know Object Oriented Design and I'm a proponent of Design Patterns.

My hobbies include writing software for fun, amateur radio, chess, and performing magic, mostly for charities.

Comments and Discussions

 
QuestionHaving trouble with VS 2019 Xamarin projects Pin
mreinslc9-Jun-20 17:11
mreinslc9-Jun-20 17:11 
GeneralVS2010 trial Pin
Dmitri Ogorodnikov23-Dec-14 6:31
Dmitri Ogorodnikov23-Dec-14 6:31 
GeneralRe: VS2010 trial Pin
Bill_Hallahan26-Dec-14 8:39
Bill_Hallahan26-Dec-14 8:39 
GeneralRe: VS2010 trial Pin
Member 1379420923-Apr-18 7:32
Member 1379420923-Apr-18 7:32 
QuestionHow could I make a VB Clone Solution? Pin
Member 1031563028-May-14 1:45
Member 1031563028-May-14 1:45 
AnswerRe: How could I make a VB Clone Solution? Pin
Bill_Hallahan28-May-14 15:26
Bill_Hallahan28-May-14 15:26 
GeneralRe: How could I make a VB Clone Solution? Pin
Member 1031563028-May-14 21:06
Member 1031563028-May-14 21:06 
GeneralRe: How could I make a VB Clone Solution? Pin
Bill_Hallahan29-May-14 16:31
Bill_Hallahan29-May-14 16:31 
GeneralRe: How could I make a VB Clone Solution? Pin
Member 103156302-Jun-14 8:27
Member 103156302-Jun-14 8:27 
GeneralMy vote of 5 Pin
sprice869-Apr-14 0:39
professionalsprice869-Apr-14 0:39 
GeneralThoughts Pin
PIEBALDconsult1-Nov-13 6:17
mvePIEBALDconsult1-Nov-13 6:17 
GeneralRe: Thoughts Pin
Bill_Hallahan1-Nov-13 9:11
Bill_Hallahan1-Nov-13 9:11 
GeneralRe: Thoughts Pin
PIEBALDconsult1-Nov-13 9:52
mvePIEBALDconsult1-Nov-13 9:52 
GeneralRe: Thoughts Pin
Bill_Hallahan1-Nov-13 16:38
Bill_Hallahan1-Nov-13 16:38 
GeneralRe: Thoughts Pin
Bill_Hallahan1-Nov-13 17:10
Bill_Hallahan1-Nov-13 17:10 
Ah, I just caught your "copied the manual way" and realized what you meant.

Doing a manual copy would copy a lot of files that should not be copied, such as intermediate linker files, and special internal Visual Studio files such as .ncb files, and .suo files, and more. These troublesome files would have to be manually deleted before opening the solution. I'm not sure which files caused problems, so I just eliminated all internal files that Visual Studio will generate automatically. I suspect some of these files contain paths to the original solution.

In addition, even without these problematic files, it would then be two step process instead of one. Having the program do the copy is more efficient, and it solves the aforementioned problem.
GeneralRe: Thoughts Pin
PIEBALDconsult1-Nov-13 17:31
mvePIEBALDconsult1-Nov-13 17:31 
GeneralRe: Thoughts Pin
Bill_Hallahan2-Nov-13 5:57
Bill_Hallahan2-Nov-13 5:57 
GeneralRe: Thoughts Pin
PIEBALDconsult2-Nov-13 6:33
mvePIEBALDconsult2-Nov-13 6:33 
GeneralRe: Thoughts Pin
Bill_Hallahan3-Nov-13 6:29
Bill_Hallahan3-Nov-13 6:29 
GeneralRe: Thoughts Pin
PIEBALDconsult3-Nov-13 7:59
mvePIEBALDconsult3-Nov-13 7:59 
Questionnice Pin
BillW331-Nov-13 6:03
professionalBillW331-Nov-13 6:03 
AnswerRe: nice Pin
Bill_Hallahan1-Nov-13 9:11
Bill_Hallahan1-Nov-13 9:11 
GeneralMy vote of 5 Pin
fredatcodeproject25-Oct-13 2:27
professionalfredatcodeproject25-Oct-13 2:27 
GeneralRe: My vote of 5 Pin
Bill_Hallahan25-Oct-13 10:20
Bill_Hallahan25-Oct-13 10:20 
SuggestionUse Linq to symplify test Pin
Eric Jaume21-Oct-13 21:22
professionalEric Jaume21-Oct-13 21:22 

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.