Click here to Skip to main content
15,868,113 members
Articles / Web Development / XHTML

Web 2.0 AJAX Portal using jQuery, ASP.NET 3.5, Silverlight, Linq to SQL, WF and Unity

Rate me:
Please Sign up or sign in to vote.
4.96/5 (51 votes)
11 Jun 2011CPOL32 min read 279K   1   266   46
Web 2.0 AJAX Portal built using jQuery, and ASP.NET 3.5. It offers Silverlight widget framework. Middle-tier built on Workflow Foundation. Data Access Layer uses Compiled Linq to SQL. Uses Enterprise Library 4.1 and Unitiy, offering Dependency Injection and Inversion of Control. All hot stuff!

Introduction

Dropthings – my open source Web 2.0 Ajax Portal has gone through a technology overhauling. Previously it was built using ASP.NET AJAX, a little bit of Workflow Foundation and Linq to SQL. Now Dropthings boasts full jQuery front-end combined with ASP.NET AJAX UpdatePanel, Silverlight widget, full Workflow Foundation implementation on the business layer, 100% Linq to SQL Compiled Queries on the data access layer, Dependency Injection and Inversion of Control (IoC) using Microsoft Enterprise Library 4.1 and Unity. It also has a ASP.NET AJAX Web Test framework that makes it real easy to write Web Tests that simulates real user actions on AJAX web pages. This article will walk you through the challenges in getting these new technologies to work in an ASP.NET website and how performance, scalability, extensibility and maintainability has significantly improved by the new technologies. Dropthings has been licensed for commercial use by prominent companies including BT Business, Intel, Microsoft IS, Denmark Government portal for Citizens; Startups like Limead and many more. So, this is serious stuff! There’s a very cool open source implementation of Dropthings framework available at National University of Singapore portal.

Visit: http://dropthings.omaralzabir.com.

Dropthings AJAX Portal

Warning before you read this article: There’s a CodeProject article that explains how this portal was built at the very first attempt, and then a feature rich version of this portal is explained in my book. This article shows you the newest technology implementations introduced in the latest version, for example, how drag-drop was implemented using jQuery. If you want to get a background on this portal, I suggest you read the book or earlier article first and then read this article. But if you are already an expert on portal technologies and well versed in .NET 3.5, and you want to learn some quick tips - just read this article.

Get the Source Code

The latest source code is hosted at Google code:

There’s a CodePlex site for documentation and issue tracking:

You will need Visual Studio 2008 Team Suite with Service Pack 1 and Silverlight 2 SDK in order to run all the projects. If you have only Visual Studio 2008 Professional, then you will have to remove the Dropthings.Test project.

image

New Features Introduced

Dropthings new release has the following features:

  • Template users – you can define a user whose pages and widgets are used as a template for new users. Whatever you put in that template user’s pages, it will be copied for every new user. Thus this is an easier way to define the default pages and widgets for new users. Similarly you can do the same for a registered user. The template users can be defined in the web.config.
  • Widget-to-Widget communication – Widgets can send messages to each other. Widgets can subscribe to an Event Broker and exchange messages using a Pub-Sub pattern.
  • WidgetZone – You can create any number of zones in any shape on the page. You can have widgets laid in horizontal layout, you can have zones on different places on the page and so on. With this zone model, you are no longer limited to the Page-Column model where you could only have N vertical columns.
  • Role based widgets – Now widgets are mapped to roles so that you can allow different users to see different widget list using ManageWidgetPersmission.aspx.
  • Role based page setup – You can define page setup for different roles. For example, Managers see different pages and widgets than Employees.
  • Widget maximize – You can maximize a widget to take full screen. Handy for widgets with lots of content.
  • Free form resize – You can freely resize widgets vertically.
  • Silverlight Widgets – You can now make widgets in Silverlight!

Why the Technology Overhauling

Performance, Scalability, Maintainability and Extensibility – four key reasons for the overhauling. Each new technology solved one or more of these problems.

First, jQuery was used to replace my personal hand-coded large amount of JavaScript code that offered the client side drag & drop and other UI effects. jQuery already has a rich set of library for Drag & Drop, Animations, Event handling, cross browser JavaScript framework and so on. So, using jQuery means opening the door to thousands of jQuery plugins to be offered on Dropthings. This made Dropthings highly extensible on the client side. Moreover, jQuery is very light. Unlike AJAX Control Toolkit jumbo sized framework and heavy control extenders, jQuery is very lean. So, total JavaScript size decreased significantly resulting in improved page load time. In total, the jQuery framework, AJAX basic framework, all my stuff is total 395KB, sweet! Performance is key; it makes or breaks a product.

Secondly, Linq to SQL queries are replaced with Compiled Queries. Dropthings did not survive a load test when regular lambda expressions were used to query database. I could only reach up to 12 Req/Sec using 20 concurrent users without burning up web server CPU on a Quad Core DELL server.

Thirdly, Workflow Foundation is used to build operations that require multiple Data Access Classes to perform together in a single transaction. Instead of writing large functions with many ifelse conditions, for…loops, it’s better to write them in a Workflow because you can visually see the flow of execution and you can reuse Activities among different Workflows. Best of all, architects can design workflows and developers can fill-in code inside activities. So, I could design complex operations in a workflow without writing the real code inside Activities and then ask someone else to implement each Activity. It is like handing over a design document to developers to implement each unit module, only that here everything is strongly typed and verified by the compiler. If you strictly follow Single Responsibility Principle for your Activities, which is a smart way of saying one Activity does only one and very simple task, you end up with a highly reusable and maintainable business layer and a very clean code that’s easily extensible.

Fourthly, Unity Dependency Injection (DI) framework is used to pave the path for unit testing and dependency injection. It offers Inversion of Control (IoC), which enables testing individual classes in isolation. Moreover, it has a handy feature to control lifetime of objects. Instead of creating instance of commonly used classes several times within the same request, you can make instances thread level, which means only one instance is created per thread and subsequent calls reuse the same instance. Are these going over your head? No worries, continue reading, I will explain later on.

Fifthly, enabling API for Silverlight widgets allows more interactive widgets to be built using Silverlight. HTML and Javascripts still have limitations on smooth graphics and continuous transmission of data from web server. Silverlight solves all of these problems.

Linq to SQL Performance Improvement

The amount of effort that goes into building SQL from a Linq to SQL lambda expression is insane and it happens every single time a lambda expression executes. This extra overhead causes too much CPU consumption and it does not scale. DON’T GO LIVE WITH LAMBDA QUERIES while using Linq to SQL. Compiled Queries solved the problem. Although not as fast as regular SqlCommand and SqlDataReader, Compiled queries are quite close to them.

I did a load test that simulates the following actions:

  • Visit Dropthings as brand new user
  • Edit a widget setting
  • Add a new widget
  • Delete a widget
  • Logout and create a brand new user again

I used Visual Studio Web Test to prepare such a script. You can find a web test file in Dropthings.Test project.

image

The load test was run on a single Quad Core box with 8 GB RAM and regular SATA drives. It’s a Windows 2008 server and the site ran on IIS 7 and SQL Server 2008. Both web server and SQL Server were on the same box. The result of load test is as follows:

image

After compiled queries, the performance is much better. Some observations:

  • Request/sec is avg 37.9/sec. This is a lot since it means at this pace, you get 3.2 million requests per day. A lot!
  • Avg response time per request is 0.39 sec. Which is fast!
  • There is zero error produced, which means we do not have scalability problems like thread deadlocks, database contention, etc.
  • The request/sec is flat, this means we do not have common synchronization problems, multi-threading problems, database getting slower and slower issues, etc. When such problems happen, you will see the graph slowly goes downward.

You get much higher throughput, almost triple of what you see here when you separate out Web server and SQL Server because both of them fight for CPU.

So, we learned Linq to Sql is bad, unless you are using Compiled Query. If you are using Compiled Queries anyway which looks like a DAL function wrapping a Stored Proc, you aren’t enjoying the productivity of writing Linq to Sql Lambda expression everywhere in the code (without ever caring for database design, locks and indexes and thus producing suboptimal queries that will bring your site down one day). If you don’t have the productivity benefit, you don’t have a reason to use Linq to Sql unless you want to look cool among your peers.

Now, you say what about the compile time validation and strong typing? How many times you needed the compiler to tell you that you passed some wrong parameter type to a Linq Query? Probably once and if you had too much coffee, twice per query, but that’s it. So, are you going to risk producing suboptimal queries that produce lock contention, transaction deadlock, miss indexes just for some compile time validation?

jQuery Front-end for Drag & Drop Widgets

The drag & drop feature is now implemented using jQuery using the jQuery sortable plugin. You can drag & drop a widget from one row to another, or from one column to another column. Similarly you can drag a new widget from the widget gallery and drop it into a column to add a new widget.

image

image

All these cool behaviors are added by the following code:

C#
 var allZones = $('.' + zoneClass);

 var zone = $('#' + zoneId);
 zone.each(function() {
   var plugin = $(this).data('sortable');
   if (plugin) plugin.destroy();
 });

zone.sortable({
   //items: '> .widget:not(.nodrag)',
   items: '.' + widgetClass + ':not(.nodrag)',
   //handle: '.widget_header',
   handle: '.' + handleClass,
   cursor: 'move',
   appendTo: 'body',
   connectWith: allZones,
   placeholder: 'placeholder',
   start: function(e, ui) {
     ui.helper.css("width", ui.item.parent().outerWidth());
     ui.placeholder.height(ui.item.height());

     DropthingsUI.suspendPendingWidgetZoneUpdate();
   },
   change: function(e, ui) {
     if (ui.element) {
       var w = ui.element.width();
       ui.placeholder.width(w);
       ui.helper.css("width", w);

       if (ui.item != undefined) {
         ui.placeholder.height(ui.item.height());
       }
       else {
         //this is a new item from galarry
         ui.placeholder.height(200);
       }
     }
   },

Zones are areas on the page where you can put widgets. On Dropthings, there are three widget zones inside the three columns. Each column contains one zone. Each zone contains one or more widgets. Each widget has two parts – the header and the body. You can drag a widget by holding onto its header. You can drag & reorder widgets within a zone and you can move widget from one zone to another zone. The above script does both – it allows reordering and moving widgets from one zone to another.

image

First the script finds out all the zones on the page using a special class .widget_zone. All three zone divs have this class set to them. Then it hooks the sortable plugin to a specific zone using the ID of the zone. This process is repeated for each zone which is not shown here.

First it removes sortable plugin from the zone if a plugin was already attached. This prevents adding duplicate plugin to the same zone. Then it initializes the sortable plugin where it specifies the items that are draggable - the widgets with class .widget, then the class of the handle of each widget which can be used to drag the widget, then using connectWith to allZones, it allows the plugin to exchange widgets between all zones.

On the start event, when Drag starts, the widget being dragged is set to a fixed width equal to the width of the column and a fixed height so that the widget does not suddenly grow or collapse in width or height when it becomes absolute positioned.

Then on change event, a placeholder, which is the dotted rectangle shown during drag, is resized to the size of the widget being dragged.

The real work, however, is in the stop event, where a widget is dropped.

C#
stop: function(e, ui) {
  var position = ui.item.parent()
      .children()
      .index(ui.item);

  var widgetZone = ui.item.parents('.' + zoneClass + ':first');
  var containerId = parseInt(widgetZone.attr(
  DropthingsUI.Attributes.ZONE_ID));

  if (ui.item.hasClass(newWidgetClass)) {
    //new item has been dropped into the sortable list
    var widgetId = ui.item.attr('id').match(/\d+/);

    // OMAR: Create a dummy widget placeholder while the real widget loads
    var templateData = { title: $(ui.item).text() };
    var widgetTemplateNode = $("#new_widget_template").clone();
    widgetTemplateNode.drink(templateData);
    widgetTemplateNode.insertBefore(ui.item);

    DropthingsUI.Actions.onWidgetAdd(widgetId[0], containerId, position,
      function() {
        DropthingsUI.updateWidgetZone(widgetZone);
      });
  }
  else {
    ui.item.css({ 'width': 'auto' });
    var instanceId = parseInt(ui.item.attr(DropthingsUI.Attributes.INSTANCE_ID));
    DropthingsUI.Actions.onDrop(containerId, instanceId, position, function() {
      DropthingsUI.updateWidgetZone(widgetZone);
    });
  }
}

There are two types of Drag & Drop handled here – one is the drag & drop of a widget from the widget gallery to a column in order to add a new widget and the other is drag & drop of an existing widget from one position to another position.

In the first case, when you drag a widget like “Flickr” from the widget gallery to one of the columns, it needs to create a new widget on that position. So, first it creates a dummy widget frame with a header and a blank body to give the user a feeling that a new widget has been created and it’s being loaded. The widget frame is created from a template using jQuery Micro Templating plugin. Then the whole widget zone goes through an async postback to refresh the zone so that the newly added widget appears fully loaded. The trick to async postback an UpdatePanel is to use a hidden LinkButton inside an UpdatePanel and programmatically simulate a click on it.

C#
asyncPostbackWidgetZone: function(widgetZone) {
  var postBackLink = widgetZone.parent().find(".dummyLink");
  eval(postBackLink.attr('href'));
},

In the second case, where an existing widget is moved from one column to another column , it finds the position where the new widget is dropped, then it calls a webservice to notify the server that a widget has been dropped on a position so that the server can rearrange the widgets on the column. After that, it queues a full zone refresh so that the ASP.NET controls are refreshed with new ID within the zone.

Now why do we need to refresh the zone by doing a async-postback? Since a widget can be moved from one column to another, the first part of the dynamically generated ID of each ASP.NET Control changes. For example, on column 1, the ID of a Button inside a widget is WidgetZone1001_Widget_1001_Button1 but when it’s dropped on second column, the ID becomes WidgetZone2002_Widget_2002_Button1. Unless we refresh both the columns where widgets changed position, the ID of the ASP.NET Controls remain the old ones. As a result, when they post back, they do not match with the server’s Control Tree and you get exception that there’s been some problem loading viewstate after postback.

Render UI on Browser using jQuery Micro Template Plugin

In AJAX applications, you have to give immediate feedback to users on some action while an expensive server call executes and response comes back. This becomes challenging to render immediate feedback with real data if you have to construct those HTML using browser DOM operations or building large HTML string and populating with dynamic data. jQuery Micro Templating comes to the rescue. You can embed the HTML for such UI snippets, that you use to give immediate feedback to user actions while server call executes, inside the page output. Then you can construct HTML with dynamic data using those templates. For example, in Dropthings, when you drag & drop a new widget, it shows you a realistic widget frame where the body shows “loading…” as if the real widget has already been added and it’s loading body content.

image

But in reality, the widget is fake, the zone is going through an async-postback and after the postback, the zone will refresh with all the widgets. Since it’s an expensive operation, the delay was unacceptable and some immediate feedback was necessary. So, we embedded the HTML for a dummy widget inside the Default.aspx and show it on the column where you drop a new widget.

XML
<!-- Template for a new widget placeholder -->
<!-- Begin template -->
<div class="nodisplay">
    <div ID="new_widget_template" class="widget">
        <div class="widget_header">
            <table class="widget_header_table" cellspacing="0" cellpadding="0">
                <tbody>
                    <tr>
                        <td class="widget_title">
<a class="widget_title_label">
<!=json.title !>
</a>
                        </td>
                        <td class="widget_edit"><a class="widget_edit">edit</a></td>
                        <td class="widget_button"><a class="widget_close widget_box">
x</a></td>
                    </tr>
                </tbody>
            </table>            
        </div>
        <div ID="WidgetResizeFrame" class="widget_resize_frame" >
            <div class="widget_body">
                Loading widget...
            </div>
        </div>            
    </div>
</div>
<!-- End template -->
</form>

Here the HTML snippet for a dummy widget is hidden inside an invisible div. The following code takes this template and injects inside a column when you drag & drop a new widget from the widget gallery:

C#
// OMAR: Create a summy widget placeholder while the real widget loads
var templateData = { title: $(ui.item).text() };
var widgetTemplateNode = $("#new_widget_template").clone();
widgetTemplateNode.drink(templateData);
widgetTemplateNode.insertBefore(ui.item);

First you construct a data object that holds some properties that you want to pass to the HTML template. For example, here we are building an object that has the title field. This title is used inside the template as <!=json.title !> and it gets replaced with the value when the plugin processes the template. Then you clone the template node that contains the entire template so that you can reuse it again for another clone operation. The cloned node is then passed through the drink function of jquery micro template library. This function drinks the cloned node HTML and replaces the template instruction using the values from the fields and spits out the output HTML inside the cloned node. So, json.title gets replaced with templateData.title. Finally the cloned node is inserted at the right position where user dropped the new widget.

jQuery Animations

jQuery has rich animation library which is used in Dropthings for effects like opening the widget gallery when you “Add Widget” or closing a widget. You will see things come and go smoothly and slowly. It’s very easy to do:

C#
$('#Widget_Gallery').show("slow");

This slowly shows the widget gallery.

A similar animation is used to smoothly close a widget when you click the cross button on a widget header. You will see the widget shrinks to nothing smoothly. This is done using the following script:

C#
widgetCloseButton
    .unbind('click')
    .bind('click', function() {
        widget.hide('slow');
    });

Perform jQuery Actions and UpdatePanel Postback on the Same Click

Sometimes you have to bind to click events on button or links in order to perform some jQuery action and then you want to execute the original postback operation of that button or link. If you bind to click event using jQuery, on different browsers, either the jQuery code does not execute or the postback does not execute. So, if you want to control the sequence of operations – like first jQuery operation and then the postback, you have to take this approach:

C#
widgetCloseButton
    .unbind('click')
    .bind('click', function() {
        widget.hide('slow');
        eval(widgetCloseButton.attr("href"));
        return false;
    });

The close button is a hyperlink that has a doPostback JavaScript like the following:

JavaScript
javascript:__doPostBack('WidgetPage$WidgetZone9205$WidgetContainer20930$CloseWidget','')

So, we first clear any click event listener calling .unbind, then attach a new click event listener that first hides the widget slowly in a smooth animation and then executes the doPostback JavaScript specified in the href attribute of the hyperlink. Finally it returns false to stop propagation of event so that browser does not execute the script in href attribute again.

Making jQuery Work with Async Postbacks

When an UpdatePanel performs an async-postback, it clears its inner HTML and then recreates the inner HTML from the HTML sent from server. So, all UI elements that were present inside the UpdatePanel are first removed and then recreated.

If you have attached some plugin or event handler using jQuery to HTML elements that are inside an UpdatePanel, every time the UpdatePanel performs an async postback, all the plugins and event handlers are gone. Since UpdatePanel recreates the body from the HTML it receives from an async-postback, none of the elements that are hooked by jQuery remains on the browser DOM. So, after every async-postback, you have to call the same JavaScript code again to hook the elements.

When you click on the “Add Stuff” link, you will see the widget gallery grows smoothly. It’s done using the jQuery animation. When you click the Add Stuff button, a Panel with a DataList of Widget links gets created inside an UpdatePanel. Since the Panel did not exist before, we need to deliver the jQuery scripts for the Panel when the click event fires on server.

The best place to do such an operation is from OnPreRender. Since it fires after firing all the control events, you can be pretty sure the Control Tree is now fully built and it’s ready to be converted to HTML. Since all the controls are going to be rendered again, regardless of whether they were already on the UI before or not, you need to hook jQuery animations, events and other behavior all over again to each control. However, make sure you check for Visible = true because only visible controls are emitted to the response HTML. If it’s not visible, it will not be part of the HTML being sent out to the browser and thus JavaScript code trying to hook onto the element will fail.

C#
protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);

    if (this.AddContentPanel.Visible)
        ScriptManager.RegisterStartupScript(this.AddContentPanel, typeof(Panel),
            "ShowAddContentPanel" + DateTime.Now.Ticks.ToString(),
            "DropthingsUI.showWidgetGallery();", true);
}

A note in specifying the control in the RegisterStartupScript, it has to be a control that is inside the UpdatePanel that is going through an async postback and it must be visible. You cannot just hook this to any arbitrary control. Also you have to generate a unique key if you want the script to execute after the async-postback completes every time it happens. If the key is not unique, once it is executed in the lifetime of the page, it won’t execute again. For example, if I did not produce unique key on every pre-render here, you would only be able to see the Add Stuff widget list once in page lifetime.

Embedding JSON in Page Output and Eliminate AJAX Calls

If your page is making a lot of AJAX calls during page load, it gives a slow and sluggish page load experience. Unlike a flat HTML page, it does not instantly load in one shot when parts of the content are loaded via AJAX calls. As the number of AJAX call grows, the performance gets noticeable bad. Although it’s a good idea to incrementally load large pages by making AJAX calls to load different part of the pages after the main page has been loaded, but sometimes this worsens the perceived speed of the page.

In Dropthings, every widget makes its own AJAX call to fetch the content from server. It’s good that the main page gets delivered instantly without waiting for all the widgets to load. But then AJAX calls take place to load widget content and widgets load one by one. Now, the AJAX calls can only happen when browser has finished loading the javascript frameworks – including Microsoft AJAX and jQuery. So, there’s a big delay between the page getting rendered on the browser without any widget content and then all the widgets starting to load their content. You have to stare at 6 “Loading…” message for a while and then see the widgets load one at a time. It feels slower than a regular static HTML page loading in one shot.

If the page were to load in one shot with all the widgets’ content in reasonable time, it would have been a better experience for user. So, we had to eliminate so many AJAX calls. For first attempt to do so, we made the RSS Widget such a way that it checks if it already has the RSS feed in server’s cache and if it’s there, embed the JSON of the RSS feed inside a <SCRIPT> block. When the RSS widget initializes on the client side, it picks up the embedded JSON and instead of making an AJAX call, it just uses the JSON to render the RSS links.

On the OnPreRender event of the RSS widget, it checks if the RSS being requested is already on server cache. If it is, it gets the RSS, converts to JSON and then embeds it in a script block.

C#
protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);
        ...
    var cachedJSON = GetCachedJSON();

    ScriptManager.RegisterStartupScript(this, typeof(Widgets_FastFlickrWidget),
             "LoadFlickr" + this.ClientID,
        string.Format("window.flickrLoader{0} = new 
             FastFlickrWidget('{1}', '{2}', '{3}', '{4}', {5});
                 window.flickrLoader{0}.load();",
            this._Host.ID, this.GetPhotoUrl(), this.FlickrPhotoPanel.ClientID,
            this.ShowPrevious.ClientID, this.ShowNext.ClientID,
                 cachedJSON ?? "null"), true);
         ...
}
private string GetCachedJSON()
{
    if (ProxyAsync.IsUrlInCache(Cache, this.GetPhotoUrl()))
    {
        var cachedString = new ProxyAsync().GetString(this.GetPhotoUrl(), 10);
        string json = new System.Web.Script.Serialization.JavaScriptSerializer()
                  .Serialize(cachedString);
        return json;
    }
    else
        return null;
}

Here the code loads the FastRssWidget.js, which contains the code for the RSS widget, and then creates an instance of FastRssWidget passing the cached JSON. The GetCachedJSON function checks if the RSS feed XML is already in server cache. If it’s available, it serializes the RSS feed XML into JSON.

JavaScript
var FastRssWidget = function(url, container, count, cachedJson)
{
    this.url = url; this.container = container; this.count = count; 
    this.cachedJson = cachedJson;
}
FastRssWidget.prototype = {
    load : function()
    {
        if( this.cachedJson == null )
        {
            var div = $get( this.container );
            div.innerHTML = "Loading...";
            
            Proxy.GetRss( this.url, this.count, 10, 
                 Function.createDelegate( this, this.onContentLoad ) );
        }
        else
        {
            this.onContentLoad(this.cachedJson);
        }
    },

Here the FastRssWidget checks if cachedJson is already available. If it’s available, it does not make a call to Proxy.GetRss to fetch RSS from server.

Now you can only do such embedding when the content you are trying to get is available to you in a very fast data source – like server cache, local file or in local database. If the content is somewhere far, like some other website, then you should not do this because fetching content from far is going to add a significant delay in loading the page. Such delay will result in the user staring at a white screen for a noticeable period after hitting the URL on the browser.

Ideally your page should respond from server within 500ms. This means Time-To-First-Byte (TTFB) needs to be within 500ms, but the whole page can take longer to download and render. This TTFB means how long it takes IIS to prepare the page output and start delivering it to the browser. If it’s taking more than 500ms, they find out what is taking so much time. You might be loading too much data from database, or you might be loading data from some external website that needs to be cached.

Silverlight Widgets

You can now build Silverlight widgets, WooHoo! Here’s an example Silverlight widget:

image

This is an example widget taken from ScottGu’s blog.

Building a Silverlight widget requires you to deal with State. Every widget has its own State, which is an XML where you can store arbitrary data for the widget. For example, in this widget, it remembers the search phrase. So, it stores it in State. Now Silverlight by default does not have a server side persistence API. So, Dropthings framework provides that support. If you have read my earlier article, every widget gets to store its own information in a State property that is an XML. Similarly, Silverlight widget can do that as well. It can call webservice to get and save state.

So, when the Silverlight widget loads, it gets the state either by calling Webservice or by taking it from InitialParams.

JavaScript
void OnLoaded(object sender, RoutedEventArgs e)
{
    GetState();
}
C#
private void GetState()
{
    App myApp = Application.Current as App;

    if (myApp.InitParams.ContainsKey("WidgetId"))
    {
        // OMAR: State is passed as InitParameters. 
        // Use that to prevent a costly roundtrip
        //int WidgetId = Convert.ToInt32(myApp.InitParams["WidgetId"]);
        //DropthingsWebService.WidgetServiceSoapClient service = 
             new DropthingsWebService.WidgetServiceSoapClient();
        //service.GetWidgetStateCompleted += 
             new EventHandler<DropthingsWebService.GetWidgetStateCompletedEventArgs>(
                service_GetWidgetStateCompleted);
        //service.GetWidgetStateAsync(WidgetId);

        this._State = XElement.Parse(myApp.InitParams["State"]);

        txtSearchTopic.Text = this.Topic;
        DoSearch();
    }
}

Here, the InitParams contains WidgetId, which is the unique identifier for the widget in database, and then the State serialized as string in the InitParams.

Similarly, you can call webservice to store the modified State.

C#
private void SaveState()
{
    App myApp = Application.Current as App;

    if (myApp.InitParams.ContainsKey("WidgetId"))
    {
        int WidgetId = Convert.ToInt32(myApp.InitParams["WidgetId"]);
        DropthingsWebService.WidgetServiceSoapClient service = 
             new DropthingsWebService.WidgetServiceSoapClient();
        service.SaveWidgetStateAsync(WidgetId, State.ToString());
    }
}

Very simple!

Now how does the State get into InitParams and how does the Silverlight control get hosted? After you have built a Silverlight Control, you will have to build a Widget Control, which is a regular ASCX but it just implements the IWidget interface.

The HTML markup for the widget is simple, just host a Silverlight control:

ASP.NET
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="DiggWidget.ascx.cs" 
Inherits="Widgets_DiggWidget" %>
    
<%@ Register Assembly="System.Web.Silverlight" 
Namespace="System.Web.UI.SilverlightControls"
    TagPrefix="asp" %>
<asp:Panel ID="SettingsPanel" runat="server" Visible="false">

</asp:Panel>
<div style="height:100%"><div style="height:450px; position:relative;">
    <asp:Silverlight ID="diggXaml" runat="server" 
Source="~/ClientBin/Dropthing.Silverlight.xap?v=1" 
MinimumVersion="2.0.30923.0" Width="100%" Height="100%">
        <PluginNotInstalledTemplate>
            Silverlight Plugin not installed. 
        </PluginNotInstalledTemplate>
    </asp:Silverlight></div>
</div>

Then the server side code just sets the State in the InitParams and that’s it:

C#
protected void Page_Load(object sender, EventArgs e)
{
    BindDiggData();
}
void IWidget.Init(IWidgetHost host)
{
    this._Host = host;
}
public void BindDiggData()
{
    diggXaml.InitParameters = "WidgetId={0}".FormatWith(this._Host.ID)
        + ",State={0}".FormatWith(this.State.Xml());
}
private XElement State
{
    get
    {
        string state = this._Host.GetState();
        if (string.IsNullOrEmpty(state))
            state = "<state><topic>football</topic></state>";
        if (_State == null) _State = XElement.Parse(state);
        return _State;
    }
}
That’s it!

Building the middle-tier using Workflow Foundation and Unity

Middle-tier is built on top of two of the most confusing, complicated, hard to debug, yet powerful, extensible and maintainable technologies – Workflow Foundation (WF) and Dependency Injection (DI).

Now I assume you have already used Workflows and if not, please do some reading and read my earlier article on building Dropthings where I explained how workflows are used in the middle tier. My book also explains Workflows in detail and many tips and tricks on using Workflow Foundation.

First there’s a WorkflowHelper class that makes synchronous execution of workflows in ASP.NET environment piece of cake. You can read how this helper class works from my blog posts:

What’s changed is the WorkflowHelper now implements a IWorkflowHelper interface that allows us to Dependency Inject it.

C#
public class WorkflowHelper : Dropthings.Business.Workflows.IWorkflowHelper
{

Now Dependency Injection is a big topic. You can learn about it doing some Google search on “dependency injection”. I won’t teach you here how to use DI but I will give you some tips on DI. I strongly suggest you do some reading on DI because this technology is phenomenal.

I have used Microsoft Enterprise Library 4.1 and Unity framework for the Dependency Injection. The Unity Application Block (Unity) is a lightweight extensible dependency injection container with support for constructor, property, and method call injection. Instead of using Unity directly throughout my code, I have built a wrapper around it so that I can change DI framework anytime later on if it’s not working for me.

C#
using Microsoft.Practices.Unity;

public class ObjectContainer
{
    private static readonly IUnityContainer _container = new UnityContainer();
    public static void Dispose()
    {
        if (null != _container)
        {
            _container.Dispose();
        }
    }
    public static void RegisterInstanceExternalLifetime<TInterface>(
       TInterface instance)
    {
        _container.RegisterInstance<TInterface>(instance, 
                new ExternallyControlledLifetimeManager());
    }
    public static void RegisterInstanceExternalLifetime<TInterface>(string name, 
       TInterface instance)
    {
        _container.RegisterInstance<TInterface>(name, instance, 
                new ExternallyControlledLifetimeManager());
    }
    public static void RegisterInstancePerThread<TInterface>(TInterface instance)
    {
        _container.RegisterInstance<TInterface>(instance, 
                new PerThreadLifetimeManager());
    }

This ObjectContainer class exposes some meaningful methods to register types and instances like RegisterInstancePerThread, which makes more sense than Unity framework’s highly generic RegisterInstance functions. This class has a default startup method that registers the default types:

C#
public static void SetupDefaults(WorkflowRuntime runtime)
{
  RegisterInstanceExternalLifetime<WorkflowRuntime>(runtime);
  RegisterTypePerThread<IWorkflowHelper, WorkflowHelper>();
}

This function shows two ways to use DI. You can register an existing instance of a class, for example, a singleton instance, and you can register a class against an interface so that whenever the interface is requested, the registered class gets created by the container.

Here an existing WorkflowRuntime instance is registered, which is stored in Application state, as there can be only one WorkflowRuntime per ASP.NET Application. Then the WorkflowHelper is registered to be thread specific so that one thread uses one instance of WorkflowHelper. Since we don’t have background threads launched from a request, we can safely reuse instance of a WorkflowHelper within the same ASP.NET thread.

Now that we have both WorkflowRuntime and WorkflowHelper registered with the Unity Container, we can anytime get their instance by calling ObjectContainer.Resolve<T>:

C#
var workflowRuntime = ObjectContainer.Resolve<WorkflowRuntime>(); 
var workflowHelper = ObjectContainer.Resolve<IWorkflowHelper>();
workflowHelper.ExecuteWorkflow<TWorkflow, TRequest, TResponse>(workflowRuntime);

ObjectContainer gives us the same WorkflowRuntime instance in all ASP.NET thread and unique instance of WorkflowHelper per ASP.NET thread.

This is the beauty of Dependency Inversion Containers – when you start using one, you stop using ClassName object = new ClassName(); in your code. You start writing IClassName object = Container.Resolve<IClassName>(); The benefit is, you can map an interface to a class during the Register call, and all your code start using the right class whenever you request the interface. You can control lifetime of objects, make objects singleton, per thread or new instance per call - all from a central place. This is such a configurable and powerful approach that I suggest you stop writing any business layer or data access layer without using a dependency injection container right now and go back and refactor all your existing classes to inherit from an interface and then replace all new ClassName() calls with Container.Resolve<Interface>(). Now you are wondering, what’s the real use of such an approach, why bother creating one instance for every class? Here’s a realistic scenario – say you have a data access class – CustomerData which uses HttpContext in some function (that’s bad, but anyway). Now you want to use the CustomerData class from a Windows Service. You can’t because there’s no HttpContext on Windows Service. So, what you do is, you write code against ICustomerData in both your web app and your windows service. Then you make two implementations of CustomerData – one is CustomerDataWeb and other is CustomerDataWinSvc, both of them implement ICustomerData. Then on Web Application_Start, you register ICustomerData with CustomerDataWeb, but in Windows Service Startup, you register ICustomerData with CustomerDataWinSvc. Another most common use for this is to register mock classes against the interfaces so that you can do unit test. So, you can have a CustomerDataMock that just returns dummy data and use that in Unit Test. You can write your unit test code against ICustomerData and you just register the mock CustomerDataMock to the interface and your unit test code runs without any issue.

If all this talk has gone over your head, don’t feel bad. It took me really long time to grasp the DI concept and understand how to effectively use DI. Dropthings is still far from being an ideal use of DI concept . I suggest you further read about Unity, see some articles on DI and check Dropthings code again to make sense of what’s going on.

Next is the workflows. The middle tier is entirely built on top of workflows. Every operation that user does is a synchronous workflow. For example, when a brand new user visits, this workflow runs:

image

This workflow does a lot of work:

  • Adds the new user to Guest role.
  • If there’s a template available for guest users, then clone that template for the new user running another workflow synchronously within this workflow.
  • If no template is available, then create two tabs and fill in the first tab with some widgets.

So, why is workflow used instead of standard business facades with large methods that orchestrate many DAL classes to do the job?

  • Workflows are like documentation of code, you can look at it and understand how things work. You don’t need a documentation to understand workflows if you are doing it right.
  • It forces you to build your system composed of reusable Activities. Each Activity is then forced to be a unit operation and be reusable, thus allowing maintainable code.
  • You can publish workflows directly via WCF service, that allows you to build an application service layer with near zero code change using the same workflows. This way you can separate out a web layer and an app service layer.
  • You can conveniently perform asynchronous operations within workflow since workflow foundation takes care of async invocation. No need to manage background threads yourselves.

Pretty cool benefits. One downside is, it takes more time to plan, design and code workflows than writing one gigantic function.

Poor Performance with Workflow Execution

There are two activities that makes Workflow execution unacceptably slow - the ForEachAcrtivity and the WhileActivity. Basically any activity that performs some loop, suffers from performance problem. During iteration, the Loop type Activity has to clone all the child activity that it contains so that it can create new instances of each Activity during each iteration. This cloning process is extremely slow. Here’s how ForEachActivity executes in each iteration:

C#
private bool ExecuteNext(ActivityExecutionContext context)
{
    // First, move to the next position.
    if (!this.Enumerator.MoveNext())
        return false;

    // Execute the child activity.
    if (this.EnabledActivities.Count > 0)
    {
        // Add the child activity to the execution context and 
        // setup the event handler to
        // listen to the child Close event.
        // A new instance of the child activity is created for each iteration.
        ActivityExecutionContext innerContext =
            context.ExecutionContextManager.CreateExecutionContext(
                this.EnabledActivities[0]);
        innerContext.Activity.Closed += this.OnChildClose;

        // Fire the Iterating event.
        base.RaiseEvent(IteratingEvent, this, EventArgs.Empty);

        // Execute the child activity again.
        innerContext.ExecuteActivity(innerContext.Activity);
    }
    else
    {
        // an empty foreach loop.
        // If the ForEach activity is still executing, then execute the next one.
        if (this.ExecutionStatus == ActivityExecutionStatus.Executing)
        {
            if (!ExecuteNext(context))
                context.CloseActivity();
        }
    }
    return true;
}

Here you see, for each iteration, a new ActivityExecutionContext is created. The CreateExecutionContext method performs a recursive clone of the contained activity inside the ForEachActivity. It also uses reflection to initialize and set all public properties of the contained activity. For example, during the first visit, there’s a workflow that clones a page setup.

image

The CreateExecutionContext recursively goes through all the activities and clones them and initializes their public properties. This process is very expensive. It’s so expensive that web server CPU gets to 100% trying to execute the workflows during the load test. So, it had to be replaced with something less expensive.

The solution is to manually execute the Activities and do the loops using .NET’s native for loop.

The first visit workflow, which is the most expensive one, and needs to be super fast is now converted to a single method that executes the activities exactly the similar way the workflow does. You have seen before how the workflow looks like, here’s how to equivalent code looks like:

C#
public UserVisitWorkflowResponse SetupNewUser(string userName)
{
    var response = new UserVisitWorkflowResponse();

    // Get template setting that so that we can create pages from templates
    var getUserActivity = RunActivity<GetUserGuidActivity>((activity) => 
                                              activity.UserName = userName);
    var getUserSettingTemplateActivity = 
             RunActivity<GetUserSettingTemplatesActivity>((activity) => { });
    RunActivity<SetUserRolesActivity>((activity) =>
    {
        activity.RoleName = getUserSettingTemplateActivity.AnonUserSettingTemplate
                                                                .RoleNames;
        activity.UserName = userName;
    });

    if (getUserSettingTemplateActivity.CloneAnonProfileEnabled)
    {
        // Get the template user so that its page setup can be cloned for new user
        var getRoleTemplateActivity = RunActivity<GetRoleTemplateActivity>(
                  (activity) => activity.UserGuid = getUserActivity.UserGuid);
        if (getRoleTemplateActivity.RoleTemplate.TemplateUserId != Guid.Empty)
        {
            // Get template user pages so that it can be cloned for new user
            var getTemplateUserPages = RunActivity<GetUserPagesActivity>(
                (activity) => activity.UserGuid = 
                       getRoleTemplateActivity.RoleTemplate.TemplateUserId);
            foreach (Page page in getTemplateUserPages.Pages)
            {
                var clonePageActivity = RunActivity<ClonePageActivity>((activity) =>
                    {
                        activity.PageToClone = page;
                        activity.UserId = getUserActivity.UserGuid;
                    });

                var getColumnsOfPageActivity = RunActivity<GetColumnsOfPageActivity>(
                                (activity) => activity.PageId = page.ID);
                foreach (Column column in getColumnsOfPageActivity.Columns)
                {
                    var getWidgetZoneActivity = RunActivity<GetWidgetZoneActivity>(
                          (activity) => activity.ZoneId = column.WidgetZoneId);
                    var cloneWidgetZoneActivity = RunActivity<AddWidgetZoneActivity>(
                            (activity) => activity.WidgetZoneTitle = 
                                 getWidgetZoneActivity.WidgetZone.Title);
                    RunActivity<CloneColumnActivity>((activity) =>
                        {
                            activity.ColumnToClone = column;
                            activity.PageId = clonePageActivity.NewPage.ID;
                            activity.WidgetZoneId = 
                                   cloneWidgetZoneActivity.NewWidgetZone.ID;
                        });

                    var getWidgetInstancesActivity = 
                      RunActivity<GetWidgetInstancesInZoneActivity>(
                      (activity) => activity.WidgetZoneId = column.WidgetZoneId);
                    foreach (WidgetInstance widgetInstance in 
                                    getWidgetInstancesActivity.WidgetInstances)
                    {
                        RunActivity<CloneWidgetInstanceActivity>((activity) =>
                            {
                                activity.WidgetInstance = widgetInstance;
                                activity.WidgetZoneId = 
                                     cloneWidgetZoneActivity.NewWidgetZone.ID;
                            });
                    }
                }
            }
        }
    }
    else
    {
        // Setup some default pages
    }

    var getUserSettingActivity = RunActivity<GetUserSettingActivity>(
                  (activity) => activity.UserGuid = getUserActivity.UserGuid);
    response.UserSetting = getUserSettingActivity.UserSetting;
    response.CurrentPage = getUserSettingActivity.CurrentPage;

    var getUserPagesActivity = RunActivity<GetUserPagesActivity>(
                  (activity) => activity.UserGuid = getUserActivity.UserGuid);
    response.UserPages = getUserPagesActivity.Pages;

    return response;
}

The RunActivity function directly calls the Execute method of an Activity using Reflection. It also allows you to first bind the properties before calling the Execute function. So, the above code is basically a code equivalent of an workflow, it executes the activities one by one, setting their properties, performing loops, getting value out of activity properties and so on. If you are not satisfied with Workflow performance and you do not want to give up the design experience but want improved workflow execution, you can try this approach to manually run the activity. You still get to design the workflows, but you just don’t run them.

Once this was done, there was significant scalability improvement:

image

Observations:

  • Request/sec is avg 48.4/sec. Almost 10 requests more per second than before. This is a lot since it means at this pace, you get 4.2 million requests per day. Almost one more million requests served per day. That’s a lot!
  • Avg response time per request is 0.29 sec. Almost 100ms response time improved.
  • There’s zero error produced, which means we do not have scalability problems like thread deadlocks, database contention, etc. Awesome!
  • The request/sec is flat, this means we do not have common synchronization problems, multi-threading problems, database getting slower and slower issues etc. Sweet! When such problems happen, you will see the graph slowly goes downward.

The extra hard work pays off.

Widget-to-Widget Communication

Dropthings now supports widget to widget communication. One widget can raise an event that other widgets can capture. For example:

image

Here, the Master widget can send messages to Child Widget.

Widget can optionally subscribe to event notification. When they are subscribed, they receive notification of any event raised by any other widget anywhere on the page. Thus they can capture the event, see if it’s any use to them and then decide to act.

Here the child widget subscribes to event notification:

C#
public partial class Widgets_EventTest_ChildWidget : System.Web.UI.UserControl, 
IWidget
{
  private IWidgetHost _Host;
  protected void Page_Load(object sender, EventArgs e)
  {

  }

  #region IWidget Members
  public new void Init(IWidgetHost host)
  {
     _Host = host;
     host.EventBroker.AddListener(this);
  }

The EventBrokerService is basically a registry for subscribers and has a handy method to broadcast events to all listeners.

C#
namespace Dropthings.Widget.Framework
{
    public class EventBrokerService
    {
        public List<WeakReference> Subscribers = new List<WeakReference>();

        public void AddListener(IEventListener listener)
        {
            this.Subscribers.Add(new WeakReference(listerner));
        }

        public void RaiseEvent(object sender, EventArgs e)
        {
            foreach (WeakReference listener in this.Subscribers)
            {
                try
                {
                    if (listener.IsAlive)
                    {
                        (listener.Target as IEventListener).AcceptEvent(sender, e);
                    }
                }
                catch (NotImplementedException)
                {
                }
                finally
                {
                }
            }
        }
    }
}

It keeps a WeakReference to the subscribers so that the subscribers do not maintain one more strong reference and thus prevent garbage collector from collecting them.

When an event needs to be raised, just call the RaiseEvent function:

C#
protected void Raise_Clicked(object sender, EventArgs e)
{
    MasterChildEventArgs args = new MasterChildEventArgs(
        "Master " + _Host.WidgetInstance.Id, this.Message.Text);
    _Host.EventBroker.RaiseEvent(this, args);
}

This notifies everyone listening for events in the EventBroker.

Just implement IWidget.AcceptEvent in order to receive such events.

C#
public void AcceptEvent(object sender, EventArgs e)
{
    if (sender != this && e is MasterChildEventArgs)
    {
        var arg = e as MasterChildEventArgs;
        this.Received.Text = arg.Who + " says, " + arg.Message;
        _Host.Refresh(this);
    }
}

Here the MasterChildEventArgs is nothing but a simple child of the EventArgs class. Once the child receives an event like this, if does its stuff and then tells Host to refresh it so that Host updates the UpdatePanel where the child is.

Configuring Widgets for Different User Roles

You can define which user role gets to see which widgets from the ManageWidgetPersmission.aspx:

image

Moreover, you can design the default template or default set of widgets that are created for every new visitor from a special account. You can create as many tabs as you like and put as many widgets as you like on this special account. When a new user comes in, this special account’s entire tab and widget collection is copied to the new user.

There are two such special users – one for anonymous user’s default page setup and another for users who signup and get a different default page and widgets. You can also turn off the signup feature where you force feed a default page setup to users when they signup. Instead you can allow user to keep the widgets and tabs form their anonymous session. This is default in Dropthings. But several customers of Dropthings wanted to show different widgets to users who signup, so we did it. Handy for enterprise portals.

XML
<userSettingTemplates cloneAnonProfileEnabled="true"
       cloneRegisteredProfileEnabled="false">
   <templates>
     <clear/>
     <add
         key="anon_template"
         userName="anon_user@yourdomain.com"
         password="changeme"
         roleNames="Guest"
         templateRoleName="Guest"
           />
     <add
         key="registered_template"
         userName="reg_user@yourdomain.com"
         password="changeme"
         roleNames="RegisteredUser"
         templateRoleName="RegisteredUser"
           />
   </templates>
</userSettingTemplates>

Enjoy!

DotNetKicks Image

Conclusion

Dropthings is a portal that showcases several hot technologies - ASP.NET 3.5, jQuery, Silverlight, ASP.NET AJAX, Workflow Foundation, Linq to SQL, Unity and Enterprise Library. This is a real example of how all these technologies can play together in a production quality product that is already in mass use on the internet.

History

  • 8th April, 2009: Initial post

License

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


Written By
Architect BT, UK (ex British Telecom)
United Kingdom United Kingdom

Comments and Discussions

 
AnswerRe: Excellent Architecture and a very nice article. Need some help ? Pin
saif_md4u28-Jun-10 1:20
saif_md4u28-Jun-10 1:20 
GeneralGreat Article.... as always Pin
Maxim198025-Jan-10 5:10
Maxim198025-Jan-10 5:10 
GeneralMind Blowing Pin
keyur soni3-Jan-10 19:31
keyur soni3-Jan-10 19:31 
QuestionEnhancement Pin
Hatem ElShenawy22-Nov-09 0:22
Hatem ElShenawy22-Nov-09 0:22 
GeneralExcellent Work Pin
p0o0q24-Sep-09 23:08
p0o0q24-Sep-09 23:08 
GeneralUFrame Usage Pin
Member 90078725-Aug-09 7:41
Member 90078725-Aug-09 7:41 
GeneralPlanning to use dropthings as my company portal [modified] Pin
rashmikatiyar2-Jul-09 4:14
rashmikatiyar2-Jul-09 4:14 
GeneralRefresh UpdatePanel during Drag and Drop Pin
ashwanigl9-Jun-09 21:27
ashwanigl9-Jun-09 21:27 
GeneralUpdatePanel vs, UFrame Pin
Vladimir Kelman2-Jun-09 7:12
Vladimir Kelman2-Jun-09 7:12 
GeneralDropthings site - resize get error Pin
Dean Wyant29-May-09 3:31
Dean Wyant29-May-09 3:31 
GeneralRe: Dropthings site - resize get error [modified] Pin
toddsloan4-Dec-09 3:42
toddsloan4-Dec-09 3:42 
Questionhow to create Dynamic listing for dropthings Pin
arunvtyc21-May-09 0:54
arunvtyc21-May-09 0:54 
Generaldropthings is undefined Pin
arunvtyc20-May-09 4:06
arunvtyc20-May-09 4:06 
GeneralRe: dropthings is undefined Pin
Omar Al Zabir20-May-09 19:45
Omar Al Zabir20-May-09 19:45 
GeneralRe: dropthings is undefined Pin
arunvtyc20-May-09 21:25
arunvtyc20-May-09 21:25 
GeneralExcellent Pin
Grav-Vt17-May-09 17:15
Grav-Vt17-May-09 17:15 
GeneralYou are a brave man Pin
Dewey24-Apr-09 14:15
Dewey24-Apr-09 14:15 
QuestionAwesome article, as usual. Pin
jpeterson21-Apr-09 8:29
jpeterson21-Apr-09 8:29 
AnswerRe: Awesome article, as usual. Pin
Omar Al Zabir22-Apr-09 8:08
Omar Al Zabir22-Apr-09 8:08 
GeneralRe: Awesome article, as usual. Pin
micmit_syd23-Apr-09 13:43
micmit_syd23-Apr-09 13:43 
GeneralIt's very amazing [modified] Pin
nicholas_pei19-Apr-09 5:12
nicholas_pei19-Apr-09 5:12 
GeneralRe: It's very amazing Pin
Omar Al Zabir19-Apr-09 13:38
Omar Al Zabir19-Apr-09 13:38 
GeneralInteresting. Pin
ZakPatat11-Apr-09 15:21
ZakPatat11-Apr-09 15:21 
GeneralFeedback Pin
kazimanzurrashid9-Apr-09 11:51
kazimanzurrashid9-Apr-09 11:51 
GeneralRe: Feedback Pin
Omar Al Zabir9-Apr-09 20:06
Omar Al Zabir9-Apr-09 20:06 

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.