Developing OneNote 2010 Addins
Developing OneNote 2010 Addins
www.malteahrens.com/blog/howto-onenote-dev/ As with other Microsoft Office applications, you can also create add-ins for OneNote. In OneNote 2007 you wrote add-ins by implementing IOneNoteAddin (see Daniel Escapas tutorial blogs.msdn.com/b/descapa/ ) and they integrated well with OneNotes UI, being toolbar based add-ins in a toolbar based UI. With OneNote 2010s ribbon, however, these toolbar based add-ins only show up on a separate Add-Ins tab, and even then only appearing as small icons. Integrating these add-ins with the ribbon / fluent UI is what we want for OneNote 2010, to properly integrate them with the UI. Like this (a custom button in a custom tab in the ribbon):
This has been tested with Visual Studio 2008 Professional and Visual Studio 2010 Professional.
Download the Visual Studio 2008 source code Download the Visual Studio 2010 source code
This guide is broken down into five parts: Part 1 Creating the project Part 2 Creating the ribbon Part 3 Writing some code Part 4 Fixing the Installer Part 5 Tips & Good Sites
For this tutorial we will choose .NET 3.5 and C#, but you can choose what you want
Page |2
3. First up we need to make the application COM-Visible a. Right-Click on the Project Properties
Page |3
c. Select Make assembly COM-Visible. This makes the class library / .dll generated by the project visible to OneNote.
d. Click OK e. Build tab Register for COM Interop. This gets Visual Studio to automatically register the application for COM Interop when its built
Page |4
4. To make debugging and building the project easier, we will change the AssemblyVersion attribute to automatically increment. This way, we wont have to always update this attribute each time we want to update your application a. Open: Properties AssemblyInfo.cs
to:
[assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("1.0.*")]
Page |5
2. To make this XML file readable by our add-in during runtime we will make it a resource (so that it is embedded in the .dll created). a. Right Click on the Project Properties Resources Click here to create one
Page |6
3. MSDN has a great introduction to XML and the Ribbon. See: msdn.microsoft.com Basically, we need a Tab (our own or an already existing one) to place our Group a collection of controls filled with our controls. 4. The XML document to customize the ribbon starts with:
<?xml version="1.0" encoding="utf-8" ?> <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui"> <ribbon> <!-- Our content goes here --> </ribbon> </customUI>
5. In order to display images in the ribbon, they need to be parsed in a special way. We will do this later on in this guide, but we need to set-up the function so that we can do this. Just add the following attribute to the <CustomUI> element:
loadImage="GetImage"
Page |7
6. In the ribbon, the first thing that we need is a tab. Either we can hook into an existing tab or we can create our own. For this demo, we will create our own tab. If you want to extend an already existent tab, check out the Control IDs (www.microsoft.com/downloads). Just replace id= with the idMso=[the control id] (without square brackets). All that a tab needs is an id and a label:
<tab id="tabCustom" label="Custom"> </tab>
7. Next, we need a group in which to place our controls. Again, this can be our own group or an already existing one. For this demo, we will create our own Group. Again, all it needs is an id and a label:
<group id="groupHello" label="Hello"> </group>
8. In this group we place our controls for example Buttons, Menus, Checkboxes, etc. For this demo, we will just add a simple button. Again it needs an id and a label, but we will also change (the next ones are optional) its size to large (for a big button), and add a tooltip. Also, we need to give it the path to the image and we need to give it an onAction function (basically the OnClick() event):
<button id="buttonHello" label="Hello World!" size="large" screentip="Press this for a 'Hello World' message" onAction="showHello" image="HelloWorld.png"/>
Page |8
Page |9
to the following: Tab Name .NET Extensibility (note the capital E) (for the COM Add-In code) .NET System.Windows.Forms (for a MessageBox) .NET System.Drawing (for putting Images into the ribbon) COM Microsoft OneNote 14.0 Object1 Library (for the OneNote API) COM Microsoft Office 14.0 Object Library (for Ribbon Extensibility If you are using .NET 4 / VS 2010 be sure to turn off Embed Interop Types for Microsoft OneNote 14.0 Object Library. See Daniel Escapas post on how and why: blogs.msdn.com/b/descapa/ 3. Add a using to the top of Class1.cs, among the other usings:
using System.Runtime.InteropServices;
For Visual Studio 2008, it appears as Microsoft OneNote 14.0 Type Library
P a g e | 10
4. Next, the Add-In requires a GUID to identify it. To create one: a. Tools Create GUID (if it isnt there, do a search in the start-menu) b. Choose Registry Format Copy Exit
5. Just above:
public class Class1
add:
[GuidAttribute("[Your apps Guid]"),ProgId("[Your apps ProgId]")]
[Your apps Guid] = <paste> (without curly braces) the one created in Step 42 [Your apps ProgId] = Project.ClassName3 Ensure that you have these values on hand (i.e. copied into Notepad, or even better OneNote) as they are needed for the setup project (later).
2 3
P a g e | 11
7. This will allow us to implement IDTExtensibility2 (it contains all the COM Add-in functions), so that our code now looks like this:
public class Class1 : IDTExtensibility2 { }
8. Right-Click on IDTExtensibility2 (in the code (above) that we just added) Implement Interface Implement Interface. This will generate all the method stubs that the Add-In encounters during runtime
9. So now there are a whole heap of stub methods with the code:
throw new NotImplementedException();
This needs to be removed; otherwise, when these methods are encountered during runtime, the application will exist because of these exceptions being thrown. So delete them, that only the methods remain:
public void OnAddInsUpdate(ref Array custom) { } public void OnBeginShutdown(ref Array custom) { } public void OnConnection(object Application, ext_ConnectMode ConnectMode, object AddInInst, ref Array custom) { } public void OnDisconnection(ext_DisconnectMode RemoveMode, ref Array custom) { } public void OnStartupComplete(ref Array custom) { }
P a g e | 12
10. Add a using to the top of Class1.cs, among the other usings:
using Microsoft.Office.Interop.OneNote;
11. The OnConnection() function is called when the Add-In is being loaded. Here OneNote passes in the current instance of the ApplicationClass, the programmatic interface to OneNotes API, so we set a new class-wide variable, onApp, to equal it.
ApplicationClass onApp = new ApplicationClass(); public void OnConnection(object Application, ext_ConnectMode ConnectMode, object AddInInst, ref Array custom) { onApp = (ApplicationClass)Application; }
13. This will allow us to implement IRibbonExtensibility (it contains all the Ribbon functions), so that our code now looks like this:
public class Class1 : IDTExtensibility2, IRibbonExtensibility { /* Code here... */ }
P a g e | 13
14. Right-Click on IRibbonExtensibility (in the code (above) that we just added) Implement Interface Implement Interface. This will create a stub of the Ribbons load event for us.
With returning the XML Resource (the ribbon.xml file) that we created in Step 2
public string GetCustomUI(string RibbonID) { return Properties.Resources.ribbon; }
16. Now to make the button work we create a function with the same name that we mentioned in the ribbon.xml for our buttons onAction event. It is important that this function is both public and takes an IRibbonControl as a parameter:
public void showHello(IRibbonControl control) { }
P a g e | 14
17. This is the part where you can do what you want with your own code. This function will be called when the (in our case) button is pressed. For this demo, we will just make it show a Messagebox with the current pages ID. a. First add:
using System.Windows.Forms;
b. To get the ID of the current page, we call the ApplicationClass we created earlier:
string id = onApp.Windows.CurrentWindow.CurrentPageId;
18. To get an image to load on the ribbon, e.g. as the image of a button, we need to pass the image as an IStream a. First, we need to add the image to our project (similarly to how we added ribbon.xml) i. ii. Go to Resources (Right-Click on the Project Properties Resources) Click on the down-arrow next to Add Resource, click Add existing file, and select the file (HelloWorld.png)
iii.
In the Solution Explorer Resources right-click the inserted image Properties change its Build Action to Content
b. We start off by creating a new class (Right-Click on the Project Add New Class). Call it CCOMStreamWrapper.cs c. Paste in the following code written by Nani (Microsoft) (relevant blog link: blogs.msdn.com/b/johnguin/ ) that converts the image to an IStream
Comments have been removed to fit this onto two pages. Original: CCOMStreamWrapper.cs
P a g e | 15
/* * Code from: * TOC Power Toy 2010 by Nani (Microsoft) * See: http://blogs.msdn.com/b/johnguin/archive/2010/10/25/code-for-thetable-sorting-powertoy-for-onenote-2010.aspx * */ using using using using using System; System.Collections.Generic; System.Text; System.Runtime.InteropServices; System.Runtime.InteropServices.ComTypes;
namespace HelloWorld { class CCOMStreamWrapper : IStream { public CCOMStreamWrapper(System.IO.Stream streamWrap) { m_stream = streamWrap; } public void Clone(out IStream ppstm) { ppstm = new CCOMStreamWrapper(m_stream); } public void Commit(int grfCommitFlags) { m_stream.Flush(); } public void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten) { } public void LockRegion(long libOffset, long cb, int dwLockType) { throw new System.NotImplementedException(); } public void Read(byte[] pv, int cb, IntPtr pcbRead) { Marshal.WriteInt64(pcbRead, m_stream.Read(pv, 0, cb)); } public void Revert() { throw new System.NotImplementedException(); } public void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition) { long posMoveTo = 0; Marshal.WriteInt64(plibNewPosition, m_stream.Position); switch (dwOrigin) { case 0: { /* STREAM_SEEK_SET */
P a g e | 16
posMoveTo = dlibMove; } break; case 1: { /* STREAM_SEEK_CUR */ posMoveTo = m_stream.Position + dlibMove; } break; case 2: { /* STREAM_SEEK_END */ posMoveTo = m_stream.Length + dlibMove; } break; default: return; } if (posMoveTo >= 0 && posMoveTo < m_stream.Length) { m_stream.Position = posMoveTo; Marshal.WriteInt64(plibNewPosition, m_stream.Position); } } public void SetSize(long libNewSize) { m_stream.SetLength(libNewSize); } public void Stat(out System.Runtime.InteropServices.ComTypes.STATSTG pstatstg, int grfStatFlag) { pstatstg = new System.Runtime.InteropServices.ComTypes.STATSTG(); pstatstg.cbSize = m_stream.Length; if ((grfStatFlag & 0x0001/* STATFLAG_NONAME */) != 0) return; pstatstg.pwcsName = m_stream.ToString(); } public void UnlockRegion(long libOffset, long cb, int dwLockType) { throw new System.NotImplementedException(); } public void Write(byte[] pv, int cb, IntPtr pcbWritten) { Marshal.WriteInt64(pcbWritten, 0); m_stream.Write(pv, 0, cb); Marshal.WriteInt64(pcbWritten, cb); } private System.IO.Stream m_stream; } }
Note: You may have to change the namespace HelloWorld to your own namespace
P a g e | 17
d. Going back to Class1.cs, next we need to use the function, GetImages(), that we referenced in the ribbon.xml earlier i. The function called, GetImage() needs an IStream of the image passed back. It provides one variable, the image name, that is the string specified in ribbon.xml each time in the image attribute (for example in a button). So first we add (for the IStream and ImageFormat):
using System.Runtime.InteropServices.ComTypes; using System.Drawing.Imaging;
ii.
iii.
So we start off with an empty function (making sure that its name is the same as the one specified in ribbon.xml load image attribute):
public IStream GetImage(string imageName) { }
iv.
Next we need to create a memory stream, save the image to it (with the correct ImageFormat), and return it (after it has been through the CCOMStreamWrapper function):
public IStream GetImage(string imageName) { MemoryStream mem = new MemoryStream(); Properties.Resources.HelloWorld.Save(mem, ImageFormat.Png); return new CCOMStreamWrapper(mem); }
where HelloWorld is the name of the image that you imported 13. When the application is closing, we want to ensure that we clear up the memory used. To do this, we: a. Collect the Garbage with .NETs GC (GarbageCollector) in the OnDisconnection void
public void OnDisconnection(ext_DisconnectMode disconnectMode, ref System.Array custom) { onApp = null; GC.Collect(); GC.WaitForPendingFinalizers(); }
P a g e | 18
b. Make our ApplicationClass variable (onApp) equal to null in the OnBeginShutdown void
public void OnBeginShutdown(ref System.Array custom) { if (onApp != null) onApp = null; }
Now Class1.cs should look like (with a different GUID & ProgId): (Original: class1.cs)
using using using using using using using using using using using using System; System.Collections.Generic; System.Linq; System.Text; Extensibility; System.Runtime.InteropServices; Microsoft.Office.Interop.OneNote; Microsoft.Office.Core; System.Windows.Forms; System.Runtime.InteropServices.ComTypes; System.IO; System.Drawing.Imaging;
namespace HelloWorld { [GuidAttribute("61139959-A5E4-4261-977A-6262429033EB"), ProgId("HelloWorld.Class1")] public class Class1 : IDTExtensibility2, IRibbonExtensibility { #region IDTExtensibility2 Members ApplicationClass onApp = new ApplicationClass(); public void OnConnection(object Application, ext_ConnectMode ConnectMode, object AddInInst, ref Array custom) { onApp = (ApplicationClass)Application; } public void OnDisconnection(Extensibility.ext_DisconnectMode disconnectMode, ref System.Array custom) { onApp = null; GC.Collect(); GC.WaitForPendingFinalizers(); } public void OnBeginShutdown(ref System.Array custom) { if (onApp != null) onApp = null; } public void OnStartupComplete(ref Array custom) { } public void OnAddInsUpdate(ref Array custom) { }
P a g e | 19
#endregion #region IRibbonExtensibility Members public string GetCustomUI(string RibbonID) { return Properties.Resources.ribbon; } public void showHello(IRibbonControl control) { string id = onApp.Windows.CurrentWindow.CurrentPageId; MessageBox.Show("Current Page ID = " + id, "Hello World!"); } public IStream GetImage(string imageName) { MemoryStream mem = new MemoryStream(); Properties.Resources.HelloWorld.Save(mem, ImageFormat.Png); return new CCOMStreamWrapper(mem); } #endregion } }
P a g e | 20
3. Remove all the keys, so that only the default hives are left
P a g e | 21
5. Create the following keys (Right-Click on a [Key / Hive] Key) with their respective values (Right-Click New <Type>). Be sure to check their spelling a common source of frustration when the add-in doesnt load in OneNote.
a. HKEY_CLASSES_ROOT\AppID\[Your app's GUID]
Type string
Type string
Name AppID
Type string
Type string
Name AppID
Type string
f.
HKEY_LOCAL_MACHINE\Software\Classes\CLSID\[Your app's GUID] Type Name Value string AppID [Your app's GUID]
g. HKEY_LOCAL_MACHINE\Software\Microsoft\Office\OneNote\AddIns
\[Your app's ProgID] Type Name string Description string FriendlyName DWORD LoadBehavior
LoadBehavior should be "9" (load the add-in when the tab loads) for editing existing tabs or "3" (load the add-in when the application loads) for newly created tabs. We are using "3" in this tutorial, because otherwise (if we used "9") our tab would only load when the tab containing it would load, but because a tab is not contained in a tab, it would never load. For more information, see: msdn.microsoft.com
P a g e | 22
If you dont want to add all the keys to Visual Studios Registry Editor manually:
a. Open notepad and paste in the following code (regkeys.reg):
Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\AppID\[Your apps GUID]] "DllSurrogate"="" [HKEY_CLASSES_ROOT\CLSID\[Your apps GUID]] "AppID"="[Your apps GUID] [HKEY_CURRENT_USER\Software\Classes\AppID\[Your apps GUID]] "DllSurrogate"="" [HKEY_CURRENT_USER\Software\Classes\CLSID\[Your apps GUID]] "AppID"="[Your apps GUID] [HKEY_LOCAL_MACHINE\Software\Classes\AppID\[Your apps GUID]] "DllSurrogate"="" [HKEY_LOCAL_MACHINE\Software\Classes\CLSID\[Your apps GUID]] "AppID"="[Your apps GUID] [HKEY_LOCAL_MACHINE\Software\Microsoft\Office\OneNote\AddIns\[ Your apps ProgId]] "Description"="Press for a Hello World!" "FriendlyName"="Hello World!" "LoadBehavior"=dword:00000003
b. Save the file as <filename>.reg (e.g. regkeys.reg) c. <Right-Click> on Registry on Target Machine Import
d. Find the save *.reg file and open it e. Visual Studio will insert the keys and values for you
Be sure to check the imported values, sometimes (especially with the DWORD) insert doesnt quite work)
P a g e | 23
6. Now it should look like this (your GUID will be different, and your ProgID will probably be different to the one displayed here):
P a g e | 24
8. Second last, we need to add the projects output (the dll created) to the install location: Click on the Application Folder (generally the \Program Files\ directory) Right-Click Add Project Output OK
9. Running 64-Bit Office? Be sure to also change the TargetPlatform to x64 in the Installer Properties (Click on the setup project F4) 10. Last, change the installer setting (Click on the setup project F4) RemovePreviousVersions to True. You may also want to change some of the other settings, especially Product Name and Manufacturer, as the default install location is: [ProgramFilesFolder]\[Manufacturer]\[ProductName]
Done!
So, now we are done with process in Visual Studio, so we just build the solution, install the setup project that it creates, and you should see your add-in in OneNote!
P a g e | 25
P a g e | 26
6. In Properties, go to the Build tab. Change the Output path (under Output) to the install path of the application (generally C:\Program Files\<Manufacturer>\<ProductName>\) 7. Now simply build (Build Build Solution) the project as normal, open OneNote again, and your addin should have updated.
P a g e | 27
Note: 1. You may need to close dllhost.exe and OneNote before Visual Studio will allow you to build the project (files may be in use that would otherwise be overwritten) 2. If your addin has not updated in OneNote, especially if the image in the ribbon has not, reload the addin in OneNote. To do this: a. File Options Add-Ins (Manage: COM Add-Ins) Go
b. Deselect your add-in and click OK (and OK in the OneNote Options window)
c. Repeat the process (starting at a) again, but this time selecting your Add-In again d. This generally works, as it gets OneNote to completely reload the addin, thus (hopefully) loading the newer image for your add-in in the ribbon
P a g e | 28
Useful links
Debugging a OneNote Add-in with Visual Studio (for OneNote 2007 add-ins, but its still relevant): http://blogs.msdn.com/b/descapa/archive/2007/05/01/debugging-aonenote-toolbar-addin-c.aspx Debugging OneNote API access through the log: http://blogs.msdn.com/b/descapa/archive/2006/12/08/debugging-theonenote-api-enable-logging.aspx OneNote Error Codes / COM Exceptions: http://msdn.microsoft.com/en-us/library/ff966472.aspx Customizing the Ribbon / Fluent UI (for Office 2007, but its still relevant): http://msdn.microsoft.com/en-us/library/aa338202(office.12).aspx Getting the Control IDs for Tabs, Groups, and Buttons in OneNotes ribbon (useful for e.g. adding a button on the Home tab, rather than a new tab): http://www.microsoft.com/downloads/en/details.aspx?FamilyID=3f2fe784610e-4bf1-8143-41e481993ac6&displaylang=en Editing content in OneNote through OneNotes API: http://msdn.microsoft.com/en-us/magazine/ff796230.aspx OMSpy taking a look at the XML structure of pages, sections (and section groups), and notebooks in OneNote: http://blogs.msdn.com/b/descapa/archive/2007/02/12/omspy-a-onenotedeveloper-s-tool.aspx Office Schema Reference (useful for trying to understand OneNotes XML elements and attributes. Its the 2007 one, but again its still relevant (and it provides it in a HTML format and *.xsd, whereas the 2010 one only has the *.xsd)): http://www.microsoft.com/downloads/en/details.aspx?FamilyId=15805380F2C0-4B80-9AD1-2CB0C300AEF9&displaylang=en Source Code from the Table Sorting Powertoy (by Nani Microsoft): http://blogs.msdn.com/b/johnguin/archive/2010/10/25/code-for-the-tablesorting-powertoy-for-onenote-2010.aspx All the OneNote blogs (especially John Guin and Daniel Escapa)
P a g e | 29