Detecting Print in Outlook (VSTO/C#)

This is a common problem in Outlook. You might have tried to override the Ribbon settings for Print in Outlook to find that your code never gets run when the user clicks Print.

There is also not any events in the Outlook object model to detect Print either. So if you need to detect the user pressing the print button, you are out of luck.

While it is still not possible to detect the print button being pressed, you can at least detect when the user has selected the Print tab on the backstage.

The following code uses a background thread and a series of Windows API calls to FindWindow/FindWindowEx to detect when the Print tab on the backstage is opened:

public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string className, string windowTitle);
/// <summary>
/// Startup for Outlook Add-in
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ThisAddIn_Startup(object sender, System.EventArgs e)
// we start by creating a background thread and look for a specific
// set of windows to appear, then we know the user clicked print
new Thread(() =>
while (true)
/// <summary>
/// Checks to see if the user has opened backstage and
/// selected the Print tab
/// </summary>
private void CheckForPrint()
// depending on whether we have an inspector active, or the explorer
// active we will need to get the caption to FindWindow
string LstrCaption = "";
if(Application.ActiveWindow() is Outlook.Inspector)
// Active inspector caption
LstrCaption = ((Outlook.Inspector)Application.ActiveWindow()).Caption;
else if(Application.ActiveWindow() is Outlook.Explorer)
// Active explorer caption
LstrCaption = ((Outlook.Explorer)Application.ActiveWindow()).Caption;
// get the window handle
IntPtr LintHostHandle = FindWindow(null, LstrCaption);
if (LintHostHandle == IntPtr.Zero) return; // if we cannot find it – nevermind
// create a list of windows to find (in reverse order)
// 4) rctrl_renwnd32 – is the print preview window
// 3) NetUICtrlNotifySink – is whole Print options and preview
// 2) NetUIHWND – is the the entire print tab
// 1) FullpageUIHost – is the backstage page
Stack<string> LobjWindowClasses = new Stack<string> (
new string[] { "rctrl_renwnd32", "NetUICtrlNotifySink", "NetUIHWND", "FullpageUIHost" });
// recursive call back to find each window in the stack.
// if all of them are found, then present a message to the user
if(FindWindowStack(LintHostHandle, LobjWindowClasses))
MessageBox.Show("You have clicked on the Print Tab in Outlook.");
catch { }
/// <summary>
/// This function will take the window classnames in the provided stack
/// and then find each one in order via recursive calls. If all of them
/// are found – we return true = found
/// </summary>
/// <param name="PintHandle"></param>
/// <param name="PobjStack"></param>
/// <returns></returns>
private bool FindWindowStack(IntPtr PintHandle, Stack<string> PobjStack)
// get the window with the classname being popped off the stack
IntPtr LintNewHandle = FindWindowEx(PintHandle, IntPtr.Zero, PobjStack.Pop(), "");
if(LintNewHandle != IntPtr.Zero && PobjStack.Count == 0)
return true; // found it
else if(LintNewHandle!= IntPtr.Zero)
// found a window, but the stack still has items, call next one
return FindWindowStack(LintNewHandle, PobjStack);
// did not find it
return false;
// oops
return false;

easyEws v1.0.16 Published

I have made a few minor updates to easyEws. First, was a fix for distribution lists with an “&” in the name. The second is a few minor JSDoc updates for better linting. And finally, I changed a couple forEach loops to traditional for loops for performance reasons.

I have published the update to NPM and to my GitHub.

Please let me know if you have any issues or questions.

Same Add-in Running Everywhere

If you have followed my blog or the Microsoft Office Developer Blogs enough, you have heard over an over again how Office Web Add-Ins are truly cross-platform.

In working on a proof-of-concept I created a simplified Word Redactions add-in that is truly cross-platform. I have shared the entire project on my GitHub repository, so you can see how it works.

Here are the screenshots to prove it runs on every platform supported:

  • Windows
  • Mac
  • Web Browser
  • iPad

Migrating a VS2019 Office Web Add-in to VS Code

I find myself frequently converting Office Web Add-in projects I originally created in VS2019/2017/2015 over to VS Code. This is because VS Code has become my preferred development platform for everything Web based.

There is a trick to this and a few things you need to know:

  1. Your project in VS2019 (et. al.) has a completely different structure, so once you move things around, you need to change some references.
  2. If you use the Yeoman generator to create the shell of your new project, you need to know a little about webpack.

So, in the following sections we will cover what you need to do:

  • Copy over all the files
  • Update the references
  • Add web pack commands

Copy Files

  1. Run “yo office” and create a default taskpane add-in, with JavaScript and for Outlook. This will create your baseline project. You will open this project and delete these files:
    1. .\src\taskpanes\taskpane.html
    2. .\src\taskpanes\taskpane.js
    3. .\src\commands\commands.html
    4. .\src\commands\commands.js
    5. .\assets\*.* (delete everything in there)
  2. In your VS2019 project and then open your Manifest (highlighted in the image below). Select All and Copy everything from the manifest.
  1. Open your shell project in Visual Studio Code, open the manifest.xml and paste. Later we will update the references.
  2. Next, copy the VS2019 FunctionFile.html and FunctionFile.js files to the .\src\commands folder in VS Code, then rename both: commands.html, commands.js
  3. Next, copy your VS2019 taskpane.html and taskpane.js to the taskpanes folder in VS Code.
  4. If you are like me and also have dialogs, you can create a .\src\dialogs folder in VS Code and then copy the dialog.html and dialog.js from VS2019 to VS Code.
  5. Be sure to copy over any other supporting scripts you have. I created a folder called .\src\common in VS Code and copied those JS files from VS2019.
  6. Next, anything you have in the Content folder in VS2019 you need to copy to VS Code .\assets folder.

That is it for the copy part. Now for the manifest part.

Update the Manifest

At this point you need to update the manifest.

  1. Open the manifest.xml in VS Code and Press CTRL+H to open the Find/Replace dialog. Type in ~remoteAppUrl and replace it with https://localhost:3000. Then replace all instances.
  2. Next and this part is a bit more tricky, but you need to look at every single html pointer URL and verify that it points to the ROOT. Meaning if it was [~remoteAppUrl/Functions/FunctionFile.html] and then you did the Find/Replace, it became [https://localhost:3000/Functions/FunctionFile.html]. You need to update this to be just: https://localhost:3000/FunctionFile.html. This is because when you compile your project with Web Pack, it places everything in the root.
  3. Again, this is tricky, but every one of your base URL’s for images will need to be updated to point to the correct image in the ./assets folder.
  4. Once you have completed, you can press CTRL+~ to open the command console and type: npm run validate. This will tell you if there is anything wrong with your manifest.

Update the HTML files

Next you need to open up each of your HTML files: dialogs.html, commands.html (was called FunctionFile.html), and taskpane.html. etc. In each of these you will find a script that points to a respective JavaScript file and possibly other source files you copied over to the .\src\common folder. Remove all those script tags. Leave only the tags that point to online CDN’s. We will get to this in one of the following sections, but essentially Web Pack will put all the references in for you automatically at compile time.

Also, any images that you are referencing in your HTML with the <img> tag will need to be updated to point to the proper file in the ./assets folder.

Update the Commands.js (formally FunctionFile.js)

Now we need to get everything ready for Web Pack and that is where we start getting into some code. I will cover Web Pack in another post soon, but what it uses is a global namespace for the functions you call. So you MUST add this code to the bottom of the JS file:

function getGlobal() {
return typeof self !== "undefined"
? self
: typeof window !== "undefined"
? window
: typeof global !== "undefined"
? global
: undefined;
const g = getGlobal();
g.onSendEvent = onSendEvent;
g.onRibbonButtonClick = onRibbonButtonClick
view raw webpack _required.js hosted with ❤ by GitHub
Web Pack Required code

In your manifest you have specified the functions that will be called when the user clicks on a Ribbon button or Events that will fire such as OnSend in Outlook. Find those function and made sure they are added to the global namespace as shown.

Also, at the top of EVERY single JS file in your project you will need to add the following line:

/* global global, self, window, Office, console, myClass*/

When you start to edit these files in VS Code, there will be a whole lot of “red squigglies” for undefined items in the global space. For each of those that the syntax highlighter defines as such, you will add it to the global tag at the top of the file. Like this:

Undefined object

Update the Common JS Files

For the .\src\common files any function you want to be available in the global namespace, meaning you call it from one JS file to the next, you will want to add the same section above and make sure the function is specified.

NOTE: You will also want to do this for any global constants or vars that you want visible as well.

Update the Web Pack Config

Finally, we have to update the webpack.config.js. This is probably the most tricky part. What you need to do is locate the values for {entry} and make sure that every JS file in your project is represented:

entry: {
polyfill: "@babel/polyfill",
dialog: "./src/dialogs/dialog.js",
commands: "./src/commands/commands.js",
shared: "./src/common/shared.js",
common: "./src/common/common.js",
taskpane: "./src/taskpane/taspane.js"
view raw entry.js hosted with ❤ by GitHub
Web Pack {entry}

NOTE: This is critical to get correct and to include all JS file because if you do not do this, it will not work.

Now that you have all the entries in place you need to update the {plugins} section. You might need to create an extra module or two as well. By default you have one for taskpane and commands, but my example will show dialogs as well:

plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename: "taskpane.html",
template: "./src/dialogs/taskpane.html",
chunks: ["polyfill", "common", "shared", "taskpane"]
new HtmlWebpackPlugin({
filename: "dialog.html",
template: "./src/dialogs/dialog.html",
chunks: ["polyfill", "common", "dialog"]
new HtmlWebpackPlugin({
filename: "commands.html",
template: "./src/commands/commands.html",
chunks: ["polyfill", "shared", "commands"]
view raw plugins.js hosted with ❤ by GitHub
Web Pack {plugins}

Note how in each plug-in (the HTML file), there are “chunks.” These chunks are the references to each script tag that will be added at compile time. It is VERY important to make sure that you reference each file as expected.

Ready to Test

At this point you should be ready to test. If you manifest validated, you can now just press CTRL+~ to open the command console and type npm start.

If your solution successfully sideloads in Office, you should verify that your commands, events and taskpanes all function. If they do not here are some common issues:

  1. You did not properly reference your JS file in the webpack.config.js. Either defined as an {entry}, or as a chunk on a {plugin}.
  2. You did not add the required code at the bottom of the JS file for your functions to be defined in the global namespace, or you forgot to add one or more.
  3. Your manifest is not pointing to the right file. Remember, once Web Pack compiles your code, everything is in the root, except for the assets.

That is a lot, I am sure I missed a few things even though this post took me a while to write. If you have any issues with porting over your project and find there are some missing steps or something I can clarify more, please let me know.

Getting Started with OfficeJS

If you are just getting started with OfficeJS, then this post is for you. I will assume you have some understanding of what Office Web Add-ins are. However, there is one very basic point I will demonstrate in this tutorial:

  • An Office Web Add-In is simply a web site and an XML “description” file. I detailed this in this blog post. And we are going to build on that.

So, you are brand new to OfficeJS, and want to get started. The first thing you need to do is get your development environment up and running. There is a script for that:

Microsoft Office Development Environment Script

NOTE: For more information on this script, please read my blog post.

Next, you will need to pull down the Web Add-in Side Loader tool. Keep this ZIP on your desktop. This will be used later in the steps below to make it really easy for you to install the Add-in to Excel. However, if you prefer to do the steps manually, see the link below. For more information on this tool, please see my blog post and/or the GitHub repository.

Now that you have the development environment setup, lets build your very first, super basic Excel add-in:

  • Create a folder on your desktop called “MyFirstAddIn”
  • Open Notepad and copy/paste the code from HERE, then click File, Save, name it “manifest.xml”, change the type to “All Files *.*”, browse to folder on your desktop, and click Save.

NOTE: In this step you created the manifest file. This file will be used to “install” the add-in into Excel. Essentially, it will tell Excel where the web page is for the task pane.

  • Close Notepad, then open it again. Copy paste this code HERE, then click File, Save, name it “home.html”, change the type to “All Files *.*”, browse to the folder on your desktop, and click Save.

NOTE: In this step you created the primary file for website. This one file contains both the HTML and JavaScript needed to make the taskpane load in Excel.

  • Now you need to side load the manifest and you do that by first opening the ZIP file on your desktop: Set-WebAddin (v1.0.0.0).zip and extracting the Set-WebAddin.exe to the “MyFirstAddIn” folder.
  • Press Window+R to open the run dialog, and type CMD and press enter.

NOTE: This will open the Windows Command Prompt which you will use to sideload the add-in and run it.

NOTE: The word “sideload” is a fancy way to say install it for only your instance of Excel. It is also known as Developer Sideloading.

  • Change the directory to the folder on your desktop with this command:
cd "%userprofile%\desktop\MyFirstAddIn"
  • Next, type this command:
Set-WebAddin -test -manifestPath "%userprofile%\desktop\myfirstaddin\manifest.xml"

NOTE: In this step you have sideloaded the add-in to Office. This essentially tells Office you have an added an Excel add-in via the information in the manifest file which, when loaded tells Excel everything it needs to know about it, including where to find it. If you prefer to do the sideloading steps manually, you can follow them here.

  • Now you are almost ready. Type this last command (that is a dot/period at the end, very important):
http-server .

NOTE: The above steps start a local the http-server on your computer so that it will serve the webpage in the browser from the folder (.) you are current inside. You can now test this by clicking this link.

  • You are now ready to go. Simply open Excel and a new Blank Workbook. Then on the Insert tab, click the down arrow next to My add-ins and click BasicAddin. It should look like this:
The BasicAddIn in the Developer Add-ins menu
  • Your add-in Task Pane should load.
  • Click in cell A1, type “Hello World!”, and then in your Task Pane, click the “Click here” button and you should see “Hello World!” in the bottom of the page.
  • Once you are done, you can press CTRL+C in the command prompt window to shutdown the server and then to uninstall the add-in you can use this command:
Set-WebAddin -cleanup -manifestPath "%userprofile%\desktop\myfirstaddin\manifest.xml"

And that is it. There are three things to note here:

  • You created your first web add-in.
  • You did not use VSCode or any Integrated Development Environment. You did this only with Notepad.
  • But as an added bonus – you have also installed VSCode and the tools you need to start doing more complex things with Office Web Add-ins.

JavaScript, HTML, Web, CSS, front-end, back-end, web server, ports, local hosts… some, if not all of these are new terms if you are just getting into this new world of the Web and Web add-ins. But it does not have to be that complicated. As I referred to earlier, the following posts should be your next two stops on your journey:

Getting started as an OfficeJS Developer

Start Developing in OfficeJS Today with ScriptLab

If you have any questions, please reach out to me.

easyEws v1.0.15 Released

In this latest release I have incorporated my first community pull. A big thank you to Vijay Samtani for adding the sendMailItem. This new addition allows you to create a message to multiple recipients and with zero to many attachments.

Here is a link to the repository:

You can references it in your code:

<script type="text/javascript" src="node_modules/easyews/easyews.min.js"></script>

And you can allow pull it down from NPM:

npm install easyEws

An Asynchronous MessageBox

Recently, I had a project I was working on where we needed to be able to notify the user of a possible problem during a long running process. The results would not be ruined, or wrong, just likely incomplete. In this particular case we were collecting several pieces of information and sometimes one of those pieces were not available (say, for example the server was down, the file was missing, or it took too long). We wanted to let the user know the process had encountered an issue, but not stop it. Yet we wanted to also give them the option to retry it. So we presented an Abort, Retry, Cancel dialog like this one:


The problem is that with a traditional MessageBox the entire process would be frozen. If this happens immediately after the user heads off to lunch, when they get back there would be no report, no data and most of the process would not have been run yet, for example.

What I needed was for the process to continue, but to allow the user to be able to take action before the process completed on anything that was found. For example, they see this error above and think: “oh crap, I forgot to copy over the report file.” So they put the correct file in place and hit “Retry.”

Therefore, I created the AsyncMessageBox class:

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
namespace NonModalMessageBox
/// <summary>
/// Provides an asyncronous MessageBox. You can use this static class to
/// present a message to the usser while your code can continue to run.
/// You can attach to the AsynMessagebox.MessageboxClosed event to get the
/// result from the dialog once the user does close it.
/// </summary>
public static class AsyncMessageBox
private static readonly IntPtr HWND_TOPMOST = new IntPtr(1);
private static readonly uint SWP_NOMOVE = 0x0002;
private static readonly uint SWP_NOSIZE = 0x0001;
private static readonly uint SWP_SHOWWINDOW = 0x0040;
private static extern IntPtr FindWindow(string PstrClassName, string PstrCaption);
private static extern void SetWindowPos(IntPtr PintWnd, IntPtr PintWndInsertAfter, int PintX, int PintY, int PintCx, int PintCy, uint uFlags);
public delegate void MessageBoxClosedHandler(object PobjSender, MessageBoxClosedEventArgs PobjEventArgs);
public static event MessageBoxClosedHandler MessageBoxClosed;
private static bool MbolAlreadyShowing = false;
/// <summary>
/// Shows an asyncronous dialog
/// Fires the MessageBoxClosed event when it is closed.
/// This is a static messagebox, so only one can be displayed at a time
/// Once called, the event handler is detached
/// </summary>
/// <param name="PstrText">The message in the message box</param>
/// <param name="PstrCaption">The cpation on the message box</param>
/// <param name="PobjButtons">The buttons for the message box</param>
/// <param name="PobjIcon">The icon for the messafe box</param>
/// <param name="PobjDefault">The default button selected in the messagebox</param>
/// <returns></returns>
public static bool Show(string PstrText, string PstrCaption = "", MessageBoxButtons PobjButtons = MessageBoxButtons.OK, MessageBoxIcon PobjIcon = MessageBoxIcon.None, MessageBoxDefaultButton PobjDefault = MessageBoxDefaultButton.Button1)
if (MbolAlreadyShowing) return false; // failed – already displayed
DialogResult LobjResult = DialogResult.None;
// start a thread to show the dialog
new Thread(() => {
MbolAlreadyShowing = true;
LobjResult = MessageBox.Show(PstrText, PstrCaption, PobjButtons, PobjIcon, PobjDefault);
MbolAlreadyShowing = false;
// start a separate thread to wait for the result from above
new Thread(() => {
// now make it topmost
IntPtr LintHwnd = FindWindow("#32770", PstrCaption);
// Stay here until we get a result
while (LobjResult == DialogResult.None)
// create a hidden form so we can invoke back to the UI thread
// otherwise anyone attached to the event handler will get an
// exception if they try anything with the UI
using (Form LobjForm = new Form { ShowInTaskbar = false, Opacity = 0 })
LobjForm.Show(); // show hidden form – ui thread
// fire the event
LobjForm.Invoke(new Action(() => {
MessageBoxClosed?.Invoke(new object(), new MessageBoxClosedEventArgs(LobjResult));
MessageBoxClosed = null; // important to release this
LobjForm.Close(); // close hidden form
return true; // created
return false; // failed
/// <summary>
/// Event arguments for an AsyncMessageBox
/// </summary>
public class MessageBoxClosedEventArgs
public DialogResult Result { get; private set; }
public MessageBoxClosedEventArgs(DialogResult PobjResult)
Result = PobjResult;

Under the covers this uses a traditional MessageBox, but on background thread. You need to attach to the MessageBoxClosed event before you make the AsyncMessageBox.Show() call if you want to get the result. Here is how you use this method:

AsyncMessageBox.MessageBoxClosed += (o, e) =>
// handle the user response here
if(e.Result == DialogResult.Retry)
// retry code here
AsyncMessageBox.Show("There was an issue accessing 'quaterly report.xlsx'.",
"Quarterly Report Generator",

Anyway, thought this might be useful, so I have shared it. wlEmoticon-hotsmile.png

Microsoft Office Development Environment Setup Script

If you are new to Office Web Add-in Development, then this post is for you. Even if you are not new and you need to setup a new development environment, this post is for you too.

Now, as far as manually setting up a new Office web add-in development environment, there are a few sources for helping you get started. There is the official guide here:

Set up your development environment

And I have also written one here:

How to-Configure VSCode for Office Development

NOTE: Mine is a tad more complicated, but essentially walks you through setting up for full fledged development, including source control with Git. That article was actually more for me when I wrote it.


However, after a co-worker and fellow Office Developer (Marty Andren) and I got to talking about this topic, I had an idea to create a script essentially automating the entire process. That was how the Microsoft Office Development Setup script, or MODES was born. The script is posted in the following repository:

The MODES script is provided as-is for folks who need to setup an Office development environment. It is a simple run it and done. Once you have completed this script you will have:

NOTE: This also installs http-server, a simple local web server that I hope to use in a forthcoming blog post that expands on a previous post demonstrating how easy it is to build a Web Add-ins from scratch.

NOTE: The script installs everything as local user, so there is no need for administrative permissions.

On the MODES GitHub post, you can follow the README directions to download and run the PowerShell script. If you are unfamiliar with PowerShell, you can just follow the steps and should walk you through what you need to do fairly quickly and easily.

If the above script cannot be run on your system (as a script), you can try to perform the steps manually by Copy/Pasting the following GIST into a PowerShell window and pressing enter:

Add-Type assembly "";$vscode_url = "";$vscode_output = $env:USERPROFILE + "\downloads\vscode.exe";$node_url = "";$node_output = $env:USERPROFILE + "\downloads\node.exe";$node_install_path = $env:USERPROFILE + "\node";$npm_url = "";$npm_output = $env:USERPROFILE + "\downloads\"
Invoke-WebRequest Uri $vscode_url OutFile $vscode_output
Invoke-WebRequest Uri $node_url OutFile $node_output
Invoke-WebRequest Uri $npm_url OutFile $npm_output
Start-Process FilePath $vscode_output ArgumentList ('/VERYSILENT', '/MERGETASKS=!runcode') Wait
md $node_install_path
copy $node_output $node_install_path
[io.compression.zipfile]::ExtractToDirectory($npm_output, $node_install_path)
[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", [EnvironmentVariableTarget]::User) + ";" + $node_install_path, [EnvironmentVariableTarget]::User)
$env:Path += ";" + $node_install_path
cd $node_install_path
Start-Process FilePath npm ArgumentList ('install', '-g', 'npm') WindowStyle Hidden Wait
Start-Process FilePath npm ArgumentList('install', '-g', 'yo', 'generator-office') WindowStyle Hidden Wait
Start-Process FilePath npm ArgumentList('install', '-g', 'http-server') WindowStyle Hidden Wait
$yo_file = $node_install_path + "\yo.ps1"
ren $yo_file "yo.ps1.old"

view raw
hosted with ❤ by GitHub

This GIST is a “minified” version of the script on GitHib (no comments, no output and some lines are combined). So if you run this GIST, you will need to do give it time to complete as there is no feedback.



easyEws v1.0.14 released

First off, there is no version 1.0.13. wlEmoticon-winkingsmile.png

For this latest release, I have added one handy new function getParentId(). This function uses the child message “InReplyTo” field to locate the parent message with the same corresponding “InternetMessageId” field. If found, you will get the Id of that parent message. If not found, you will get NULL.

As per usual, to get the latest you can link to the CDN:

Or you can access it from NPM via this command:

npm -install easyews

NOTE: I was informed that I had forgot to update NPM to v1.0.12 per the last release. So I have fixed that and have updated this build on NPM.

easyEws v1.0.12 released

I have updated the easyEws library.

The changes I made in v1.0.11 for ResolveNames were only ES6 compliant and did not work in IE11. Specifically, I defined an ES6 class object versus a more compliant ES3+ function/class. The update was recoded and now functions in IE11.

Additionally, there was a reported issue with recipient aggregation in the getallRecipientsAsync() function. I was using a .forEach() on the recipient array which seemed to be causing problems with larger result sets. So I updated it to use a standard for loop.

Other than that there were no major changes to this version.