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.