OneNote 2010

使用 OneNote 对象模型创建 OneNote 2010 扩展

Andy Gray

下载代码示例

Microsoft Office OneNote 是一款强大的电子笔记本软件,用于收集、整理、搜索和共享信息。在最近发布的 Microsoft Office 2010 中,不仅 OneNote 的用户体验得到改善,而且 OneNote 笔记本的使用范围也变得更加广泛。用户可以通过 Windows Live 在多台计算机之间同步笔记内容;通过任意 Web 浏览器搜索、编辑和共享笔记;从 Windows Mobile(包括很快就要面世的 Windows Phone 7)访问所有笔记。而且,OneNote 以前只包含在部分 Office 版本中,现在 Office 2010 的每个版本中都提供了 OneNote。所有这一切都意味着出现了前所未有的机遇,使 OneNote 能够集成到信息管理解决方案之中。

在本文中,我将概要介绍如何开发与 Microsoft OneNote 2010 和 2007 中的数据进行互操作的应用程序。期间,我将介绍可从 CodePlex 免费获得的 OneNote 对象模型项目,并展示如何使用这个库轻松地将 OneNote 笔记本、分区和页面中的信息集成到客户端应用程序。

OneNote 开发的演变

最早发布的 OneNote 2003 并未提供面向外部应用程序的 API。但此后不久,OneNote 2003 SP 1 加入了一个 COM 库,称为 OneNote 1.1 类型库。使用这个库,可以通过名为 CSimpleImporter 的简单类以编程方式向 OneNote 中导入图形、墨迹和 HTML。很显然,这个类只能提供数据导入功能,您可以使用它将数据推入 OneNote 笔记本,却无法以编程方式从笔记本中取回内容。

OneNote 2007 发布之后,其中提供了一个新的 COM API,可支持更强大的开发功能。使用这个 API,您可以通过编程方式导入、导出和修改 OneNote 2007 内容。这个库中的 OneNote 应用程序类有各种各样的方法可供开发人员使用:

  • 笔记本结构: 发现、打开、修改、关闭和删除笔记本、分区组和分区
  • 页面内容: 发现、打开、修改、保存和删除页面内容
  • 导航: 查找、链接和导航到页面和对象

这些方法中的大部分都可以返回或接受包含笔记本结构和页面内容的 XML 文档。Saul Candib 撰写了包含上下两部分的系列文章“OneNote 2007 中面向开发人员的新增功能”,介绍了此 API,其地址是 msdn.microsoft.com/library/ms788684(v=office.12);XML 架构则在 msdn.microsoft.com/library/aa286798(office.12) 有详细论述。

OneNote 2010 的 XML 架构基本上与 OneNote 2007 的相同。但 OneNote 2010 对文件格式有所更改,以支持一些新功能(例如以链接方式记笔记、版本控制、Web 共享、多级子页和公式支持)。但是,OneNote 2010 可以继续处理 OneNote 2007 笔记本而无需更改文件格式。在 OneNote 2010 中,从以 OneNote 2007 文件格式存储的分区检索数据将获得与 OneNote 2007 中的 XML 文档相似的文档。OneNote 2010 分区的 XML 架构中的最主要区别是添加了一些更改,以支持前文所述的新功能。可以使用新的 XMLSchema 枚举来代表 OneNote 架构版本。很多 OneNote 方法都有带有 XMLSchema 参数的新重载方法来指示所需的架构版本。

请注意,在 OneNote 2003 中引入并在 OneNote 2007 中继续提供的 CSimpleImporter 类已从 OneNote 2010 中删除,因此使用该类的应用程序需要重写,以便使用新的接口与 OneNote 2010 互操作。

使用 COM API 访问 OneNote 数据

使用 OneNote COM API 访问 OneNote 笔记本中的实时数据非常简单。首先在 Visual Studio 中创建一个新的控制台应用程序,然后添加对 Microsoft OneNote 14.0 类型库 COM 组件(对于 OneNote 2010)或 Microsoft OneNote 12.0 类型库 COM 组件(对于 OneNote 2007)的引用。

如果您使用 Visual Studio 2010 来开发 OneNote 2010 应用程序,需要注意两个有关兼容性的小问题。首先,由于与 Visual Studio 2010 随附的 OneNote 互操作程序集不匹配,您不应该在“添加引用”对话框的“.NET”选项卡上直接引用 Microsoft.Office.Interop.OneNote 组件,而是应该在“COM”选项卡上引用 Microsoft OneNote 14.0 类型库组件。这样做仍会向项目的引用中加入 OneNote 互操作程序集。

其次,OneNote 14.0 类型库与 Visual Studio 2010“NOPIA”功能不兼容。在 NOPIA 功能中,主互操作程序集默认情况下不会嵌入到应用程序中。因此,请务必将 OneNote 互操作程序集引用的“嵌入互操作类型”属性设置为 False。(这两个问题在 OneNote 项目经理 Daniel Escapa 的博客 blogs.msdn.com/descapa/archive/2010/04/27/onenote-2010-and-visual-studio-2010-compatibility-issues.aspx 中有详细介绍。)添加 OneNote 库引用之后,您就可以调用 OneNote API 了。图 1 中的代码使用 GetHierarchy 方法来检索包含 OneNote 笔记本列表的 XML 文档,然后使用 LINQ to XML 来提取笔记本的名称并将其输出到控制台。

图 1 枚举笔记本

using System;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Office.Interop.OneNote;

class Program
{
  static void Main(string[] args)
  {
    var onenoteApp = new Application();

    string notebookXml;
    onenoteApp.GetHierarchy(null, HierarchyScope.hsNotebooks, out notebookXml);
    
    var doc = XDocument.Parse(notebookXml);
    var ns = doc.Root.Name.Namespace;
    foreach (var notebookNode in 
      from node in doc.Descendants(ns + "Notebook") select node)
    {
      Console.WriteLine(notebookNode.Attribute("name").Value);
    }
  }
}

HierarchyScope 枚举作为 GetHierarchy 方法的第二个参数传递,它指定了要检索的笔记本结构的深度。如果既要检索笔记本又要检索分区,只需将此枚举值更新为 HierarchyScope.hsSections 并处理更多的 XML 子节点,如图 2 所示。

图 2 枚举分区

using System;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Office.Interop.OneNote;

class Program
{
  static void Main(string[] args)
  {
    var onenoteApp = new Application();

    string notebookXml;
    onenoteApp.GetHierarchy(null, HierarchyScope.hsSections, out notebookXml);
    
    var doc = XDocument.Parse(notebookXml);
    var ns = doc.Root.Name.Namespace;
    foreach (var notebookNode in from node in doc.Descendants(ns + 
      "Notebook") select node)
    {
      Console.WriteLine(notebookNode.Attribute("name").Value);
      foreach (var sectionNode in from node in 
        notebookNode.Descendants(ns + "Section") select node)
      {
        Console.WriteLine("  " + sectionNode.Attribute("name").Value);
      }
    }
  }
}

检索和更新页面内容

GetPageContent 方法将返回包含指定页面中所有内容的 XML 文档。要检索的页面通过 OneNote 对象 ID 指定。这个字符串类型的 ID 是 OneNote 笔记本层次结构中每个对象的唯一标识符。该对象 ID 将作为 GetHierarchy 方法返回的 XML 节点的特性。

图 3 构建在前面的示例基础之上,使用 GetHierarchy 方法来检索 OneNote 笔记本层次结构,直到页面范围。然后使用 LINQ to XML 选择名为“Test page”的页面的节点,并将该页面的对象 ID 传递到 GetPageContent 方法。代表页面内容的 XML 文档随后被输出到控制台。

图 3 获取页面内容

using System;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Office.Interop.OneNote;

class Program
{
  static void Main(string[] args)
  {
    var onenoteApp = new Application();

    string notebookXml;
    onenoteApp.GetHierarchy(null, HierarchyScope.hsPages, out notebookXml);

    var doc = XDocument.Parse(notebookXml);
    var ns = doc.Root.Name.Namespace;
    var pageNode = doc.Descendants(ns + "Page").Where(n => 
      n.Attribute("name").Value == "Test page").FirstOrDefault();
    if (pageNode != null)
    {
      string pageXml;
      onenoteApp.GetPageContent(pageNode.Attribute("ID").Value, out pageXml);
      Console.WriteLine(XDocument.Parse(pageXml));
    }
  }
}

可以使用 UpdatePageContent 方法来更改页面。页面内容由图 3 中的代码检索的同一 XML 文档架构指定,可以包含各种内容元素,包括定义文本边框、插入文件、图像、墨迹以及音频或视频文件的各种元素。

UpdatePageContent 方法将所提供的 XML 文档中的元素视作可能已更改的内容集合,通过 OneNote 对象 ID 将指定内容与现有内容进行匹配。因此您可以通过以下方法来更改现有内容:调用 GetPageContent 方法,对返回的 XML 进行所需的更改,然后将该 XML 传回 UpdatePageContent 方法。您还可以指定要添加到页面的新内容元素。

为说明这一点,图 4 在测试页面的底部添加了一个日期戳记。它使用图 3 中显示的方法来确定页面的 OneNote 对象 ID,然后使用 System.Xml.Linq 中的 XDocument 和 XElement 类来构造包含新内容的 XML 文档。由于文档中指定的页面对象 ID 与现有页面的对象 ID 相匹配,因此 UpdatePageContent 方法将在现有页面上附加新内容。

图 4 更新页面内容

using System;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Office.Interop.OneNote;

class Program
{
  static void Main(string[] args)
  {
    var onenoteApp = new Application();

    string notebookXml;
    onenoteApp.GetHierarchy(null, HierarchyScope.hsPages, out notebookXml);

    var doc = XDocument.Parse(notebookXml);
    var ns = doc.Root.Name.Namespace;
    var pageNode = doc.Descendants(ns + "Page").Where(n => 
      n.Attribute("name").Value == "Test page").FirstOrDefault();
    var existingPageId = pageNode.Attribute("ID").Value;

    if (pageNode != null)
    {
      var page = new XDocument(new XElement(ns + "Page", 
                                 new XElement(ns + "Outline", 
                                   new XElement(ns + "OEChildren", 
                                     new XElement(ns + "OE", 
                                       new XElement(ns + "T", 
                                         new XCData("Current date: " +
                                           DateTime.Now.
                                             ToLongDateString())))))));
       page.Root.SetAttributeValue("ID", existingPageId);
       onenoteApp.UpdatePageContent(page.ToString(), DateTime.MinValue);
    }
  }
}

OneNote 对象模型库

用上述方法与 OneNote 数据进行交互并不是很困难,但如果只是执行基本的数据操作,则分析和构造 XML 文档就有些费事了。这时就用到了 OneNote 对象模型。它是一个托管的代码库,为基于 COM 的 OneNote API 提供面向对象的抽象。这个库是开源的,依照 Microsoft Public License (Ms-PL) 进行授权。

OneNote 对象模型可从位于 onom.codeplex.com 的 CodePlex 下载。该库是专为 OneNote 2007 设计的。在本文发表时,公布的下载应该已经更新,可以兼容 OneNote 2010。如果尚未更新,您可以按照前文所述的方法,下载源代码、删除 OneNoteCore 项目中现有的 Microsoft.Office.Interop.OneNote 程序集引用并添加对 Microsoft OneNote 14.0 类型库的引用,以在 OneNote 2010 中使用 OneNote 2007 分区。

除了包括一些单元测试项目和示例代码,该解决方案还包括两个类库项目:OneNoteCore 和 OneNoteFramework。OneNoteCore 库是 OneNote COM API 和我们熟知的 Microsoft .NET Framework 隐喻之间的低层桥梁,它提供真正的返回值而不是 COM out 参数,将 COM 错误代码转变为 .NET 异常,提供 OneNoteObjectId 结构和 XDocument 实例而不是原始字符串,以及其他功能。研究这段代码有助于您了解 OneNote API 的工作原理,但大多数情况下,您无需直接与 OneNoteCore 库交互。

OneNoteFramework 库提供 OneNote 概念的更高级抽象。此库中的类名称很直观,例如 OneNoteNotebook、OneNoteSection 和 OneNotePage。与 OneNote 层次结构进行交互的主要入口点是名为 OneNoteHierarchy 的类,它包含一个名为 Current 的静态成员。通过添加对 OneNoteFramework 库的程序集引用,我们可以重写程序,以更加简洁地枚举笔记本名称(图 1),如下所示:

using Microsoft.Office.OneNote;

class Program
{
  static void Main(string[] args)
  {
    foreach (var notebook in OneNoteHierarchy.Current.Notebooks)
      System.Console.WriteLine(notebook.Name);
  }
}

正如您可能想到的,OneNoteNotebook 类具有一个名为 Sections 的属性。因此,您可以简单地枚举分区名称(图 2),如下所示:

using Microsoft.Office.OneNote;

class Program
{
  static void Main(string[] args)
  {
    foreach (var notebook in OneNoteHierarchy.Current.Notebooks)
    {
      System.Console.WriteLine(notebook.Name);
      foreach (var section in notebook.Sections)
      {
        System.Console.WriteLine("  " + section.Name);
      }
    }
  }
}

OneNote 对象模型属性提供的集合使用专门的泛型集合类 OneNoteObjectCollection<T> 进行管理。由于 OneNoteObjectCollection<T> 实现了 IList<T> 以及 IEnumerable<T>,因此可以使用 LINQ 来查询这些集合。

例如,假设分区变量中有一个对 OneNoteSection 实例的引用,我们可以使用简单的 LINQ 表达式确定今天修改过的所有页面,如下所示:

var pagesModifiedToday = from page in section.Pages 
                           where page.LastModifiedTime >= DateTime.Today 
                           select page;

使用 OneNote 对象模型库执行数据绑定

OneNote 对象模型提供 IEnumerable 集合,因此也能与 Windows Presentation Foundation (WPF) 实现基于 XAML 的数据绑定。图 5 显示了使用数据绑定来显示 OneNote 笔记本层次结构的 WPF TreeView(只使用了 XAML 标记,而无需使用代码隐藏)。

图 5 与 Windows Presentation Foundation 执行数据绑定

<Window x:Class="NotebookTree.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:onf="clr-namespace:Microsoft.Office.OneNote;assembly=
          OneNoteFramework"
        Title="OneNote Notebook Hierarchy" >
  <Grid>
    <Grid.Resources>
      <DataTemplate x:Key="PageTemplate">
        <StackPanel Orientation="Horizontal">
          <Image Source="Images\Page16.png" Margin="0,0,2,0"/>
          <TextBlock Text="{Binding Name}" />
        </StackPanel>
      </DataTemplate>
            
      <HierarchicalDataTemplate x:Key="SectionTemplate" 
        ItemsSource="{Binding Pages}"
        ItemTemplate="{StaticResource PageTemplate}">
        <StackPanel Orientation="Horizontal">
          <Image Source="Images\Section16.png" Margin="0,0,2,0"/>
          <TextBlock Text="{Binding Name}" />
        </StackPanel>
      </HierarchicalDataTemplate>
            
      <HierarchicalDataTemplate x:Key="NotebookTemplate" 
        ItemsSource="{Binding Sections}"
        ItemTemplate="{StaticResource SectionTemplate}">
        <StackPanel Orientation="Horizontal">
          <Image Source="Images\Book16.png" Margin="0,0,2,0"/>
          <TextBlock Text="{Binding Name}" />
        </StackPanel>
      </HierarchicalDataTemplate>
    </Grid.Resources>
        
    <TreeView Name="NotebookTree" BorderThickness="0"
              HorizontalAlignment="Left" VerticalAlignment="Top"
              ItemsSource="{Binding Notebooks}" 
              ItemTemplate="{StaticResource NotebookTemplate}" 
              DataContext="{Binding Source={x:Static 
                onf:OneNoteHierarchy.Current}}" />
  </Grid>
</Window>

此 XAML 先引用 OneNoteFramework 程序集,为其添加 XML 命名空间前缀 onf。引用之后,可将 TreeView 的 DataContext 设置为 OneNoteHierarchy 类的静态 Current 属性,从而让您控制 OneNote 层次结构的根。然后使用 HierarchicalDataTemplates 实现树的各层与 OneNote 对象模型提供的相应集合之间的数据绑定(请参见图 6)。

图 6 层次结构与树视图之间的数据绑定

简单的数据访问

总而言之,OneNote 对象模型库提供了可以使用 LINQ 表达式和 WPF 数据绑定进行查询和处理的丰富对象集合,从而大大简化了对 Microsoft OneNote 笔记本中数据的访问。后续的文章将扩展这些概念,让您了解如何在 Silverlight 和 Windows Phone 应用程序中使用 OneNote 笔记本,以及访问云计算环境中的 OneNote 数据。

Andy Gray 是 Five Talent Software 的合伙人兼技术总监。该公司通过战略性技术解决方案帮助非营利性组织更高效地运营。他在 onenotedev.com 撰写有关 OneNote 开发的文章。

衷心感谢以下技术专家对本文进行了审阅:Michael GerfenJohn Guin。