Click here to Skip to main content
15,561,246 members
Articles / Web Development / ASP.NET
Posted 25 Dec 2009


53 bookmarked

OneNote on iPhone and Palm Pré using Windows Azure

Rate me:
Please Sign up or sign in to vote.
4.96/5 (39 votes)
25 Dec 2009CPOL12 min read
Learn how to synchronize your OneNote notebooks on Windows Azure and access it from your iPhone or your Palm Pré.


This article will show you how you can synchronize OneNote on Windows Azure using OneNote API and Azure Blob Storage. Then, you'll learn how to develop an ASP.NET application to access it from an iPhone and a WebOS application to access it from a Palm Pré©.

Introduction: Why OneNote?

A few months ago, I thought of the two applications I'm using the most. Because I'm now more a manager than a full time developer, Outlook first came to my mind. Of course, everyone has an e-mail address so you should have one, and when you've got an Exchange Server, Outlook is a must have. After Outlook, the second application I'm using every day is OneNote.

OneNote is my favorite application. It allows me to store and structure everything: ideas, meeting reports, things to do, favorites links, etc. So I've got none less than four notebooks with plenty of sections and tenth pages in each.


Of course, it could be great to be able to take all these information everywhere. Exchange is already available from most existing SmartPhones so you could access your mailbox everywhere. Conversely, there is no existing solution to synchronize OneNote and use it, at least as a reader, from your SmartPhone. Microsoft released a OneNote Mobile client a few years ago, but it was not very useful because it requires an ActiveSync USB synchronization and because it could work only on a Windows Mobile phone. Office 2010 should democratize the use of Office on other SmartPhones, but a few details are available now about OneNote mobile features.

That's why I chose to adapt OneNote to run on a SmartPhone.

Functional overview

My need is to have permanent access to my OneNote notebooks from a SmartPhone. At the time of beginning this project "my SmartPhone" meant an iPhone 3G, and at the end of the project, it means a Palm Pré©. So both these terminals are supported here.

OneNote stores all its contents locally on your hard drive. Of course, accessing OneNote from everywhere needs synchronizing the content in a shared place. What else than Internet to store shared content ? So, I naturally chose to synchronize OneNote "on the Cloud" and, more precisely on Windows Azure.

Here is a functional overview of the process.


It can't be more simple, can it?

Rino detailed architecture

The previous schema is an overview of the process. The next one is an architecture overview of components used to implement this process.

Architecture Overview

The process uses five tasks:

  1. Detect a change in OneNote
  2. Extract OneNote contents as XML
  3. Put OneNote content on an Azure blob storage
  4. View blob content from Safari on iPhone
  5. View blob content from a Palm Pré© application

The following paragraphs describe all these tasks and their implementation. As a dedication to the Microsoft Bing team, I choose to call my application "Rino" like "Rino Is Not OneNote" :-)

Detect a change in OneNote

Tray icon application

To be sure that our Azure storage is always in sync with the OneNote content, we need to have an always running agent on the PC. This agent is a WinForms .NET application hidden on the Windows tray.

Rino tray icon

Doing a Windows tray application in WinForms is as simple as including a NotifyIcon control in your main window.

NotifyIcon in VS

Then, you just need to hide the window when minimized, and show the window on Tray icon double-click.

private void MainWin_Resize(object sender, EventArgs e)
    if (WindowState == FormWindowState.Minimized)

private void notifyIcon_DoubleClick(object sender, EventArgs e)
    WindowState = FormWindowState.Normal;

Watch OneNote directories

Because there is no single way via the OneNote API to be notified of a page update, I chose instead to look for file changes. OneNote stores notebooks in files with a .one extension. These files are usually located in your documents directory. To easily find the place for all files, I look in a Registry key maintained by OneNote and named OpenNoteBooks. The Unfiled Notes section could be located elsewhere, so I need to read another Registry key.

private const string regNotebooksKey = 
private const string unfiledNotebookKey = 
private const string unfiledValueName = "UnfiledNotesSection";

public static List<string> GetNotebookPaths()
    List<string> paths = new List<string>();

    RegistryKey openNotebooks = Registry.CurrentUser.OpenSubKey(regNotebooksKey);
    if (openNotebooks == null)
        return paths;

    string[] names = openNotebooks.GetValueNames();
    foreach (string key in names)
        paths.Add(openNotebooks.GetValue(key) as string);


    RegistryKey unfiledNotebook = Registry.CurrentUser.OpenSubKey(unfiledNotebookKey);
    if (unfiledNotebook == null)
        return paths;

    string unfiledValue = (string)unfiledNotebook.GetValue(unfiledValueName);
    FileInfo unfiledNotebookFile = new FileInfo(unfiledValue);


    return paths;

Finally, to detect a change in these directories, I need to create a FileSystemWatcher for each one.

private void WatchDirectory(string path)
    FileSystemWatcher watcher = new FileSystemWatcher();
    watcher.Path = path;
    watcher.NotifyFilter = NotifyFilters.LastWrite;
    watcher.EnableRaisingEvents = true;
    watcher.SynchronizingObject = this;
    watcher.Changed += new FileSystemEventHandler(OnFileChanged);

private void OnFileChanged(object source, FileSystemEventArgs e)
    Trace(">\"{0}\" changed", e.Name);


Extract OneNote content as XML

OneNote API overview

Thanks to the Microsoft.Office.Interop.OneNote namespace, OneNote allows developers to retrieve and update notebooks programmatically. The base class to use for this is ApplicationClass.

ApplicationClass provides methods for most of the things you would need:

namespace Microsoft.Office.Interop.OneNote
    public class ApplicationClass : IApplication, Application
        public virtual void CloseNotebook(string bstrNotebookID);
        public virtual void CreateNewPage(string bstrSectionID, 
                       out string pbstrPageID, NewPageStyle npsNewPageStyle);
        public virtual void DeleteHierarchy(string bstrObjectID, 
                       DateTime dateExpectedLastModified);
        public virtual void DeletePageContent(string bstrPageID, 
                       string bstrObjectID, DateTime dateExpectedLastModified);
        public virtual void FindMeta(string bstrStartNodeID, 
                       string bstrSearchStringName, out string pbstrHierarchyXmlOut, 
                       bool fIncludeUnindexedPages);
        public virtual void FindPages(string bstrStartNodeID, string bstrSearchString, 
                       out string pbstrHierarchyXmlOut, 
                       bool fIncludeUnindexedPages, bool fDisplay);
        public virtual void GetBinaryPageContent(string bstrPageID, 
                       string bstrCallbackID, out string pbstrBinaryObjectB64Out);
        public virtual void GetHierarchy(string bstrStartNodeID, 
                       HierarchyScope hsScope, out string pbstrHierarchyXmlOut);
        public virtual void GetHierarchyParent(string bstrObjectID, out string pbstrParentID);
        public virtual void GetHyperlinkToObject(string bstrHierarchyID, 
                       string bstrPageContentObjectID, out string pbstrHyperlinkOut);
        public virtual void GetPageContent(string bstrPageID, 
                       out string pbstrPageXmlOut, PageInfo pageInfoToExport);
        public virtual void GetSpecialLocation(SpecialLocation slToGet, 
                       out string pbstrSpecialLocationPath);
        public virtual void NavigateTo(string bstrHierarchyObjectID, 
                       string bstrObjectID, bool fNewWindow);
        public virtual void OpenHierarchy(string bstrPath, string bstrRelativeToObjectID, 
                       out string pbstrObjectID, CreateFileType cftIfNotExist);
        public virtual void OpenPackage(string bstrPathPackage, 
                       string bstrPathDest, out string pbstrPathOut);
        public virtual void Publish(string bstrHierarchyID, string bstrTargetFilePath, 
                       PublishFormat pfPublishFormat, string bstrCLSIDofExporter);
        public virtual void UpdateHierarchy(string bstrChangesXmlIn);
        public virtual void UpdatePageContent(string bstrPageChangesXmlIn, 
                            DateTime dateExpectedLastModified);

Because my need is to extract notebooks content, I first use the GetHierarchy method which returns an XML document including all the information and content regarding opened notebooks.

OneNote.ApplicationClass onApp = new OneNote.ApplicationClass();
string xml;

onApp.GetHierarchy(null, OneNote.HierarchyScope.hsPages, out xml);
XDocument document = XDocument.Parse(xml);

OneNote objects

Contents in OneNote are dispatched in three classes of objects: Notebooks, Sections, and Pages.

  • Notebook is the first level of a OneNote object. A notebook is stored in a .one file. On the OneNote user interface, notebooks could be handled with buttons at the left side. A notebook contains one to many sections.
  • Section is the second level. On the user interface, sections are sheet pages on the top of the screen. A section has a specific color that each page uses. A section contains one to many pages.
  • Page is the lower level. A page contains text, image, link, embedded files, ...

All these objects are extractible via the API. A nice tool from Ilya Koulchin named OMSpy allows you to browse all this hierarchy.

OMSpy view

.NET objects

To simplify memory handling of these objects, I map them to some equivalent .NET class. Here's the full hierarchy.

.NET class hierarchy for OneNote

Notebook and Page objects are split in two classes to allow lazy loading during Azure request. A User class was also added to hold all notebooks.

Mapping is done with a few LINQ to XML requests similar to this one:

XDocument document = XDocument.Parse(xml);

var notebooks = (from notebook in document.Descendants(XNameNotebook)
             where notebook.Attribute("ID").Value == header.ID
             select new Notebook
                 ID = notebook.Attribute("ID").Value,
                 Name = notebook.Attribute("name").Value,
                 Nickname = notebook.Attribute("nickname").Value,
                 Color = notebook.Attribute("color").Value,
                 Sections = LoadSections(notebook, out date),
                 LastModifiedTime = 

Put OneNote content on an Azure blob storage

Azure storage structure

Windows Azure is both a platform to host web/background services and a storage platform. Azure storage provides three ways to store information: blobs, tables, and queues.

Blobs allow storage for a large amount of data (up to 50 gigabytes) though Tables limit the size of each field to 64K. Because a OneNote page could include images, it could have a size of more than 64 KB, so I decided to store contents into Azure Blobs.

More precisely, the Rino application synchronizes OneNote contents into three blob containers: one for users, one for notebooks, and one for pages. Here is a screen capture of these containers in the nice BlobExplorer tool from Richard Blewett.

Blob Explorer

Azure storage API

Azure storage could be requested using a dedicated REST API. However, the Windows Azure SDK includes some samples providing a set of .NET classes to encapsulate most Azure storage features. I choose here to call methods from the StorageClient sample.

Thanks to this sample, here's the source code to save an object into a Blob:

private static bool SaveObject(BlobContainer container, string name, object o, Type t)
        MemoryStream stream = new MemoryStream();
        DataContractSerializer serializer = new DataContractSerializer(t);
        serializer.WriteObject(stream, o);
        stream.Position = 0;

        container.CreateBlob(new BlobProperties(name), new BlobContents(stream), true);


        return true;
    catch (StorageException)
        return false;
    catch (WebException)
        return false;

As you can see, the SaveObject method takes as parameter the .NET object to save (either User, Page, or Notebook) and serializes it in a stream. Then, the whole stream content is just stored in a new Azure blob.

Conversely, here is the source code for LoadObject. Of course, I just need to deserialize the previously stored blob content.

private static object LoadObject(BlobContainer container, string name, Type t)
        BlobContents contents = new BlobContents(new MemoryStream());
        BlobProperties properties = container.GetBlob(name, contents, false);
        if (properties.Name != name)
            return null;

        DataContractSerializer serializer = new DataContractSerializer(t);
        Stream stream = contents.AsStream;
        stream.Position = 0;
        object o = serializer.ReadObject(stream);


        return o;
    catch (StorageException se)
        if (se.StatusCode == HttpStatusCode.NotFound)
            return null;
            throw se;

Synchronize only when needed

Each class (User, Notebook, Page) contains a LastModifiedTime property. So each time a notebook has changed, the Rino Tray application makes a comparison for each section, then for each page to save only the updated pages. Finally, a check is done to delete pages which no longer appear in the notebooks. Here is a part of this algorithm:

public void UpdateSection(Section machineSection, Notebook azureNotebook, bool forceUpdate)
    // Compute last Azure update
    DateTime lastAzureUpdate = DateTime.MinValue;
    if (azureUser != null)
        lastAzureUpdate = azureUser.LastModifiedTime;

    // Check each page
    List<string> processed = new List<string>();
    foreach (PageHeader machinePage in machineSection.Pages)
        // Page updated in OneNote since last update in Azure ?
        if (forceUpdate || machinePage.LastModifiedTime > lastAzureUpdate)
            // Yes, update page in Azure

    // Check the Section in Azure
    if (azureNotebook != null)
        // Look for this section since the last Azure update
        foreach (Section azureSection in azureNotebook.Sections)
            // Check if its the right section
            if (azureSection.ID != machineSection.ID)

            // Check each Page
            foreach (PageHeader pageAzure in azureSection.Pages)
                // Page already processed previously ?
                if (!processed.Contains(pageAzure.ID))
                    // No, the Section need to be deleted in Azure

WCF REST service

Since the OneNote content is stored in Azure Blob storage, it's time now to expose these objects to the outside. I chose to expose it as REST services so each object could be created/updated/deleted with a single HTTP request.

Thanks to the recent WCF upgrade in .NET Framework 3.5, REST services are very easy to write in .NET with a few attributes.

Here is a part of the Rino WCF REST service hosted by Azure:

[AspNetCompatibilityRequirements(RequirementsMode = 
 ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class Service
    [WebInvoke(Method = "PUT", UriTemplate = "notebooks/{notebookid}")]
    bool PutNotebook(string notebookid, Notebook notebook)
        return AzureStorage.SaveNotebook(notebook);

    [WebGet(UriTemplate = "notebooks/{notebookid}", 
               ResponseFormat = WebMessageFormat.Json)]
    public Notebook GetNotebook(string notebookid)
        Notebook notebook = AzureStorage.LoadNotebook(notebookid);
        if (notebook != null)
            return notebook;

        return null;

    [WebInvoke(Method = "DELETE", UriTemplate = "notebooks/{notebookid}")]
    bool DeleteNotebook(string notebookid)
        return AzureStorage.DeleteNotebook(notebookid);

A REST service is just a .NET class with a ServiceContract attribute. Each method to expose is identified by a OperationContract attribute and by a WebGet/WebInvoke attribute (for HTTP GET or HTTP PUT) giving the URL template to call the method. Using the WebMessageFormat.Json value, I'm telling WCF to deserialize a .NET object into a JSON object equivalent.

So, I now have the back office to access OneNote content from the outside.

View blob content from iPhone

About iPhone development

When you think of iPhone development, you probably think of something like: Objective C, Cocoa, AppStore, ... To be honest, I'm not at all familiar with Apple technologies or with MacOS. And whatever technology you use, if you don't have a Mac, you just can't develop iPhone applications!

However, if you want to avoid buying a Mac, to avoid learning Objective C and to avoid the complex validation process of AppStore, you could write your iPhone application using... Safari. Safari includes WebKit extensions which provide to your web application the iPhone look and feel. Moreover, a URL shortcut could become an icon on the iPhone desktop, providing an integrated experience to your user. It's why lot of iPhone applications (including iPhone pre-installed applications) are just web applications.

So why not develop iPhone applications using ASP.NET? The Rino iPhone application is just an ASP.NET application hosted as an Azure Web Role.

So note that we don't need to use the WCF service.


iUI is a nice framework consisting of a JavaScript library, CSS, and images for developing iPhone webapps. It's very simple to integrate iUI in ASP.NET: you just have to include the iUI directory in your ASP.NET project. It's what I've done to access OneNote from iPhone.

Here is the source code for the main window:

<%@ Page Language="C#" AutoEventWireup="true" 
  CodeBehind="nbooks.aspx.cs" Inherits="Rino.iphone.nbooks" %>

<%@ Register src="iphonelist.ascx" 
      tagname="iphonelist" tagprefix="uc1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 

<html xmlns="" >
<head runat="server">
    <meta name="viewport" content="width=320; 
           initial-scale=1.0; maximum-scale=1.0; user-scalable=0;"/>
    <link rel="apple-touch-icon" href="rino-icon.png" />
    <meta name="apple-touch-fullscreen" content="YES" /> 
    <meta name="apple-mobile-web-app-capable" content="yes" />   
    <style type="text/css" 
        media="screen">@import "iui/iui.css";</style>
    <script type="application/x-javascript" 
    <div class="toolbar">
        <h1 id="pageTitle"></h1>
        <a id="backButton" class="button" href="#"></a>
    <uc1:iphonelist id="home" selected="true" 
       title="Notebooks" runat="server" />

It's a standard WebForm except that:

  • A few tags (meta and link) are used to give information to Safari about the page appearance.
  • The style sheet and the JavaScript Framework of iUI are included.
  • The div tag gives information to Safari about the standard iPhone toolbar appearance.

Finally, a custom control named iPhoneList is used in the WebForm. All screens of the Rino iPhone application are built on the same way.


The following images are screen captures of the application running on iPhone. Just one screen is missing here: the page list of a section.

iPhone desktopiPhone Rino NotebooksiPhone Rino SectionsiPhone Rino Page

However, as you could see, most of the application could be resumed to iPhone lists. It's what the iPhoneList custom control is used for.

Here is the main part of this control, including the rendering function.

public partial class iPhoneList : System.Web.UI.UserControl
    private class ListItem
        public string text;
        public string color;
        public string link;

    public bool selected { get; set; }
    public string title { get; set; }
    private List<ListItem> Items;
    protected override void Render(HtmlTextWriter writer)
        writer.Write("<ul id='" + ID + "' title='" + 
                     HttpUtility.HtmlEncode(title) + "'");
        if (selected)
            writer.Write(" selected='true'");

        foreach (ListItem item in Items)
            if (item.color != null)
                writer.Write(" style='background:" + item.color + "'");

            if ( != null)
                writer.Write("<a href='" + + "'>");
            if ( != null)



The iPhoneList custom control just renders an HTML UL tag. Items are described by a specific class named ListItem to include some specific attributes like the color of the notebook/section.

Filling each list is pretty easy: I just need to load each object from the Azure storage to the fill the list.

Here is the source code to fill the notebook screen:

protected void Page_Load(object sender, EventArgs e)
    string notebookid = Request.QueryString["notebookid"];
    if (notebookid == null)
        content.AddItem("No notebook");

    Notebook notebook = AzureStorage.LoadNotebook(notebookid);
    if (notebook == null)
        content.AddItem("Notebook don't exist");
    content.title = notebook.Nickname;

    foreach (Section section in notebook.Sections)
        string url = String.Format("section.aspx?notebookid={0}&sectionid={1}", 
                                   notebook.ID, section.ID);

        content.AddItem(section.Name, section.Color, url);

View blob content from Palm Pré©

About Palm Pré© developpement

Palm Pré© is a very cool terminal including a very cool Operating System: webOS. "webOS" means that the whole OS is built on the concept of web, so each webOS application is just a few HTML 5 pages and a bunch of JavaScript. Palm webOS SDK includes some utilities to generate and package applications and an emulator based on VirtualBox running either on Windows, Linux, or MacOS.

There is no Integrated Development Environment for webOS. But because a webOS application is just HTML 5 and JavaScript, you could fully use Visual Studio as the development environment. So I've created a dummy DLL project and copied/cut it into directories needed for a webOS application. Here is a full view of the project:

WebOS project in VS.NET

webOS applications are built on the JavaScript Mojo Framework and are compliant with the MVC model. So, views are separated from process and data. Each page is a view, and the JavaScript code to handle this page is located in a controller.

webOS screens

Like for the iPhone application, the Rino webOS application is pretty simple: it's just screens including a list (for notebooks, sections, or pages).

Palm Pré desktopPalm Rino NotebooksPalm Rino SectionsPalm Rino Page

Here is the HTML file for the notebook view:

<div id="Div1">
    <div class="palm-header center" id="main-hdr">
<div style="margin-top:40pt; margin-left:0pt; margin-right:0pt" class="palm-list">
    <div x-mojo-element="List" id="notebooksListWgt" ></div>
    <div  style="margin-top:120pt; margin-left:70pt; 
      margin-right:0pt" x-mojo-element="Spinner" 
      id="waitingWgt" class="spinnerClass"></div>

Controls in a webOS applications are named Widgets. They are described in a div tag of the HTML file. Here you could see the header and two controls: the most important is the list (see below) and the other one is a spinner control displayed only during the loading process.

Two other HTML files are need to describe the list Widget:

<div class="palm-list">#{-listElements}</div>

<div class="palm-row" x-mojo-tap-highlight="momentary" 
        <div class="palm-row-wrapper">
            <div id="sectionName" class="title truncating-text" 

The first one describes the list, the second one describes the row template. As we're going to see, both give binding information to link data to the view.

Finally, here is the JavaScript code from the controller to display the screen and its widgets. You can see the setup of each widget using the setupWidget function.

NotebookViewAssistant.prototype.setup = function() {
    $("main-hdr").innerHTML = this.notebook.Nickname;

        this.sectionAttr = {
            itemTemplate: "notebookView/sectionRowTemplate",
            listTemplate: "notebookView/sectionListTemplate",
            swipeToDelete: false,
            renderLimit: 40,
            reorderable: false
        this.sectionModel = {
            items: []
         this.attributes = {
             spinnerSize: 'large'
         this.model = {
             spinning: true
    this.controller.listen("sectionListWgt", Mojo.Event.listTap,


webOS binding to JSON objects

Let's now see how to fill the list with the contents of a notebook.

It's a two step process:

  • Call the WCF REST service.
  • Bind the result to the list.

Calling the WCF REST service is done using an AJAX request. The called URL contains the address of the GetNotebook method defined previously, followed by the notebook ID.

NotebookViewAssistant.prototype.LoadNotebook = function(ID) {
    // Launch REST request
    var request = new Ajax.Request(URL.Notebook + ID, {
        method: 'get',
        evalJSON: 'false',
        onSuccess: this.LoadNotebookSuccess.bind(this),
        onFailure: this.LoadNotebookFailure.bind(this)

I remind you that the WCF service returns JSON objects. So at the end of the request, I've got a ready-to-bind JavaScript object. Then, I just need to attach it to the list, and the binding is automatic.

NotebookViewAssistant.prototype.LoadNotebookSuccess = function(response) {
    // Stop spinner

    // Get content
    this.notebook = response.responseJSON;
    // Bind to the view
    this.sectionModel.items = this.notebook.Sections;

Other features


All events or actions in the Rino Tray application are logged on the Main Window. So if you double-click on the tray icon, you could have a view of what happens during sync.

Rino Tray in action

As you could see, to avoid overloading of the PC, Rino waits for a long idle time period (no mouse and keyboard event) before updating content.

Choose notebook to sync

Finally, the "Options" button allows you to choose which notebook should be synced to the Cloud and which should not.

Rino Tray options


The Rino application described in this article is just a proof of concept. However, it allows you to understand how Azure could be used to synchronized content from a local machine and give it access from other places. The next step should be to allow not just reading but also updating information.


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

Written By
Architect C2S
France France
Lionel is a software architect at C2S, a software company based in France and subsidiary of the Bouygues group.
Lionel is also the author of Liogo, an open-source Logo compiler for .NET.
Lionel is a contributor of DotNetGuru and Dr.Dobb's Journal.
Lionel is President and co-founder of OLPC France.

Comments and Discussions

GeneralMy vote of 5 Pin
Monjurul Habib26-Dec-11 7:41
professionalMonjurul Habib26-Dec-11 7:41 
GeneralMy vote of 5 Pin
Stanciu Vlad15-Nov-11 7:38
Stanciu Vlad15-Nov-11 7:38 
GeneralMy vote of 5 Pin
maq_rohit23-Mar-11 7:37
professionalmaq_rohit23-Mar-11 7:37 
GeneralMy vote of 1 Pin
Andrey Mazoulnitsyn9-May-10 2:13
Andrey Mazoulnitsyn9-May-10 2:13 
GeneralHave a 5! Pin
pophelix19-Jan-10 19:34
pophelix19-Jan-10 19:34 
GeneralNice Article Pin
Abhijit Jana12-Jan-10 3:28
professionalAbhijit Jana12-Jan-10 3:28 
GeneralGreat Article Pin
L Hills11-Jan-10 3:01
L Hills11-Jan-10 3:01 
GeneralNice Pin
Bassam Saoud28-Dec-09 11:16
Bassam Saoud28-Dec-09 11:16 
Generalvery fine app! Pin
Marcelo Ricardo de Oliveira28-Dec-09 10:43
mvaMarcelo Ricardo de Oliveira28-Dec-09 10:43 
GeneralVery nice Pin
Sacha Barber27-Dec-09 23:53
Sacha Barber27-Dec-09 23:53 
GeneralWow! Pin
Milkweed27-Dec-09 8:23
Milkweed27-Dec-09 8:23 
GeneralDude this is Brilliant! PinPopular
ThinkMud26-Dec-09 14:15
ThinkMud26-Dec-09 14:15 

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.