Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / Javascript

TodoMVC in C# with Saltarelle and Knockout

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
28 Dec 2012MIT16 min read 29.2K   373   25   3
a TodoMVC implementation written in C# and compiled to JavaScript

Introduction  

In this article I present a TodoMVC implementation written in C# and compiled to JavaScript with the Saltarelle compiler. It's an example project meant to show that it's possible to write full featured single-page JavaScript applications directly in C# and Visual Studio, using a known JavaScript framework like Knockout.js.

C# + Saltarelle 

Saltarelle is a C# to JavaScript compiler written by Erik Källén, derived from Script# by Nikhil Kothari. It can be used stand-alone (from command line) or from within Visual Studio. When called from Visual Studio, it compiles a C# project producing a single .js file containing the JavaScript executable code.

Saltarelle comes with a full HTML/DOM support library, as well as libraries for emulating basic .NET types, and libraries for widely known JavaScript packages like jQuery, Knockout and NodeJs.

Despite C# and JavaScript are very different languages, most C# features are translated seamlessly into JavaScript with the result that it's possible to code freely in C# without renouncing to classes, types, refactoring, intellisense and all the amazing features that make C# one of the best managed languages.

The code compiled by Saltarelle is also very efficient, compact and human readable, very close to writing JavaScript directly.

Knockout

Knockout.js is a JavaScript library written by Steve Sanderson, that allows to write MVVM (model-view-viewmodel) applications in JavaScript. And thanks to an import library, it integrates perfectly in C#/Saltarelle.   

Explaining Knockout is beyond the scope of this article, so I'll assume a basic knowledge of it. Those who are not familiar with this library might want to check directly the Knockout website which is full of tutorials and documentations.

To describe it as briefly as possible, Knockout scans the HTML/DOM looking for data-bind attributes (which are valid HTML) triggering UI refreshes depending on what's defined in the attribute itself, according to a specific syntax. 

For example a

HTML
<p data-bind="text: mystring">  
markup will cause the <p> to be updated automatically every time mystring changes in the ViewModel. It does that by using a Binding named text, and by having previously declared mystring as an Observable property. By using bindings and observable properties it's possible to achieve the needed separation between UI and ViewModel, as per MVVM pattern.

TodoMVC   

TodMVC is an open source project written by Addy Osmani and Sindre Sorhus where a trivial web application (list of things "Todo") is coded using the most common JavaScript MVC frameworks. Its purpose is to compare such frameworks by using a common term (the Todo application) so to let the developer decide what's best suited for him. It's also a good learning exercise for people starting to write single page JavaScript applications, as it touches most of the aspects of an application of this kind.

In this article I have used the Knockout version of the TodoMVC application as a reference. My code tried to be as close as possible to its JavaScript counterpart so it's more easy to compare the machine generated code with the human-written one.   

Before going deeply in the article I suggest you to play enough with the TodoMVC application itself, getting acquainted with its functionalities, so that you know what we will be talking about later in the article.   

I suggest that you also inspect the index.html (unchanged from to the original implementation) to see how the data-bind attributes are plugged to HTML elements. The good thing of TodoMVC is that it's a small application so its code isn't overwhelming, and you can easily get a grasp of it.

MVC and MVVM

TodoMVC was born to feature MVC (model-view-controller) frameworks, but in reality Knockout is based on a MVVM (model-view-viewmodel) architecture (which can be thought as a special case of MVC).  In our MVVM design,   

  • Model is the data stored on the LocalStorage (the list of "todo" items)
  • View is the presentation layer provided by the HTML page (index.html)
  • ViewModel is the C# class connected both to View (via Knockout bindings) and to Model (via LocalStorage and JSON parsing).

Getting Started   

The first step is to get the TodoMVC template files as we don't want to rewrite everything from scratch. Since we want to write only the JavaScript part of the application, we copy entirely the TodoMVC sample application from the Knockout version. So get the files and organize them in folders as follows:
TodoMVC
   assets
      base.css
      base.js 
      director.min.js
   js 
     lib
        knockout.min.js
   index.html
   app.js  

it's better to rename app.js into something else like app.original.js, as our C# compiled code will overwrite it. Keep it separate so you can compare the two versions and study them. 

Configuring Saltarelle in Visual Studio 

Everything will be organized in a Visual Studio solution. The solution will contain two projects: one is a minimalistic web site project (no ASP.NET involved at all) for the index.html file, assets and libs. Another project, in the same solution, will host the C# files that will be compiled by Saltarelle. This latter project will be a normal console application, for lack of a better project template in Visual Studio.  

What really happens on the backstage, is that Visual Studio does a first compilation of the code, and if it succeeds, Saltarelle steps in doing its own compilation. So in reality the code it's compiled twice, the first one only needed for syntax checking (.exe produced will be ignored) and the second one being the real C# to JavaScript compilation.

So open Visual Studio and create a new "Console" C# project. Then open the management console (under tools) and type:

install-package Saltarelle.Compiler
install-package Saltarelle.Runtime
install-package Saltarelle.Web
install-package Saltarelle.Knockout

this will download the Saltarelle compiler from the internet and will install it properly in your solution folder.

Now go to project property -> application, and change both application name and application namespace to "app". This will tell the compiler to produce a single file named "app.js", which is what is referenced by our index.html.

Go to project property -> compilation events -> post compilation command line and add the following line:

copy "$(TargetDir)$(TargetName).js" "$(SolutionDir)\TodoMVC\js"  

this will instruct Visual Studio to automatically copy the compiled "app.js" to the website folder everytime you recompile the project. This is required because the output .js file normally is located in the bin folder of the C# project and not in the website folder as required by index.html.

Now add an "existing web site" to the solution, choosing the "TodoMVC" prepared before as root folder. So now you should have two projects in your solution: the website and the C# project.  

Set your web site as startup project and mark index.html as startup page so that you can easily run and debug in Visual Studio.

Copy the file mscorlib.js from the "packages/Saltarelle.Runtime.1.6.3" folder to "js/lib"; this is the library that mimics .NET types and functionalities in JavaScript, and it's needed for the app to work. "mscorlib.min.js" is the minified version, but for debug purposes use the unminified one.

The mscorlib.js library has also to be referenced in the html source, so open index.html and add the following line, just before the "app.js" script reference:

HTML
<script src="js/lib/mscorlib.js"></script> 
The last thing needed is to update the project dependencies in the solution. Since we have two projects, we have to tell Visual Studio that the website project depends on the C# projects, and that the C# project is to be compiled first.

Let's start to code

Having done all the tedious preparatory steps, we can now start to code. Open Program.cs created by the Saltarelle compiler and replace its content with:
C#
using System;
using System.Html;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
 
using KnockoutApi; 

this adds the basic references.

System contains the .NET type emulations (e.g. int is converted to JavaScript number type, and so on).

System.HTML contains the references for manipulating the DOM (e.g. the Window, Document, Element objects).

KnockoutApi contains the metadata for the Knockout.js library, so that we can use it directly from C#. And since it's all metadata, no real code is added by the compiler--using this library is as efficient as writing directly in JavaScript.

System.Runtime.CompilerServices contains the [Attribute] decorators interpreted by Saltarelle that help to control the output JavaScript code. Some attribute used in the project are:

  • [IgnoreNameSpace] tells the compiler to not add namespaces in the JavaScript output file, here for the sole reason of making it more human readable.
  • [PreserveCase] by default the compiler adopts camel-case names in the JavaScript output, for example Dummy.ToString() becomes Dummy.toString(). This design was made to let coders write in ".NET style" and have it output in "JavaScript style". To override this behavior, use [PreserveCase] for members or [PreserveMemberCase] for whole classes.

    When using Knockout, it's generally advised to use case preservation for the ViewModel class, so to have corresponding names between HTML data-bind attributes and C#. Anyway in this article, to make things simpler, I kept the ViewModel camel-cased, similar to the original JavaScript, so there is no risk of making mistakes.

The Todo Class

Our Todo app manages "things to do" entities, so we need a basic object that represents them:
C#
public class Todo 
{
    public Observable<string> title;
    public Observable<bool> completed;
    public Observable<bool> editing;
 
    public Todo(string title, bool completed=false) 
    {
        this.title = Knockout.Observable(title);
        this.completed = Knockout.Observable(completed);
        this.editing = Knockout.Observable(false);
    }
}

title, completed and editing are declared as Observable, so when they change, Knockout automatically updates the UI. Being in a strongly typed environment, we are providing types in <> and we are also providing startup default values.

The ViewModel

Now let's code our ViewModel:

C#
public class ViewModel
{
    public ObservableArray<todo> todos;         
    public Observable<string> current; 
    public Observable<string> showMode;
 
    public Action add;
    public Action<todo> remove;
    public Action removeCompleted;
    public Action<todo> editItem;
    public Action<todo> stopEditing;
 
    public ComputedObservable<todo[]> filteredTodos;
    public ComputedObservable<int> completedCount;
    public ComputedObservable<int> remainingCount;
    public ComputedObservable<bool> allCompleted;

todos holds the list of Todo objects as an array. In Knockout they are called ObservableArray and have a similar functionality as standard Arrays. 

current is the input box where the user can add new items.  

showMode is a string that can be "all", "active" or "completed" and allows to filter the results on the UI. By changing it, filteredTodos will change accordingly. Note that showMode is also referenced by the URL router so that the app can have bookmarkable links (more on this later).

All UI events (add, remove, removeCompleted, editItem and stopEditing) are coded as Action objects, that is the equivalent of the JavaScript function type (but without any return parameter). Actions can be simply defined inline with lambda expressions as we'll see later.

ComputedObservables<code> are values that are calculated as the result of other properties, and as such they do not store a value, but calculate it when needed. For example remainingCount is recalculated anytime todos changes, and being remainingCount an observable itself, any UI object referencing to it will automatically update. 

So far we have only declared objects inside our ViewModel, we now have to define them. We do this in the class constructor:

C#
public ViewModel(dynamic[] initial_todos)
{
    var self = this;
 
    // map array of passed in todos to an observableArray of Todo objects
    todos = Knockout.ObservableArray(KnockoutUtils.ArrayMap( initial_todos, 
                   (todo)=> {return new Todo( todo.title, todo.completed );}
            ));                               

the constructor takes an initial array of objects and turns them into Observables. The objects come from the "model" part of the architecture, that is, from the LocalStorage provided by the HTML5 browser.  

ArrayMap() converts simple objects into Todo objects, and ObservableArray() turns the resulting Todo[] into an observable array. So in the end we have an ObservableArray containing Todo items which in turn contain Observable properties.

Continuing on the ViewModel constructor code, we initialize our Observables:

C#
// store the new todo value being entered
current = Knockout.Observable("");
showMode = Knockout.Observable("all");
a bit more complex is the initialization of ComputedObservables:
C#
filteredTodos = Knockout.Computed( () => 
{
    switch( self.showMode.Value ) 
    {
        case "active":
            return self.todos.Value.Filter( (todo)=> {return !todo.completed.Value;} );
        case "completed":
            return self.todos.Value.Filter( (todo)=> {return todo.completed.Value;} );
        default:
            return self.todos.Value;
    }
}); 

the parameter for Knockout.Computed() is an inline function defined with the => lambda operator that returns an array of Todo objects according to what self.showMode is set to ("all", "active" or "completed") by the help of Filter() (which in turn takes another function as parameter!).

Notice that, differently from JavaScript, in C# we use the ".Value" syntax to access the content of an Observable property. So, showMode is the property object, and showMode.Value is the string value stored within. In JavaScript, Observable properties are instead accessed writing them as methods, e.g. showMode().

We now code Actions:

C#
// add a new todo, when enter key is pressed
add = () => 
{            
    var curr = self.current.Value.Trim();
    if(curr!="") 
    {
        self.todos.Push( new Todo( curr ) );
        self.current.Value = "";
    }
}; 

since add is an Action it can be defined inline with a lambda expression. The action body does nothing else than adding a new Todo object via the ObservableArray.Push() method.

Similarly, we define the remaining actions:

C#
// remove a single todo
remove = (todo) => 
{
    self.todos.Remove(todo);
};
        
// remove all completed todos
removeCompleted = () =>
{
    self.todos.Remove( (todo) => {return todo.completed.Value;} );
};
 
// edit an item
editItem = (item) =>
{
    item.editing.Value = true;
};
 
// stop editing an item.  Remove the item, if it is now empty
stopEditing = (item) => 
{
    item.editing.Value = false;
 
    if (item.title.Value.Trim()=="") 
    {
        self.remove(item);
    }
};
and ComputableObeservables:
C#
// count of all completed todos
completedCount = Knockout.Computed( () =>
{
    return self.todos.Value.Filter( (Todo todo) => { return todo.completed.Value; }).Length;
});
 
// count of todos that are not complete
remainingCount = Knockout.Computed( () =>
{
    return self.todos.Value.Length - self.completedCount.Value;
});

The last remaining ComputableObeservable is a bit tricky, because it's a of particular kind. Instead of being read-only like the majority of ComputableObeservables, it's capable of being written too. The beaviour we want to implement here is: on read allCompleted returns whether the list of items is empty or not (true/false). This is used to update the checkbox "All completed" on the UI. On write, that is when the checkbox is flagged by the user, allComplete will change the boolean field .complete in all the todos, according to the checkbox value coming from the UI (parameter newValue in the code below).

To implement this read/write ComputedObservable, in C# we need to pass a special object of type ComputedOptions <t>that contains two methods: GetValueFunction <t>and SetValueFunction <t>(corresponding to read and write).

<t>We can do this quickly by using the object literal syntax when calling the ComputedOptions <t><bool>constructor: 

C#
// writeable computed observable to handle marking all complete/incomplete            
allCompleted = Knockout.Computed( new ComputedOptions<bool>()
{
    GetValueFunction = () => 
    {
        return self.remainingCount.Value==0;
    },
 
    SetValueFunction = (newValue) => 
    {
        KnockoutUtils.ArrayForEach<todo>(self.todos.Value, (todo)=>
        {
            // set even if value is the same, as subscribers are not notified in that case
            todo.completed.Value = newValue;
        });                
    }
});  

for those who don't remember, the object literal syntax allows to construct objects simply with the syntax

C#
new ClassName() { property=value, [...] };  

which is not to be confused with named parameters passed in constructors, e.g.  

C#
new ClassName(property: value, [...]);

The last thing we do in the ViewModel constructor is to make it save our data when a change is detected. This can be done in Knockout with the use of an advanced concept called extenders, in particular the throttle extender.   

To make it brief, the throttle extender causes the ComputedObservable to delay its evaluation until all its dependencies have stopped changing for a specified period of time--which in our example is set to 500 ms.

So by using a throttle extender, anytime the ViewModel is changed by the UI our data will be saved half second after. 

First we create a dummy ComputedObservable that holds the behavior of saving to LocalStorage:

C#
// internal computed observable that fires whenever anything changes in our todos
ComputedObservable<string> th = Knockout.Computed( () => 
{            
    // store a clean copy to local storage, which also creates a dependency 
    // on the observableArray and all observables in each item
    Window.LocalStorage.SetItem("todos-knockout", Knockout.ToJson( self.todos ) );                
    return "";
}); 
then we extend it with:
C#
KnockoutUtils.Extend<ComputedObservable<string>>( th, new { throttle = 500 });  

here Extend() takes an object declared inline containing a field named "throttle".

Our constructor is now complete and the ViewModel class misses only this small function:

C#
// helper function to keep expressions out of markup
public string getLabel( Observable<int> count ) 
{
    return KnockoutUtils.UnwrapObservable<int>( count ) == 1 ? "item" : "items";
} 

which is used by the index.html page to display "1 item" or "2 items"..., getting the code out of the markup to make it more readable.   

Passing the argument through the UnwrapObservable() function is a Knockout technique that allows to reuse the getLabel function even with parameters that are not Observables, but are normal variables.

Custom Bindings

Our ViewModel class is now ready, but there are still two missing pieces. The index.html page was built around the use of two custom Knockout bindings: enterKey and selectAndFocus, to implement to distinct functionalities.

enterKey is used to trigger an event function when the enter key is pressed; in our application that happens when the user finishes his editing and the item has to be added to the list.

The second custom binding, selectAndFocus, is used to manage the UI feature "double click to edit an item".

To write a custom binding, we write a new class deriving it from BindingHandler and overriding the Init() and Update() methods. 

C#
public class EnterKeyBindingHandler : BindingHandler
{
    public const int ENTER_KEY = 13;
 
    public override void Init(Element element, 
                              Func<object> valueAccessor, 
                              Func<jsdictionary> allBindingsAccessor, 
                              object viewModel)
    {                            
        Action<object,ElementEvent> wrappedHandler = (object data, ElementEvent ev) =>
        {
            if ( ev.KeyCode == ENTER_KEY ) 
            {                    
                ((dynamic)valueAccessor)().call(this, data, ev );                         
            }                                
        };
            
        Func<object> newValueAccessor = () => 
        {
            return new { keyup = wrappedHandler }; 
        };
 
        Knockout.BindingHandlers["event"].Init( element, 
                                                newValueAccessor, 
                                                allBindingsAccessor, 
                                                viewModel );
    }                   
}  

the EnterKeyBindingHandler defined here, modifies the binding named event so that when "keyup" is triggered, the function contained inside wrappedHandler is called, producing the call to valueAccessor if enter is pressed.

Similarly the SelectAndFocusBindingHandler:

C#
public class SelectAndFocusBindingHandler : BindingHandler
{
    public override void Init(Element element, 
                              Func<object> valueAccessor, 
                              Func<jsdictionary> allBindingsAccessor, 
                              object viewModel)
    {
        Knockout.BindingHandlers["hasfocus"].Init(element, 
                                                  valueAccessor, 
                                                  allBindingsAccessor, 
                                                  viewModel);
 
        KnockoutUtils.RegisterEventHandler(element, "focus", (el,ev) => {element.Focus();});            
    }
 
    public override void Update(Element element, 
                                Func<object> valueAccessor, 
                                Func<jsdictionary> allBindingsAccessor, 
                                object viewModel)
    { 
        KnockoutUtils.UnwrapObservable<object>(valueAccessor());  // for dependency

        // ensure that element is visible before trying to focus
        Window.SetTimeout( ()=> 
        {
            Knockout.BindingHandlers["hasfocus"].Update(element, 
                                                        valueAccessor, 
                                                        allBindingsAccessor, 
                                                        viewModel );
        }, 0);    
    }
} 

The Main function 

At this point the TodoMVC application is almost complete, we only need to plug in all the different parts together.

Similarly to a console application, our compiled JavaScript code will start from a Main() function:

C#
class Program
{       
    static void Main()
    {  
inside the Main() we firstly register in Knockout  the two custom bindings created before:
C#
// a custom binding to handle the enter key (could go in a separate library)
Knockout.BindingHandlers["enterKey"] = new EnterKeyBindingHandler();
 
// wrapper to hasfocus that also selects text and applies focus async
Knockout.BindingHandlers["selectAndFocus"] = new SelectAndFocusBindingHandler();    
then we instantiate our ViewModel and initialize it with data coming from the LocalStorage:
C#
// check local storage for todos
var todos = KnockoutUtils.ParseJson<dynamic[]>( (string) Window.LocalStorage.GetItem("todos-knockout") );
 
// bind a new instance of our view model to the page
var viewModel = new ViewModel(todos); 

LocalStorage.GetItem() returns a JSON string that we parse to produce an array of dynamic objects where our Todo items are stored. "dynamic" in C# is a way to reference to a generic JavaScript object that doesn't have a C# class defined for it. Another possible way, is to have a JsDictionary<string,object /><string,object> and treat the object as a collection of name-value pairs.

<string,object>We now plug Knockout itself, so that data-bind attributes in the HTML file become meaningful and our application takes life:

C#
Knockout.ApplyBindings(viewModel);

URL Routing

A routing library intercepts certain URLs in the browser, and instead of requesting a new page to the server, it routes them to a function call in the JavaScript code. Routing is how single-page applications simulate the behavior of old-style multi-page applications, allowing bookmarks, history buttons and stateless navigation.

In our application URL routing is used to write in the showMode observable property, causing a filter on the todo items shown on the UI. For example the link

HTML
http://.../TodoMVC/completed    

will write the string "completed" to showMode, and, being showMode an Observable property, it will trigger a UI refresh where required.  

The TodMVC app we are examining uses the Director router library, which doesn't have a corresponding API in C#/Saltarelle. But we can define a simple one by the following class definition:
C#
[IgnoreNamespace]    
[Imported(IsRealType=true)]    
public static class Director
{
    [InlineCode("Router({route})")]
    public static dynamic Router(JsDictionary<string,dynamic> route) {return null;}
} 

which serves the only purpose of having the inline code put into the JavaScript output.

Having the small API defined, we can call the URL Router from Main():

C#
// set up filter routing
Director.Router(new JsDictionary(new object[]{"/:filter",viewModel.showMode})).init();
Router() accepts an object with a name-value pair, in our case name is the URL to intercept, and value is where in our ViewModel the router will write.

Running the project  

Now the application is complete and can be run in Visual Studio under your favorite browser. Unfortunately Saltarelle C# code can't be debugged natively (you can't place breakpoints) but, if using IE as browser, breakpoints can be put on the JavaScript compiled file (that's why it's so important to have it human-readable).  

When debugging JavaScript, Visual Studio immediate mode (CTRL+ALT+I) gives access to all objects; remember that classes <string,object><director>generated by Saltarelle have the prefix "$" as by ECMA standard for machine generated code. You'll notice also that native JavaScript objects (such as String  <string,object><director>or even Object) have been extended by the mscorlib.js to introduce .NET-like methods and to support type checking.

Comparing the compiler output  

Now that we have compiled our application, we can compare it with the original JavaScript code by Addy Osmani, evaluating the quality of the compiled code.

Firstly, we compare the two files by size:

                 Original      Saltarelle     % incr.    
-------------------------------------------------------
lines               169            181          +7 %  
bytes              4946           6518         +31 %
minified bytes     2262           3893         +72 %

While the increment in line numbers is modest, the byte increment is consistent. This is mostly due to the verbose object names present in the Saltarelle output, which have no impact on the final execution speed of the code (apart of course from loading times).

If we do a line by line comparison, we can see that the two files are indeed very similar--even identical in some parts.

<string,object><director>

<string,object><director>

The only notable difference, as expected, is due to type checking introduced by the C# language and carried over in JavaScript. But this has to be seen as an additional feature rather than compiler code bloat. T<string,object><director>ype checking is done with a new object named <string,object><director /></string,object>Type, introduced in mscorlib.js. C# classes are first registered within the Type object (which can be seen as a Type manager proxy)

<string,object><director>

<string,object><director>

JavaScript
Type.registerClass(null, '$Program', $$Program, Object);
Type.registerClass(global, 'EnterKeyBindingHandler', $EnterKeyBindingHandler);
Type.registerClass(global, 'SelectAndFocusBindingHandler', $SelectAndFocusBindingHandler);
Type.registerClass(global, 'Todo', $Todo, Object);
Type.registerClass(global, 'ViewModel', $ViewModel, Object);  
and then Type is used when needed, for example to solve a type cast:
JavaScript
return new $Todo(Type.cast(todo.title, String), !!todo.completed); 

Using the attached file

To use the source files attached to this article, you have to install the Saltarelle compiler because it's not included in the .zip (for size reason). Just do the steps involving the install-package commands. If you only want to see the TodoMVC running, without using the compiler, just open index.html in your favourite browser. Switch back and forth the two versions by renaming app.js.<string,object><director>

Conclusion

By implementing a TodoMVC sample application, I tried to demostrate that C# + Saltarelle is an available option for the .NET developer who wants to develop single page web applications and maintain C# as his main language.   

Credits

  • TodoMVC by Addy Osmani and Sindre Sorhus
  • Knockout by Steve Sanderson
  • Saltarelle by Erik Källén 
  • Script# Nikhil Kothari

History

  • Version 1.0, 27 dec 2012
    based on Saltarelle 1.6.3 and Knockout version of TodoMVC as of 27 dec 2012

License

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


Written By
Software Developer
Italy Italy
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionExcellent article Pin
Ashraf Sabry28-Dec-12 7:06
Ashraf Sabry28-Dec-12 7:06 
AnswerRe: Excellent article Pin
Antonino Porcino28-Dec-12 7:27
Antonino Porcino28-Dec-12 7:27 
GeneralRe: Excellent article Pin
Ashraf Sabry28-Dec-12 7:55
Ashraf Sabry28-Dec-12 7:55 

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.