Introduction
I have been developing applications and add-ins integrated into Microsoft Office for quite a long time now, and no matter what type of application you are working on, at some point, you always face the same frustration: the frustration of limited functionality provided by the "official" Microsoft interfaces, such as Outlook Object Model or Word Object Model. One of the weakest bits in these interfaces is the lack of support for user interface customization - as long as you just want to add your own toolbar, you are fine, but if you want to do anything more than that, you are going to hit a brick wall. Or, are you really? This article should hopefully give you some guidelines on how to overcome these limitations of Word, Excel, Outlook, and other Office applications by using couple of dirty, yet reliable tricks, all from 100% managed code.
To demonstrate the principle of advanced UI integration, I've decided to build a simple panel to Microsoft Outlook which will display some basic information about the email which is selected. I've picked up Microsoft Outlook, but this principle can be applied to any application from the Office family such as Word, Excel, OneNote etc.
Hacking into Outlook
Before we start coding, let's first discuss the principles behind the scenes. Like any other Windows application, the main window of Microsoft Outlook consists of multiple child windows (in .NET analogy, they would be members of the Control.Controls
collection), so it should be possible to add your own window to this hierarchy. Before we start adding any new windows, at first, we need to know how the window structure of Outlook looks like. We can use my favorite tool Spy++ to view this structure:
From Spy++, you can see that there is a toolbar panel window (green) on the top, the folder list view window (purple) on the left, and finally, the main grid (red) on the right side of the window.
Now, we know the structure, so we can solve the crucial problem - we have nowhere to place our own window. To get some free space on the dialog, we will have to "borrow" the space from an existing child window. I would like to dock my panel to the right, which makes the main grid the most logical candidate to borrow some space from. So, if we reduce the width of the messages grid, we should have some free space to place our own window:
But, that is enough with the theory - let's see how we implement this.
Implementation
I wanted to build a generic framework which will allow docking any standard .NET UserControl into Outlook, so I have decided to use the following structure:
I call the window which we are borrowing the space from, the "sibling window". Any child window of the Outlook main window can become the sibling window; however, for this case, we are going to use the messages grid. As mentioned above, we reduce the width of the sibling window, which creates free space on the dialog. We are going to place our own window called PanelContainer
into the newly created free space. The PanelContainer
has two purposes: it contains an empty Panel
control which can host any .NET control, and it provides Outlook look and feel for the background and borders (otherwise, the background would be white and there would be no border lines). Once we have the PanelContainer
in place, we can just assign a standard .NET UserControl
to it, and we are done - the PanelContainer
will do all the hard work, and the nested control does not have to even know that it is "living" inside the Outlook panel.
The easiest to get our PanelContainer
displayed in Outlook is through an Outlook add-in. An Outlook add-in is is a DLL library (either managed or unmanaged) which implements the IDTExtensibility2
COM interface:
public interface IDTExtensibility2
{
void OnAddInsUpdate (ref Array custom);
void OnBeginShutdown (ref Array custom);
void OnConnection (Object Application, ext_ConnectMode connectMode,
Object AddInInst, ref Array custom);
void OnDisconnection (ext_DisconnectMode RemoveMode, ref Array custom);
void OnStartupComplete (ref Array custom);
}
When Outlook starts, it will load the DLL, and if it finds out that the IDTExtensibility2
interface is implemented, it will call the OnStartupComplete
method. In any process/application, there should always be just a single thread which executes the message loop of all child windows - the UI thread. Thanks for us, the call to OnStartupComplete
(like all calls of the IDTExtensibility2
interface) is made from the UI thread, which is very important because we are going to create the PanelContainer
window and we will assign it as a child of the Outlook main window. If this call was made from another thread, we would have bigger problems because the PanelContainer
instance would be created out of the UI thread, which would cause big stability issues.
public void OnStartupComplete(ref System.Array custom)
{
...
IntPtr outlookWindow = FindOutlookWindow();
_panelContainer = new PanelContainer();
SafeNativeMethods.SetParent(_panelContainer.Handle, outlookWindow);
...
}
We have made the PanelContainer
instance a child window; now, we need to move it to its place. But, before we do that, we need to reduce the width of the sibling window (i.e., the message grid). To find the sibling window, we are going to use the FindWindowEx
API (see the MSDN documentation) method, which will return the first child window of the Outlook main window with the specified window class. That is when Spy++ comes handy again - we can see that the window class of the message grid is rctrl_renwnd32
.
private const string SIBLING_WINDOW_CLASS = "rctrl_renwnd32";
IntPtr siblingWindow = SafeNativeMethods.FindWindowEx(outlookWindow,
IntPtr.Zero, SIBLING_WINDOW_CLASS, null);
Now, we have all window handles we need; so, we can finally resize the sibling window and move the PanelContainer
window on its place.
private void ResizePanels()
{
Rectangle siblingRect = SafeNativeMethods.GetWindowRectange(this.SiblingWindow);
Rectangle parentRect = SafeNativeMethods.GetWindowRectange(this.ParentWindow);
SafeNativeMethods.POINT topLeft =
new SafeNativeMethods.POINT(siblingRect.Left, siblingRect.Top);
SafeNativeMethods.ScreenToClient(this.ParentWindow, ref topLeft);
int newWidth = parentRect.Width - topLeft.X - _panelContainer.Width;
SafeNativeMethods.SetWindowPos(this.SiblingWindow, IntPtr.Zero, 0, 0, newWidth,
siblingRect.Height, SafeNativeMethods.SWP_NOMOVE |
SafeNativeMethods.SWP_NOZORDER);
_panelContainer.Left = topLeft.X + newWidth;
_panelContainer.Top = topLeft.Y;
_panelContainer.Height = siblingRect.Height;
}
We are almost done; but, we need to handle one more situation. The ResizePanels
method calculates the correct placement for the PanelContainer
; but, as soon as the user resizes the Outlook window, it is not valid anymore because, by default, the width of the sibling window would be restored and the PanelContainer
would stay in place, which is not what we want. The solution is, however, quite simple; we just need to make sure to call the ResizePanels
every time the sibling window is resized.
In this situation, we cannot use the standard Control.SizeChanged
event because the sibling window is just a native window and we have nothing but its window handle. Fortunately, that is where the NativeWindow
class comes in place. The NativeWindow
class allows us to subclass a window procedure of any window (no matter if it is managed or unmanaged). By subclassing a window, we are going to receive all window messages into our own WndProc
method; so, by looking for the WM_SIZE
message, we can detect when the window is resized. We will first let the default window procedure process the message, and then we will do our own processing. I created a small derived class, SubclassedWindow
, for this purpose - it accepts a native window handle, and it will raise a SizeChanged
event every time the window receives the WM_SIZE
message.
sealed class SubclassedWindow : NativeWindow
{
public event EventHandler SizeChanged;
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (m.Msg == (int)SafeNativeMethods.WindowsMessages.WM_SIZE)
OnSizeChanged();
}
private void OnSizeChanged()
{
if (SizeChanged != null)
SizeChanged(this, null);
}
}
The only remaining bit is to call the ResizePanels
method every time the sibling window size changes.
_subclassedSiblingWindow = new SubclassedWindow();
_subclassedSiblingWindow.AssignHandle(this.SiblingWindow);
_subclassedSiblingWindow.SizeChanged +=
new EventHandler(subclassedSiblingWindow_SizeChanged);
...
private void subclassedSiblingWindow_SizeChanged(object sender, EventArgs e)
{
ResizePanels();
}
The PanelContainer
will now be placed in the correct position, and its dimensions will always match the free space.
Recipients Panel Demonstration
Since we have our generic PanelContainer
in place, it is time to create some demonstration control and nest it into the container. I chose a simple, yet interesting, demo of Outlook integration - I am going to show the subject and recipients of the selected email in the panel. If the user clicks on the recipient of the mail in the panel, it will open a new web browser window and search for the person's name on Google. And, all this will be achieved through a standard UserControl
, which I call MyPanel
.
To link this control to Outlook, I need to capture the SelectionChange
event (we finally use the Outlook Object Model for this) and update the controls according to the email which is selected in the message grid.
private void outlookExplorer_SelectionChange()
{
MailItem mailItem = _outlookExplorer.Selection[1] as MailItem;
string senderName = mailItem.SenderName;
this.lblSender.Text = String.Format("{0} writes regarding", senderName);
this.lblSubject.Text = mailItem.Subject;
...
}
And, that's it - we're done!
Final Word
I hope this example proved to you that there are further possibilities for Office integration than the ones officially mentioned by Microsoft.
This code could, however, still use some improvements - for example, the ability to resize the width of the PanelContainer
is missing; also, a more generic docking mechanism would be nice. I hope to eventually implement this, once I need it...