Send to OneNote from Windows Explorer – Sample App

It has been awhile since I have blogged about the OneNote API so I thought I would post a nice long article showing a sample app I made awhile ago.

Goal: Write an app that allows you to send a file from the Windows shell directly to OneNote, and it looks like this:

High-level: We will write an app that takes command line args that are paths to files. We will then create XML for a page in OneNote which contain embedded files pointing to these source files. Import this all into OneNote and success.

Requirements: Visual Studio 2005 & OneNote 2007 (this XML works with the RTM version)

Steps:

  1. Create a new Windows application. Even though my app is 100% console based and doesn't need the Windows UI it is just good to create this so that you don't see the app's window on the Taskbar.

  2. Add a reference to OneNote's COM object model. In the Solution Explorer find References, right-click on it and choose Add Reference. Go to the COM tab and hit "m" and look for Microsoft OneNote 12.0 Object Library, select it and choose OK.

  3. Add this line for your using statements:

    using OneNote = Microsoft.Office.Interop.OneNote;

  4. For this sample I will create a new class to do all of the work required for this, and I will call it Send2OneNote. Send2OneNote will contain a reference to OneNote and it will also have a importFiles method that will create XML and import it into OneNote's Unfiled Notes section.

  5. This is the class' constructor and destructor:

    class
    Send2OneNote

    {

    private OneNote.Application onApp;

    public Send2OneNote()

    {

    try

    {

    onApp = new Microsoft.Office.Interop.OneNote.Application();

    }

    catch(Exception e)

    {

    MessageBox.Show("Please install Micorosft Office OneNote 2007");

    onApp = null;

    Environment.Exit(-99);

    }

    }

       
     

    ~Send2OneNote()

    {

    onApp = null;

    }

    As you can see when you first instantiate the class it will get a COM reference to OneNote, if OneNote isn't installed then we will throw a dialog box @ the user and we exit. Don't ask me why I use exit error code -99, heck I imagine that most of you will say that you don't even need an error exit code, but whatever : )

  6. Now we will want to start writing importFiles, but we need to think about what it needs. I mentioned earlier that we will be add a new page to the unfiled notes section. And if you are familiar with the OneNote API you can see from the CreateNewPage() call that we need an ID to pass in, so I will write a small helper function called findUnfiledID() which returns the ID to the Unfiled Notes section; here it is:

    private
    string findUnfiledID()

    {

    string unfiledID = "";

       
     

    try

    {

    string unfiledPath;

    onApp.GetSpecialLocation(Microsoft.Office.Interop.OneNote.SpecialLocation.slUnfiledNotesSection, out unfiledPath);

    onApp.OpenHierarchy(unfiledPath, null, out unfiledID, Microsoft.Office.Interop.OneNote.CreateFileType.cftNone);

    }

    catch (Exception e)

    {

    MessageBox.Show("Couldn't get your Unfiled Notes Location");

    onApp = null;

    Environment.Exit(-99);

    }

       
     

    return unfiledID;

    }

    What does this do? I call OneNote and I ask it for the path to the Unfiled Notes section (that is the GetSpecialLocation call) and then I ask OneNote to open that path and give me back the ID. You might ask why would I do this if OneNote already has the Unfiled Notes open, well this is an easy way for me to get the ID instead of needing to parse through XML or anything else. Anytime you ask OneNote to open something (with OpenHierarchy) then OneNote will return the ID back to you.

  7. Okay so now we have the Unfiled Notes' ID and now we just need to create the XML and insert it into OneNote. Let's look @ a page that contains an embedded file, just so we can get an idea of what we will need to create ourselves:

    <?xml
    version="1.0"?>

    <one:Page
    xmlns:one="https://schemas.microsoft.com/office/onenote/2007/onenote"
    ID="{9DF3CC2B-E016-4F0F-AB50-E9B62532839A}{1}{B0}"
    name="Sent Files">

    <one:Title>

    <one:OE
    author="Daniel Escapa"
    lastModifiedBy="Daniel Escapa"
    creationTime="2006-10-04T02:13:43.000Z"
    lastModifiedTime="2006-10-04T02:13:44.000Z"
    objectID="{7F6FE609-34C0-4D0B-9A39-AB52DADC2BC2}{31}{B0}"
    alignment="left">

    <one:T><![CDATA[Sent Files]]></one:T>

    </one:OE>

    </one:Title>

    <one:InsertedFile
    pathCache="D:\Documents and Settings\descapa\Local Settings\Application Data\Microsoft\OneNote\12.0\OneNoteOfflineCache_Files\Sample file.one"
    pathSource="D:\Documents and Settings\descapa\Desktop\Sample file.one"
    preferredName="Sample file.one"
    lastModifiedTime="2006-10-04T02:13:44.000Z"
    objectID="{7F6FE609-34C0-4D0B-9A39-AB52DADC2BC2}{36}{B0}">

    <one:Position
    x="36.0"
    y="86.4000015258789"
    z="0" />

    <one:Size
    width="54.0"
    height="63.0" />

    </one:InsertedFile>

    </one:Page>

    I did take out some of the XML but you can see the basics of the page here. I have a title which says "Sent Files" and then there is an embedded file on the page which is for the file "Sample file.one". You can see that we have a pathSource and a pathCache attributes on the InsertedFile element. The IDs & objectIDs will be filled in by OneNote once the XML has been imported and there are other special items like exact positioning that we can ignore when we import content. If you really wanted to place this in a certain location we could do so, but let's just deal with the basics now.

    You can easily embed a file in OneNote because you just need to specify the path via the XML and then OneNote will do the rest. You don't need to actually break down the file and put it on the page, just give it a path. OneNote will then take this, insert it into the file, and put a cached copy in the OneNote cache.

    I think we can go and write insertFiles().

  8. I will just paste the code for insertFiles and then we can discuss it and why I did it the way that I did:

    public
    void importFiles(string[] args)

    {

    string unfiledID = findUnfiledID();

    string importedPageID = "";

       
     

    try

    {

    onApp.CreateNewPage(unfiledID, out importedPageID, Microsoft.Office.Interop.OneNote.NewPageStyle.npsDefault);

    }

    catch (Exception e)

    {

    MessageBox.Show("Could not create a new page! Error code: " + e.Message);

    onApp = null;

    Environment.Exit(-99);

    }

       
     

    string xml2importBase = "<?xml version=\"1.0\"?>\n<one:Section xmlns:one=\"https://schemas.microsoft.com/office/onenote/2007/onenote\" ID=\"";

    xml2importBase += unfiledID + "\">\n";

    xml2importBase += "<one:Page ID=\"" + importedPageID + "\">\n";

    xml2importBase += "<one:Title>\n<one:OE>\n<one:T><![CDATA[Sent Files]]></one:T>\n</one:OE>\n</one:Title>\n";

    string tailXml = "</one:Page>\n</one:Section>";

       
     

    bool succeeded = false;

       
     

    foreach (string s in args)

    {

    string xml2import = xml2importBase + "<one:InsertedFile pathSource=\"" + s + "\"></one:InsertedFile>\n";

    xml2import += tailXml;

    try

    {

    onApp.UpdateHierarchy(xml2import);

    succeeded = true;

    }

    catch(Exception e)

    {

    if (args.Length == 1)

    {

    try

    {

    onApp.DeleteHierarchy(importedPageID, DateTime.MinValue);

    }

    catch (Exception e1)

    {

    MessageBox.Show("Could not delete page and error on insert, bailing! Error: " + e1.Message);

    onApp = null;

    Environment.Exit(-99);

    }

    MessageBox.Show("You cannot import a folder into OneNote!");

    onApp = null;

    Environment.Exit(-99);

    }

    else

    {

    //supressed, we just hit a folder but we will jsut keep on going...

    //MessageBox.Show("Couldn't import " + s + " into OneNote. Error code: " + e.Message);

    }

    }

    }

       
     

    if (!succeeded)

    {

    try

    {

    onApp.DeleteHierarchy(importedPageID, DateTime.MinValue);

    }

    catch (Exception e)

    {

    MessageBox.Show("Failed to delete the page with nothing on it. Error: " + e.Message);

    //add code to remove this stuff!

    }

    MessageBox.Show("You cannot send folders to OneNote!");

    }

    else

    {

    onApp.NavigateTo(importedPageID, null, false);

    }

     }

*wipes brow* Okay lots going on here so I will take look @ each part.

  1. Creating a new page in the Unfiled Notes section:

    public
    void importFiles(string[] args)

    {

    string unfiledID = findUnfiledID();

    string importedPageID = "";

       
     

    try

    {

    onApp.CreateNewPage(unfiledID, out importedPageID, Microsoft.Office.Interop.OneNote.NewPageStyle.npsDefault);

    }

    catch (Exception e)

    {

    MessageBox.Show("Could not create a new page! Error code: " + e.Message);

    onApp = null;

    Environment.Exit(-99);

    }

    This code will create a new page, with CreateNewPage, in the section that I supply with unfiledID and then it will output the new pageID. If it fails then we capture that error code but in most cases this should never fail, but you _just_ never know.

  2. Now I have the main XML that I am importing into OneNote:

    string xml2importBase = "<?xml version=\"1.0\"?>\n<one:Section xmlns:one=\"https://schemas.microsoft.com/office/onenote/2007/onenote\" ID=\"";

    xml2importBase += unfiledID + "\">\n";

    xml2importBase += "<one:Page ID=\"" + importedPageID + "\">\n";

    xml2importBase += "<one:Title>\n<one:OE>\n<one:T><![CDATA[Sent Files]]></one:T>\n</one:OE>\n</one:Title>\n";

    string tailXml = "</one:Page>\n</one:Section>";

  3. The next part is the main part of code which inserts this into OneNote:

    bool succeeded = false;

       
     

    foreach (string s in args)

    {

    string xml2import = xml2importBase + "<one:InsertedFile pathSource=\"" + s + "\"></one:InsertedFile>\n";

    xml2import += tailXml;

    try

    {

    onApp.UpdateHierarchy(xml2import);

    succeeded = true;

    }

    catch(Exception e)

    {

    if (args.Length == 1)

    {

    try

    {

    onApp.DeleteHierarchy(importedPageID, DateTime.MinValue);

    }

    catch (Exception e1)

    {

    MessageBox.Show("Could not delete page and error on insert, bailing! Error: " + e1.Message);

    onApp = null;

    Environment.Exit(-99);

    }

    MessageBox.Show("You cannot import a folder into OneNote!");

    onApp = null;

    Environment.Exit(-99);

    }

    else

    {

    //supressed, we just hit a folder but we will jsut keep on going...

    //MessageBox.Show("Couldn't import " + s + " into OneNote. Error code: " + e.Message);

    }

    }

    }

    This might seem like a lot of code but what I am doing is going through each string in the arguments (that came from Windows Explorer when I chose to send to OneNote).

    There is a lot more going on with this code for example I put all of the XML together and you can see to embed a file you only need to provide the pathSource. I then send this to OneNote with the UpdateHierarchy method, you might ask why I do this for each individual file instead of creating the XML for all of the files and import that together? I did this because I wanted my imports to be more transactional; if something failed to import I wanted to know more about it and have something atomic that I could rollback if need be. It also made my code pretty clean and easy (imho).

    If we failed on the import that means that we tried to insert something which cannot be embedded which was probably a folder. If the user only tried to import the folder this is invalid and we throw an error to the user. I also go ahead and delete the page that I just created, otherwise the user would have a blank page in their Unfiled Notes.

    However if we were successful I go through and do this for each file that was passed to our app.

  4. The final part of this code is error handling and last steps. If we were successful importing the files we will navigate the user to the newly created page, otherwise we will delete the new blank page because the user tried to send only folders which are invalid. See here:

    if (!succeeded)

    {

    try

    {

    onApp.DeleteHierarchy(importedPageID, DateTime.MinValue);

    }

    catch (Exception e)

    {

    MessageBox.Show("Failed to delete the page with nothing on it. Error: " + e.Message);

    //add code to remove this stuff!

    }

    MessageBox.Show("You cannot send folders to OneNote!");

    }

    else

    {

    onApp.NavigateTo(importedPageID, null, false);

    }

   
 

The last steps would be to test this out and put a shortcut in your SendTo folder under your user profile. At some point I hope to package this all up and make it a download that anyone can easily use. I guess I will have to wait until RTM when we don't have any more changes in the code.

As always I appreciate your feedback and thoughts on this, hope you learned something.

Update: I am including the source as well, you can find it here: SendtoOneNote.zip