Click here to Skip to main content
15,861,168 members
Articles / Hosted Services / Azure

Walkthrough: Creating a O365 SharePoint 2013 App with ASP.NET MVC 5

Rate me:
Please Sign up or sign in to vote.
4.78/5 (31 votes)
20 May 2014CPOL19 min read 175.5K   53   39
Guide to creating SharePoint 2013 Autohosted Apps with ASP.NET MVC 5.

Update: Autohosted apps are no more. You should check out my new article on creating a Provider-hosted app. Alternatively, read on, but be careful to use Provider-hosted features instead of Autohosted. Thanks!

Introduction

SharePoint apps are not at all like the SharePoint solutions we used to deal with. With the App model, your code is a completely self-contained web application running on a web server far away from the SharePoint server. This presents a number of downsides – gone are the days of using the Server object model and elevating privileges to hack in a solution to whatever problem faces you. But this article isn't about dwelling on the past (much…); it's about how to write a cleanly architected app based on MVC 5 using Visual Studio 2013. And that in itself will make programming for SharePoint much faster and cleaner (in theory!).

Why Apps?

You can still write a traditional SharePoint solution. But that may not be a good idea:

  • Solutions aren't future-proofed: they are deprecated and might not be supported in future versions of SharePoint
  • SharePoint online only supports sandboxed solutions and they're very highly restricted
  • Apps can be listed on the Office Store for easy deployment to O365
  • Apps can be written in any web technology – not necessarily .NET – using open web standards such as OAuth.

Click here for more app propaganda!

So assuming you've decided to write an app, what's next? A big, initial architectural decision you'll need to make is around the hosting model: there are three options and they depend on the functionality you're planning to provide, and the target audience for your app:

  • SharePoint-hosted
    Your app will contain only client-side code. You can include custom lists and web parts and interact with SharePoint using the JavaScript client-side object model (CSOM). Since your JavaScript executes in the context of the SharePoint domain, security is trivial.
  • Provider-hosted
    Your app will have server-side code that is hosted on another server. This server could be your own (if you want to host the app), or it could be set up within a client's premises (in the case of a super secure government custom on-premise app, for example).
  • Autohosted
    This is a special one! The architecture is similar to provider-hosted – the app has server-side code, and again, runs on a machine outside SharePoint. However, that machine is on Azure. And you don't need to worry about any of the Azure deployment details – just mark your app as 'autohosted', and when you install it, an Azure instance is automatically created and connected up – as if by magic.
    Note that as of December 2013, autohosted apps are still not yet accepted into the Office Store. You can still send people your app package to install manually though, so it's more a question of exposure.

Check out this MSDN article for more in-depth coverage of the hosting options.

This article is going to focus on the creation of an Autohosted app for SharePoint 2013 online.

Prerequisites

You will need:

  • Visual Studio 2013
    You can develop apps with Visual Studio 2012 + Office Developer Tools , however the MVC project templates won't be available.
  • SharePoint Online (O365) with a developer site configured (ignore the stuff about Napa, we've got Visual Studio :-)). Autohosted apps will only work with SharePoint Online on Office 365. If you wish to build against your own installation of SharePoint (on-premise) then you will need to build a provider-hosted app. However, many of the concepts in this article (not involving authentication) will still apply.

Creating the Solution

We're going to create a pretty silly app called AnimalApp. This is going to allow our users to manage a list of animals. So useful! Features are:

  • CRUD operations on the list
  • Users only see their own animals
  • Administrators can see everyone's animals
  • Storage is in a database table

Now, it would probably be sensible to store the list in a SharePoint list. But I wanted to show how the database connection string is handled in autohosted apps.

Let's get started! Click File -> New -> Project and choose App for SharePoint 2013:

Image 1

Choose autohosted and fill in the address of your developer site:

Image 2

On the next screen, select ASP.NET MVC Web Application and click finish. This creates two projects for you:

  • AnimalApp
    This project contains the part of your app that is hosted on SharePoint. It contains any definitions for custom lists or web parts, and defines what permissions your app needs.
  • AnimalAppWeb
    This is the web project and represents what is going to be installed on Azure. This project contains your code!

Take a look in the AnimalAppWeb project. A lot of code is generated for us! The main areas we're concerned with are:

  • Controllers: the C in MVC! Handles interaction between the model and view (the database and the web page in our case)
  • Filters: a handy way of executing code when particular pages are rendered. We'll use one to make sure the user is logged into SharePoint at the start of each request
  • Models: the M in MVC… in our case, a class to represent each database table
  • Scripts: We're not doing any scripting but we'll look at spcontext.js which has an important function.
  • Views: Contains a .cshtml file for each page – containing HTML and Razor view rendering code.

Without doing anything, you can hit F5 and deploy your app. You may need to login to your developer site. Once it's deployed a browser window will be opened and SharePoint will request that you approve your app:

Image 3

Only the basic permissions are requested by default. Click Trust It, and with any luck you'll be redirected to this:

Image 4

Securing Pages

Before we write any code, let's see how SharePoint security "happens". There are SharePointContext.cs and TokenHelper.cs files (see image above) that contain all of the security-related code – but how do they get called? The app authentication mechanism happens something like this:

Up-to-and-including-installation:

  1. You configure the permissions your app requires within the AppManifest.xml file in AnimalApp (note that effective permissions at runtime are those of the current user combined with those granted to your app)
  2. An administrator installs your app and confirms the app's requested permissions

Following installation, the flow is as follows:

  1. A SharePoint user clicks on your app icon
  2. SharePoint redirects the user to your app default page with a POST request containing all the details required for your app to authenticate back to SharePoint – including site URL and OAuth access token
  3. Your default page stores off all of this authentication data into the HTTP session1
  4. As the user navigates around your app, the SPHostUrl parameter is passed to every page to maintain a context (ie. What SharePoint site the app is currently dealing with). Passing this as a URL parameter ensures that if your app is accessed within the context of a number of different sites, that context is maintained correctly.

1 Point 3 was a little wooly. You don't really need to write any code to store off the authentication data – the project template handles this out of the box. Take a look in Controllers\HomeController.cs:

C#
public class HomeController : Controller
{
    [SharePointContextFilter]
    public ActionResult Index()
    {
        return View();
    }

    ...
}

The Index method above (i.e., your app home page) has a helpful annotation – SharePointContextFilter. This is defined in Filters\SharePointContextFilterAttribute.cs. Its job is to call into that SharePointContext code we mentioned and handle user checks. This is where the OAuth calls live – it handles the app access token validation and stores it off into the user session. Each time it's called it combines what is stored in the user session with the SPHostUrl and redirects to the login page if necessary.

Give the login mechanism a go – try loading the default page in a Chrome Incognito (or Internet Explorer InPrivate) tab and the login window is presented. Now open the 'About' page in a new tab – it won't require login, since it doesn't have the SharePointContextFilter attribute on its Controller method by default.

Using the SharePointContextFilter attribute

You can stick this attribute on any controller method you like! If you leave it out, then anyone can access that page. So if it's for users' eyes only, or you're interacting with SharePoint using the CSOM, then you need to add this attribute in.

Making SharePoint calls

So now that you know the user is logged in successfully, you can create a SharePoint context and make CSOM calls. This is pretty simple:

C#
var spContext = SharePointContextProvider.Current.GetSharePointContext(httpContextBase);
using (var clientContext = spContext.CreateUserClientContextForSPHost())
{
    if (clientContext != null)
    {
        //CSOM code
    }
}

Passing around the SPHostUrl

I mentioned that the SPHostUrl (the URL of the SharePoint site you're currently on, in our case, the developer site) is passed to every page. This is pretty much true: the script in Scripts\spcontext.js is loaded into each page (How? See App_Start\BundleConfig.cs and Global.asax.cs and connect the dots…). Once the script is loaded onto the page it performs a little bit of a hack – every URL on the page is appended with SPHostUrl parameter which is later read-in by the SharePointContextProvider code. And the cycle begins again!

Now, this presents a slight complication – if you have AJAX requests in your app, or a form in which you post data back to the server, then the SPHostUrl isn't going to be automatically included in those requests. The solution is simply to add it in manually. We're going to see this "bug in action when we add some functionality to our AnimalApp.

Adding an SQL Azure Database

As described earlier, our app is database powered – let's see how that all works!

Click File -> Add -> New Project and select Other Languages -> SQL Server Database Project .

Image 5

Once it's added, you need to point your app project at the database project. Do this by clicking on the AnimalApp project, and in the properties window, select AnimalDatabase under the SQL Database property.

Image 6

Visual Studio will helpfully offer to update your SQL Server project to target Azure SQL. Click Yes. Now your SQL Azure instance will be automatically configured along with the rest of your app upon deployment. Sweet!

Image 7

Now we'll add data to the database. Right click the AnimalDatabase project, click Add -> Table. Call it Animals and click Add.

Image 8

Add a Name column and a UserId column:

Image 9

This will store the animal name, and the User Id who inserted it.

If you want the Id to be automatically incremented (and we do), change the script for the Id column to contain the IDENTITY keyword.

Image 10

Generate Data Access Layer Code with Entity Framework

OK, so this isn't really SharePoint app related, and it's not MVC related, but it's cool so I'm mentioning it!

In ye olden times, you'd start writing a data access layer in your C# project to access your database. We're not going to do that: instead, we're going to use Entity Framework to generate all that code for us. You could alternatively use the Entity Framework "Code First" functionality to write your C# classes and have it generate the appropriate SQL table schema; I prefer writing the SQL myself.

You'll need to add Entity Framework Power Tools to Visual Studio at this point if you haven't already.

Click Tools -> Extensions and Updates and search for Entity Framework Power Tools. As of writing, the current version is Beta 4.

Image 11

Once that's installed, you can use it to generate a C# class for each database table. Note that if you'd added multiple tables, along with relationships (foreign keys and the like), the resultant C# classes will be created with members and collections as appropriate.

Before generating that code, you'll need to deploy your database locally, so right click on AnimalDatabase and click Publish.

Click Edit for the Target database connection and set it to "(localdb)\Projects. This corresponds to SQL Express on your developer machine but obviously you can use whatever SQL Server you have available.

Image 12

Click Publish. The Data Tools Operations window should tell you it's been published successfully. Now we can invoke Entity Framework – right click on the AnimalAppWeb project (the project we'll be accessing the database from) and click Reverse Engineer Code First.

Image 13

Again, enter the server name as (localdb)\Projects. Under 'Connect to a database', the AnimalDatabase should be present, so choose that. Click OK.

Image 14

Now, once it's finished generating all the loveliness, you'll notice that it has helpfully put all your data classes under the Models folder – right where they belong!

Image 15

At this point I'd like to point out that the classes generated are marked as partial. This is very helpful because you will probably want to extend them. For example, let's add a new constructor to the Animal class. Add a file in the same folder as Animal.cs and call it Animal_Partial.cs. Edit the code as follows:

C#
public partial class Animal
{
    public Animal() { }
 
    public Animal(string name, int userId)
    {
        this.Name = name;
        this.UserId = userId;
    }
}

Now when we re-run the Entity Framework code generation, our changes to the Animal class won't be overwritten.

Autohosted App SQL Connection String

Now we've got to sort out our connection string. SharePoint autohosted apps use a specific convention for connection strings: you define it in the web.config with the key SqlAzureConnectionString. This means that when your app is deployed and the database is installed to an Azure instance, the installer will automatically update the connection string to point to the dynamically deployed database. Clever! So add this setting to your web.config appSettings node:

XML
<add key="SqlAzureConnectionString" 
  value="Data Source=(localdb)\Projects;Initial Catalog=AnimalDatabase;Integrated 
    Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False" />

All you need to do here is change the Initial Catalog to be the name of your database.

Now, Entity Framework has created another class for us which we need to look at: the AnimalsDatabaseContext class which sets up the database connection for us. The base of this class, DbContext, accepts the connection string as a constructor argument, so we'll just add a new constructor and read in the value from the web.config. Add a new file, AnimalDatabaseContext_Partial and mark it partial like we did last time:

Image 16

I've also added a convenience function here for creating a new instance of the class by reading the connection string out of the web.config.

MVC Pages for Editing Animals

We're going to quickly add some pages to read/create/update/delete animals. Visual Studio will generate these for you based on your entity framework models that you created earlier.

Right click on the Controllers folder and go to Add Controller. Select "MVC5 Controller with views, using Entity Framework:

Image 17

Fill in the Add Controller dialog like this (it should be fairly self-explanatory):

Image 18

Once you click Add, lots of files will be generated and added to your solution:

  • A new class, AnimalController, is added to the Controllers folder. This is responsible for the create/read/update/delete operations on your database.
  • Under the Views/Animal folder, a page is created for each operation: Create.cshtml, Delete.cshtml, etc.

The only thing remaining for us to do is to add links to our new CRUD pages on the main navigation, which lives in Views/Shared/_Layout.cshtml.

Open that file and look for the section where the navbar is rendered:

Image 19

Add in links for your Animal pages:

Image 20

The three arguments for the ActionLink method are:

  • title – the string that gets rendered for the link
  • actionName – this corresponds to the method name in the AnimalController
  • controllerName – this should be Animal to match the controller we've just created.

Hit F5 and you'll be presented with some lovely CRUD pages. How easy was that??

Image 21

Securing the new pages

You may notice that you can use your new animal pages anonymously. To secure them, we'll apply the SharePointContextFilter to them.

Open the new controller file, AnimalController, and annotate each method with SharePointContextFilter:

Image 22

Now those pages are secured.

Passing SPHostUrl in post-backs

The complication with adding security to the new Animal CRUD pages is that they're going to need the SPHostUrl passed to them during post back (for example, when you add, edit, or delete an Animal). If you try and add a new animal, it'll be inserted successfully but then you'll be presented with this message:

Image 23

"Unknown User: Unable to determine your identity. Please try again by launching the app installed on your site".

Why is that? The reason is that (as you can see from the URL) the SPHostUrl parameter hasn't been passed along and therefore authentication has failed. This line is responsible (inside the Create method of AnimalController):

Image 24

What's happening is that the animal is created and inserted, but then we're redirecting back to the index page – and without the vital SPHostUrl parameter. We can fix that very simply by adding the parameter to the redirection:

C#
return RedirectToAction("Index", 
  new { SPHostUrl = SharePointContext.GetSPHostUrl(HttpContext.Request).AbsoluteUri });

Here we're simply adding the SPHostUrl as a URL parameter to the request for the Index page. You should add this parameter to any RedirectToAction call where the target page performs SharePoint authentication.

Different Content for Different Users

We're going to configure our pages so that users can only see their own animals.

Let's first do a little bit of tidy up. Remove the UserId column from the views:

Image 25

Above is the relevant code to remove from the Create.cshtml page. You should locate and remove the relevant UserId code from Delete.cshtml and Edit.cshtml too. We'll leave Details.cshtml and Index.cshtml for now; we're going to display the UserId for administrators in a later step.

Now, we're going to manually set the User Id, on creation, to the current SharePoint user. Firstly we need to retrieve the User Id – the place that makes most sense to do this is in the Filters\SharePointContextFilterAttribute.cs class – it is, after all, already executed for any secured page. And this is the same point at which we know a user is successfully logged in.

Add the following method:

C#
private void GetSPUserDetails(HttpContextBase httpContextBase, dynamic viewBag)
{
    var spContext = SharePointContextProvider.Current.GetSharePointContext(httpContextBase);
    using (var clientContext = spContext.CreateUserClientContextForSPHost())
    {
        if (clientContext != null)
        {
            User spUser = clientContext.Web.CurrentUser;
            clientContext.Load(spUser, user => user.Title, user => user.Id);
            clientContext.ExecuteQuery();
 
            viewBag.UserName = spUser.Title;
            viewBag.UserId = spUser.Id;
        }
    }
}

This simply requests user details from SharePoint using the CSOM, and applies a couple of properties – UserName and UserId to the supplied viewbag. You can call this method from the OnActionExecution method like this:

Image 26

We're passing in the current HttpContext, so that we can create a SharePoint context, and also the ViewBag – the ViewBag is used as a convenient location for caching data which is accessible both from the View and the Controller.

Then open AnimalController and update Index() to filter Animals for the current user ID:

C#
public ActionResult Index()
{
    int userId = ViewBag.UserId;
    return View(db.Animals.Where(a => a.UserId == userId).ToList());
}

Next, update the Create method to set the user id on creation:

Image 27

Note that we've also removed UserId from the bound properties; we removed it from the HTML page earlier. You should also remove it from the Edit method.

Run the solution again – now users can only see their own animals.

SharePoint Styling

SharePoint apps generally match the styling of the site on which they're installed. In this step, we'll add that styling and remove some of our MVC-default styling.

Firstly, we're going to add the SPHostUrl to the ViewBag. The reason will become apparently in a minute. Add it within the GetSPUserDetails method we created earlier:

Image 28

Note that we're trimming the final slash off, as we're going to concatenate another URL portion on to it.

Open up Views\Shared\_Layout.cshtml and add this code to the header:

HTML
<link href='@ViewBag.SPHostUrl/_layouts/15/defaultcss.ashx' type='text/css' rel='stylesheet' />
<script src='@ViewBag.SPHostUrl/_layouts/15/SP.UI.Controls.js'></script>

This will pull in the CSS and controls script from your SharePoint server. This is cool because now your app will match the styling of your SharePoint site! The goal is to make your app blend in as much as possible.

If you run your app now you can actually already see that the fonts have changed to match your SharePoint site.

Rendering the SharePoint Navigation Bar

Again in the _Layout.cshtml file, we're going to start by importing jQuery:

HTML
@Scripts.Render("~/bundles/jquery")

jQuery is actually already imported, but at the bottom of the file. So remove that. Alternatively, you can just ensure that any reference to jQuery is after the import at the bottom.

Then add the script to render the top bar whenever the page loads:

JavaScript
$(function () {
    var options = {
        appHelpPageUrl: '@Url.Action("About","Home")',
        appIconUrl: "AppIcon_Blue.png",
        appTitle: "MVC5 app",
        settingsLinks: [
            {
                linkUrl: '@Url.Action("Contact","Home")',
                displayName: "Contact"
            },
        ]
    };

    var nav = new SP.UI.Controls.Navigation("chrome_ctrl_container", options);
    nav.setVisible(true);
});

Notice here that we're rendering the Contact page as an option under the Settings menu. This is really just for illustration; contact isn't a setting, I know!! I've also added an Img folder to my project, and a 96x96 pixel image to use as my app icon.

Next, add the div tag that will indicate where to render the navigation bar. This should be the first element under body.

HTML
<body>
<div id="chrome_ctrl_container"></div>

Now, at this stage we're into the realm of CSS. You might want to get a designer involved! We are essentially combining the MVC5 default CSS with the SharePoint CSS and app navigation bar. To make it look semi-decent, I had to do the following:

  • Open Content/Site.css and remove padding-top: 50px on body
  • In _Layout.cshtml, remove the div tag with class navbar-header.
  • Remove the classes navbar-inverse and navbar-fixed-top from the MVC navbar div tag.
  • Remove the classes navbar-collapse and collapse.

Your body HTML should begin something like this:

HTML
<body>
    <div id="chrome_ctrl_container"></div>
    <div class="navbar">
        <div class="container">
            <ul class="nav navbar-nav">
                <li>@Html.ActionLink("Create Animal", "Create", "Animal")</li>
                <li>@Html.ActionLink("View Animals", "Index", "Animal")</li>
            </ul>
        </div>
    </div>
    ...
</body>

With your app looking like this:

Image 29

Future work

This article is getting too long already, so I'm going to stop here! However, there are a few things that I'd love to include:

Event receivers

Event receivers in apps are a pretty tough concept; I mean, Microsoft will tell you they're easy, but I haven't had a lot of luck. You need to implement a WCF web service (hence they are called remote event receivers) which SharePoint calls when the event occurs. You register that service as an event receiver during app installation. Click here for details on creating an app event receiver.

Handling Custom Lists

This isn't really too difficult and isn't really related to MVC so I've not included it. You can add a custom list that gets created when your app is installed (right-click your app project, Add -> New Item -> List), and interacting with it is a CSOM matter.

Web Parts

A web part in the app world is simply a web page inside an iFrame – which is placed on a SharePoint page. So the best idea may be to write a controller specifically for your web part and then create an associated view.

Download Solution

Click here to download a zip file containing the sample code. It's pretty big (16MB), because of the plethora of dependencies it includes!

The End

There are quite a few little caveats that you need to overcome to make a proper SharePoint 2013 app out of an MVC application. I hope it's helpful - please let me know in the comments! Happy coding!

License

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


Written By
Software Developer Repstor Ltd
United Kingdom United Kingdom
I am a Product Architect at Repstor.

Repstor custodian and Repstor Provisioning Engine provide case management and provisioning for SharePoint.

Repstor affinity provides uninterrupted access to content systems, like SharePoint through the familiar interface of Microsoft Outlook.

Comments and Discussions

 
GeneralEADESIGN Pin
Member 1286494822-Nov-16 7:55
Member 1286494822-Nov-16 7:55 
QuestionUninstall app for sharepoint: The remote server returned an error: (417) expectation failed Pin
Member 81885997-Oct-15 19:59
Member 81885997-Oct-15 19:59 
QuestionWebpart - Editform Pin
FransVanEk24-Oct-14 4:48
FransVanEk24-Oct-14 4:48 
QuestionLocal IIS doesn't pass the SPHostUrl token Pin
JCPuerto3-Apr-14 8:21
JCPuerto3-Apr-14 8:21 
AnswerRe: Local IIS doesn't pass the SPHostUrl token Pin
Jonathan Cardy8-Apr-14 4:40
Jonathan Cardy8-Apr-14 4:40 
QuestionDeployment Pin
Member 1070169226-Mar-14 8:10
Member 1070169226-Mar-14 8:10 
AnswerRe: Deployment Pin
Jonathan Cardy8-Apr-14 4:37
Jonathan Cardy8-Apr-14 4:37 
QuestionAnimal App as an App Part: This content cannot be displayed in a frame Pin
Lawrence Armour22-Feb-14 6:30
Lawrence Armour22-Feb-14 6:30 
AnswerRe: Animal App as an App Part: This content cannot be displayed in a frame Pin
Jonathan Cardy22-Feb-14 19:40
Jonathan Cardy22-Feb-14 19:40 
GeneralRe: Animal App as an App Part: This content cannot be displayed in a frame Pin
Lawrence Armour24-Feb-14 3:10
Lawrence Armour24-Feb-14 3:10 
GeneralRe: Animal App as an App Part: This content cannot be displayed in a frame Pin
Jonathan Cardy24-Feb-14 3:58
Jonathan Cardy24-Feb-14 3:58 
GeneralRe: Animal App as an App Part: This content cannot be displayed in a frame Pin
Lawrence Armour25-Feb-14 3:06
Lawrence Armour25-Feb-14 3:06 
QuestionAnonymous access Pin
Member 95622959-Feb-14 17:14
Member 95622959-Feb-14 17:14 
AnswerRe: Anonymous access Pin
Jonathan Cardy9-Feb-14 22:23
Jonathan Cardy9-Feb-14 22:23 
AnswerRe: Anonymous access Pin
Carlos A Bueno5-May-14 2:59
Carlos A Bueno5-May-14 2:59 
QuestionUnknown User error when deployed Pin
m1nhae3-Feb-14 10:36
m1nhae3-Feb-14 10:36 
AnswerRe: Unknown User error when deployed Pin
Jonathan Cardy3-Feb-14 22:40
Jonathan Cardy3-Feb-14 22:40 
QuestionDeployed App Pin
Member 38647463-Feb-14 1:09
Member 38647463-Feb-14 1:09 
AnswerRe: Deployed App Pin
Jonathan Cardy3-Feb-14 1:16
Jonathan Cardy3-Feb-14 1:16 
GeneralRe: Deployed App Pin
Lawrence Armour8-Feb-14 9:47
Lawrence Armour8-Feb-14 9:47 
GeneralRe: Deployed App Pin
Jonathan Cardy8-Feb-14 11:00
Jonathan Cardy8-Feb-14 11:00 
GeneralRe: Deployed App Pin
Lawrence Armour9-Feb-14 4:13
Lawrence Armour9-Feb-14 4:13 
QuestionHow do I make the App MVC based Pin
Member 386474630-Jan-14 2:17
Member 386474630-Jan-14 2:17 
AnswerRe: How do I make the App MVC based Pin
Jonathan Cardy30-Jan-14 3:34
Jonathan Cardy30-Jan-14 3:34 
GeneralRe: How do I make the App MVC based Pin
Member 386474630-Jan-14 5:44
Member 386474630-Jan-14 5:44 

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.