Objektorientierter Zugriff auf OneNote-XML 2010/2013 mit LINQ to XML

Die OneNote API mit ihrem Namensraum Microsoft.Office.Interop.OneNote (Developer Referenz für 2013 hier) bietet zwar für den Zugriff auf Fenster oder Enumerationen ein Objektmodell, wie wir es aus anderen Office-Anwendungen gewohnt sind.
Doch dort, wo es wirklich spannend würde, hört das Modell abrupt auf und stellt lediglich simple API-Aufrufe zur Verfügung: Beim Zugriff auf Notizbücher, Abschnitte und Seiten sieht sich der geneigte .NET-Entwickler plötzlich XML-Manipulationen gegenüber, die in den meisten Beispielen (nicht nur im Dev Center) in wilde String-Bearbeitungen ausarten. Deswegen hier der Versuch eines objektorientierten Ansatzes.

Schön wäre etwas wie

	meinNotizbuch.Sections.Count();

Natürlich ist ein einzelner Blogbeitrag zu klein, um ein ganzes Objektmodell zu entwickeln, doch einen Ansatz dafür möchte ich hier anbieten.

Ganz prinzipiell und stark verkürzt sieht das Schema von OneNote XML so aus (wer das ganze XSD haben möchte – hier), wie sie uns die GetHierarchy-Methode der OneNote-Application liefert:

<?xml version="1.0"?>
<one:Notebooks xmlns:one="http://schemas.microsoft.com/office/onenote/2013/onenote">
	<one:Notebook name="" ID="" path="">
		<one:Section name="" ID="" path="">
			<one:Page  name="" ID="" />
			<one:Page  name="" ID="" />
			<!-- weitere Pages -->
		</one:Section>
		<one:Section name="" ID="" path="">
			<one:Page  name="" ID="" />
			<one:Page  name="" ID="" />
			<!-- weitere Pages -->
		</one:Section>
		<!-- weitere Sections -->
	</one:Notebook>
</one:Notebooks>

Wir brauchen also für einen objektorientierten Ansatz eine Notebook-, eine Section– und eine Page-Klasse, wo bei jeweils eine Notebook-Instanz eine Sections-Auflistung anbietet, die ihrerseits jeweils eine Pages-Auflistung enthält – man beachte die Verwendung des Plurals bei den Auflistungen:
Simples Objektmodell

Für LINQ wurde in C# der sogenannte Objektinitialisierer eingeführt (mehr dazu in der MSDN), der im Prinzip so aussieht:

Typ meineInstanz = new Typ() { 
                               Property1 = Wert, 
                               Property2 = Wert 
                             };

Dieser läßt sich ganz prima innerhalb von LINQ-Statements verwenden, um Objektmengen zu initialisieren. Im Falle unseres Objektmodells bräuchten wir also in einer aufrufenden Instanz, zum Beispiel einer OneNoteController-Klasse, eine Funktion, die uns das OneNote-XML parst und die Objektmengen aus unserem Objektmodell baut:

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

[...]

private List<Notebook> GetNotebookStructure()
{
        string strOneNoteXml;
        appOneNote.GetHierarchy(null, ont.HierarchyScope.hsPages, out strOneNoteXml);

        XDocument xdNotebooks = XDocument.Parse(strOneNoteXml);

        XNamespace one = "http://schemas.microsoft.com/office/onenote/2013/onenote";

        var qNotebooks = from nd in xdNotebooks.Root.Descendants(one + "Notebook")
                select new Notebook()
                {
                        Name = nd.Attribute("name").Value,
                        ID = nd.Attribute("ID").Value,
                        Path = nd.Attribute("path").Value,
                        Sections = (from ab in nd.Descendants(one + "Section")
                                     select new Section()
                                     {
                                          Name = ab.Attribute("name").Value,
                                          ID = ab.Attribute("ID").Value,
                                          Path = ab.Attribute("path").Value,
                                          Pages = (from pg in ab.Descendants(one + "Page")
                                                  select new Page()
                                                  {
                                                       Name = pg.Attribute("name").Value,
                                                       ID = pg.Attribute("ID").Value
                                                   }).ToList()
                                     }).ToList()
                        };
        List<Notebook> lst = qNotebooks.ToList();
        return lst;
}

Das passiert hier:
Die GetHierarchy-Methode betankt unseren strOneNoteXML-String. Damit wird ein XDocument (Namensraum System.Xml.Linq) erzeugt, das uns danach für LINQ-Abfragen zur Verfügung steht. Die Descendants([XName])-Methode liefert uns für den jeweiligen Ausgangsknoten die in der Hierarchie darunterliegenden Knoten mit dem gegebenen Namen. Da diese (siehe oben in der XML-Struktur) jeweils ihre Kinder mitbringen, lassen sich unsere Auflistungsproperties Notebook.Sections bzw. Section.Pages gleich innnerhalb dieses LINQ-Statements ebenfalls aus Queries befüllen.

Eine Rohform der bereits erwähnten OneNoteController-Klasse könnte also so aussehen:

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

namespace ont2013OM
{
    public class OneNoteController
    {
        ont.Application appOneNote = null;

        public OneNoteController(ont.Application oneNoteApp)
        {
            appOneNote = oneNoteApp;
        }

        private List<Notebook> GetNotebookStructure() [...]
        
        public List<Notebook> Notebooks
        {
            get
            {
                return GetNotebookStructure();
            }
            private set { }
        }
    }
}

Wird diese Klasse beim Start eines OneNote-AddIns instanziiert, steht dem engagierten OneNote-Entwickler danach ein – zugegebenermaßen noch sehr rudimentäres – Objektmodell für OneNote-Elemente zur Verfügung, das solchen Code erlaubt:

    OneNoteController meinController = new OneNoteController(OneNote.Application);

    Notebook nbMeineNotizen = meinController.Notebooks
                                   .Where(nb => nb.Name == "Meine Notizen")
                                   .FirstOrDefault();

    int anzahlAbschnitte = nbMeineNotizen.Sections.Count();

Die bisherigen Erfahrungen mit diesem Ansatz zeigen gute Performanz, auch bei größeren Notebook-Sammlungen.
Analog zum XmlDocument aus System.Xml stellt das hier verwendete XDocument aus System.Xml.Linq auch Methoden zur Manipulation der XML-Struktur bereit, wodurch Änderungen an der Notebook-Struktur von OneNote keine Unmöglichkeit mehr sind. Die XDocument.ToString()-Methode gibt nach Abschluss aller Manipulationen den fertigen XML-Code des XDocuments zurück und kann daher ganz hervorragend in den Application.UpdateHierarchy()– bzw. Application.UpdatePageContent()-Methoden verwendet werden, um den Inhalt von OneNote anzupassen.

OneNote 2013 Desktop: AddIn Vorlage für Visual Studio

Visual Studio liefert kein fertiges Template für OneNote-AddIns. Das OneCode Team stellt im Office Developer Center jedoch eine Alternative bereit – Download hier. Obwohl für OneNote 2010 entwickelt, ist dieser Code auch für OneNote 2013 verwendbar!

Wer einen grafischen Designer fürs Ribbon erwartet, wird zwar enttäuscht, eine funktionsfähige Ribbon-XML-Datei liegt jedoch in den Ressourcen des Projekts und steht sofort zur Verfügung, da das OneCode Team bereits die Implementierung des IRibbonExtensibility-Interfaces umgesetzt hat – sogar das Fensterhandling in COM32-Umgebungen ist bereits implementiert!

Leider liegt die angebotene Projektmappe im Format für Visual Studio 2010 vor, und das OneCode Team hat, wohl im Bemühen, uns verzweifelten Entwicklern das Leben so einfach wie möglich zu machen, gleich auch ein fertiges Setupprojekt mit hineingebaut. Visual Studio 2013 unterstützt diesen Projekttyp nicht mehr, weswegen es beim Konvertieren jede Menge Zicken macht. Ein getrenntes Öffnen des eigentlichen AddIn-Projekts (Ordner „CSOneNoteRibbonAddIn“) in Visual Studio 2013 ist jedoch nahezu problemlos möglich, ein Installshield-Projekt kann der geneigte Entwickler dann später immer noch hinzufügen.

Zur Installation auf dem Entwicklerrechner müssen einige Registry-Keys für das AddIn angepasst/hinzugefügt werden:

reg32.onenote

Die GUID der Assembly (findet sich in der AssemblyInfo.cs bzw. den Projekteigenschaften) muß an den entsprechenden Stellen eingetragen werden (Suchen & Ersetzen leistet hier gute Dienste), desweiteren der Einstiegspunkt des AddIns sowie Name und Beschreibung – letztere erscheinen im AddIn-Manager von OneNote, werden also vom Benutzer wahrgenommen!

Die echte Programmierung des AddIns mit C# beginnt in der beiliegenden Connect.cs, wo sich neben den AddIn-Ereignissen auch die Formularsteuerung findet. Hier können saubere Objektmodelle für die eigentliche AddIn-Idee angeflanscht werden.