Abstract
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.
The process uses five tasks:
- Detect a change in OneNote
- Extract OneNote contents as XML
- Put OneNote content on an Azure blob storage
- View blob content from Safari on iPhone
- 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.
Doing a Windows tray application in WinForms is as simple as including a NotifyIcon
control in your main window.
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)
Hide();
}
private void notifyIcon_DoubleClick(object sender, EventArgs e)
{
Show();
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 =
"Software\\Microsoft\\Office\\12.0\\OneNote\\OpenNotebooks";
private const string unfiledNotebookKey =
"Software\\Microsoft\\Office\\12.0\\OneNote\\Options\\Paths";
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);
openNotebooks.Close();
RegistryKey unfiledNotebook = Registry.CurrentUser.OpenSubKey(unfiledNotebookKey);
if (unfiledNotebook == null)
return paths;
string unfiledValue = (string)unfiledNotebook.GetValue(unfiledValueName);
FileInfo unfiledNotebookFile = new FileInfo(unfiledValue);
paths.Add(unfiledNotebookFile.DirectoryName);
unfiledNotebook.Close();
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);
RequestComparison();
}
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.
.NET objects
To simplify memory handling of these objects, I map them to some equivalent .NET class. Here's the full hierarchy.
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 =
Convert.ToDateTime(notebook.Attribute("lastModifiedTime").Value)
}
);
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.
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)
{
try
{
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);
stream.Close();
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)
{
try
{
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);
stream.Close();
return o;
}
catch (StorageException se)
{
if (se.StatusCode == HttpStatusCode.NotFound)
return null;
else
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)
{
DateTime lastAzureUpdate = DateTime.MinValue;
if (azureUser != null)
lastAzureUpdate = azureUser.LastModifiedTime;
List<string> processed = new List<string>();
foreach (PageHeader machinePage in machineSection.Pages)
{
if (forceUpdate || machinePage.LastModifiedTime > lastAzureUpdate)
{
UpdatePage(machinePage);
}
processed.Add(machinePage.ID);
}
if (azureNotebook != null)
{
foreach (Section azureSection in azureNotebook.Sections)
{
if (azureSection.ID != machineSection.ID)
continue;
foreach (PageHeader pageAzure in azureSection.Pages)
{
if (!processed.Contains(pageAzure.ID))
{
DeletePage(pageAzure);
}
}
}
}
}
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 =
AspNetCompatibilityRequirementsMode.Allowed),
ServiceBehavior(IncludeExceptionDetailInFaults = true)]
[ServiceContract]
public class Service
{
[WebInvoke(Method = "PUT", UriTemplate = "notebooks/{notebookid}")]
[OperationContract]
bool PutNotebook(string notebookid, Notebook notebook)
{
return AzureStorage.SaveNotebook(notebook);
}
[WebGet(UriTemplate = "notebooks/{notebookid}",
ResponseFormat = WebMessageFormat.Json)]
[OperationContract]
public Notebook GetNotebook(string notebookid)
{
Notebook notebook = AzureStorage.LoadNotebook(notebookid);
if (notebook != null)
return notebook;
WebOperationContext.Current.OutgoingResponse.SetStatusAsNotFound();
return null;
}
[WebInvoke(Method = "DELETE", UriTemplate = "notebooks/{notebookid}")]
[OperationContract]
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 and ASP.NET
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"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Rino</title>
<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"
src="iui/iui.js"></script>
</head>
<body>
<div class="toolbar">
<h1 id="pageTitle"></h1>
<a id="backButton" class="button" href="#"></a>
</div>
<uc1:iphonelist id="home" selected="true"
title="Notebooks" runat="server" />
</body>
</html>
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.
iPhoneList
The following images are screen captures of the application running on iPhone. Just one screen is missing here: the page list of a section.
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'");
writer.Write(">");
foreach (ListItem item in Items)
{
writer.Write("<li");
if (item.color != null)
writer.Write(" style='background:" + item.color + "'");
writer.Write(">");
if (item.link != null)
writer.Write("<a href='" + item.link + "'>");
writer.Write(HttpUtility.HtmlEncode(item.text));
if (item.link != null)
writer.Write("</a>");
writer.Write("</li>");
}
writer.Write("</ul>");
}
...
}
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");
return;
}
Notebook notebook = AzureStorage.LoadNotebook(notebookid);
if (notebook == null)
{
content.AddItem("Notebook don't exist");
return;
}
content.title = notebook.Nickname;
foreach (Section section in notebook.Sections)
{
string url = String.Format("section.aspx?notebookid={0}§ionid={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 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).
Here is the HTML file for the notebook view:
<div id="Div1">
<div class="palm-header center" id="main-hdr">
Notebooks
</div>
</div>
<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>
</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"
x-mojo-gesture="Mojo.Gesture.HorizontalDrag">
<div class="palm-row-wrapper">
<div id="sectionName" class="title truncating-text"
style="background:#{Color}">#{Name}</div>
</div>
</div>
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.controller.setupWidget("sectionListWgt",
this.sectionAttr = {
itemTemplate: "notebookView/sectionRowTemplate",
listTemplate: "notebookView/sectionListTemplate",
swipeToDelete: false,
renderLimit: 40,
reorderable: false
},
this.sectionModel = {
items: []
}
);
this.controller.setupWidget("waitingWgt",
this.attributes = {
spinnerSize: 'large'
},
this.model = {
spinning: true
}
);
this.controller.listen("sectionListWgt", Mojo.Event.listTap,
this.viewSection.bindAsEventListener(this));
this.LoadNotebook(this.notebook.ID);
};
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) {
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) {
$("waitingWgt").hide();
this.notebook = response.responseJSON;
this.sectionModel.items = this.notebook.Sections;
this.controller.modelChanged(this.sectionModel);
};
Other features
Logging
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.
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.
Conclusion
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.