Wednesday, August 10, 2005

Use a toolbar in multiple Outlook 2003 inspectors with VSTO

Making .NET add-ins for Outlook 2003 has become straightforward with VSTO 2005 (VSTO-O), especially of you only add your menus and toolbars and event handlers to the main window of Outlook (Outlook.Application.ActiveExplorer). Most of the samples available with VSTO-O do just this.

Adding toolbars and event handlers to the popup windows (Outlook.Application.Inspectors) that Outlook uses to show details about a mail, an appointment, a task, etc, will at first seem to be trivial, but there are some pitfalls. Things might work fine with a single inspector, but you need to test your Inspector add-in stuff by opening multiple inspectors at once and clicking your toolbar in all of them to test your event handler. Typically, only the first inspector will trigger the event, or your event will trigger once for each open inspector. Not to forget the event working for some time, then stop working due to the well-known 'garbage collector ate my event handler' mistake.

What you need to make your code support Outlook inspectors correctly, is an inspector wrapper class that gives your code one custom object per open inspector at run-time. The wrapper lets you add code that ensures that you handle only inspectors for specific item types; e.g. mails, but not contacts, tasks and appointments (check the Outlook.OlItemType of the Inspector.CurrentItem). The wrapper also ensures that the correct instance of your event handler code gets called when your toolbar is clicked in one of the multiple open inspectors. Finally, the wrapper keeps a reference to your toolbar and event handler for the lifetime of each inspector, solving the garbage collector problem.

I have used the inspector wrapper code written by Helmut Obertanner, which is available here at OutlookCode.com. The code is for .NET C# pre VSTO-O, but will work with a few modifications. Refer to the related discussions in the forum for how to solve diverse add-in problems.

[UPDATE] Helmut has provided an updated version of the explorer and inspector wrapper at his site, including applicable Marshal.ReleaseComObject() calls: download the X4UTools.

[UPDATE] If Outlook hangs around in the background when closed, then you have missed calling Marshal.ReleaseComObject() for some Outlook objects created or referenced by your add-in. This can also be the cause of the "The operation failed due to network or other communication problems. Check your connections and try again." message, be sure to release all Outlook (COM) objects you create.

I have modified the code slightly to work with "temporary" Outlook toolbars and to ensure that multiple inspectors functions correctly:


public class XMailItem
{
private const string _TOOL_EDITMAILINGLIST = "OW_EDITMAILINGLIST";
private const string _BTN_EDITMAILLIST = "Choose mailing list members";
private DateTime _createdDts = DateTime.Now;
private Office.CommandBar _toolBar;
private Office.CommandBarButton _btnEditMailingList;


. . .

private void MailItem_Open(ref bool Cancel)
{
#if DEBUG
DateTime tmp = _createdDts; //inspect to check which run-time inspector object this is
#endif
// event isn't needed anymore
_mailItem.Open -= new Microsoft.Office.Interop.Outlook. ItemEvents_10_OpenEventHandler(MailItem_Open);


// get the Inspector here
_inspector = (Outlook.InspectorClass)_mailItem.GetInspector;


// register for the Inspector events
_inspector.InspectorEvents_Event_Close += new Microsoft.Office.Interop.Outlook. InspectorEvents_CloseEventHandler(Inspector_InspectorEvents_Close);


//create the toolbar
this.InitializeMailToolbar();
}




private void InitializeMailToolbar()
{
try
{

if (_mailItem is Outlook.MailItem)

{
//find existing toolbar (same toolbar in all inspectors), even when temporary
try
{
_toolBar = _inspector.CommandBars[_TOOL_EDITMAILINGLIST];
}
catch (Exception)
{
//add toolbar
_toolBar = _inspector.CommandBars.Add(_TOOL_EDITMAILINGLIST, Office.MsoBarPosition.msoBarTop, false, true);
}


//find existing toolbar button
try
{
_btnEditMailingList = (Office.CommandBarButton)_inspector. CommandBars[_TOOL_EDITMAILINGLIST].Controls[_BTN_EDITMAILLIST];
}
catch (Exception)
{
//add button
_btnEditMailingList = (Office.CommandBarButton)_toolBar.Controls.Add(Office.MsoControlType.msoControlButton, Type.Missing, Type.Missing, 1, true);
_btnEditMailingList.Caption = _BTN_EDITMAILLIST;
_btnEditMailingList.Style = Office.MsoButtonStyle.msoButtonCaption;
}


_toolBar.Visible = true;

_btnEditMailingList.Visible = true;
//add event handler to button; each open inspector adds itself to the event handler chain (+=)
_btnEditMailingList.Click += new Microsoft.Office.Core._CommandBarButtonEvents_ClickEventHandler(_btnEditMailingList_Click);


}
}
catch (Exception ex)
{
MessageBox.Show("An unexpected error occurred during toolbar init: " + ex.Message, CONST.MSGBOX_TITLE, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
}



void _btnEditMailingList_Click(Microsoft.Office.Core.CommandBarButton Ctrl, ref bool CancelDefault)
{
#if DEBUG
DateTime tmp = _createdDts; //inspect to check which run-time inspector object this is
#endif
this.ShowEditMailingListDialog();
}



private void Inspector_InspectorEvents_Close()
{
#if DEBUG
DateTime tmp = _createdDts; //inspect to check which run-time inspector object this is
#endif
try
{
//raise event, to remove us from active items collection
if (Item_Closed != null)
{
Item_Closed(this, new XEventArgs(_mailItem.GetHashCode()));
}


//cleanup resources; remove this from event handler chains
_btnEditMailingList.Click -= new Microsoft.Office.Core. _CommandBarButtonEvents_ClickEventHandler(_btnEditMailingList_Click);


_inspector.InspectorEvents_Event_Close -= new Microsoft.Office.Interop.Outlook. InspectorEvents_CloseEventHandler(Inspector_InspectorEvents_Close);

//release Outlook COM objects as applicable

Marshal.ReleaseComObject(_inspector);
Marshal.ReleaseComObject(_mailItem);
}
catch (System.Exception ex)
{
MessageBox.Show("An unexpected error occurred during inspector close: " + ex.Message, CONST.MSGBOX_TITLE, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
}

. . .

} //XMailItem

It is important to know that the key of the Controls[] collection is actually the Caption of the button. Failure to use the same text in both places will cause multiple buttons to be added to the toolbar in some circumstances (e.g. use the next and previous mail buttons a couple of times to move back and forth, use the move to folder button, etc).

Note how the toolbar button click event handler for each wrapper is added (+=) to the button's event delegate chain. With no further code than this, Outlook (.NET) will be able to call only the event handler in the wrapper object of the inspector that triggered the click event of the common toolbar. Remember to remove your event handler from the event chain when the inspector closes.

Note that the toolbar is shared between all open inspectors, thus you must never delete it when an inspector closes as this will remove the toolbar from all open inspectors. Still, it is recommended to remove the toolbar when the add-in unloads, as a best practise, should the "temporary" flag not apply to your Outlook configuration (temporary has no effect when using Word as the mail editor).

If your toolbar opens dialog boxes, I strongly suggest that they are modal, .ShowDialog(), to avoid confusing your users with which dialog belongs to which inspector.

[UPDATE] Ken Slovak has published example code including explorer and inspector wrappers for Outlook 2007: templates from Professional Outlook 2007 Programming.

6 comments:

Anonymous said...

Hello -
I have a VSTO sample now for Inspector and Explorer wrapper on my site. It also has a workaround for setting the Button Picture in an Emailinspector even when Word is used as editor.

See: http://www.x4u.de/LinkClick.aspx?link=X4UTools.zip&tabid=77&mid=393

greets, Helmut
[http://www.x4u.de]

Anonymous said...

If an event handler for the button click is added for each inspector, then when the button is clicked, won't it be fired on every inspector ?

I don't see how you can tell which inspector fired the event with your code here.

Anonymous said...

my bad, it does work..
for some reason when i was debugging, my changes didn't show up.. and then it suddenly worked..

Anonymous said...

Hello Kjell-Sverre,
Thanks a lot for your add-in on Helmut add-in :-) !
Despite very detailed info given to the rest of us by Helmut and yourself , I am still having few questions.
Hope you would be able to spend some of your time looking into them.

1. I need to create the statefull command button in each new mail inspector window. I am not sure if it is possible with OL Object Model. And , of course , it is related to multiple open instances of mail inspectors.

The statefull Icon button should change the icon or ,say, keep its Up or Dawn state after user has pressed it . This is to notify the user that its new mail will or will not be then encrypted. On this button click I also set some value in the wrapper associated mail item UserProperty. This property is then used in OnItem Send event to modify the outgoing mail item subject line. Why it is so complicated? Why not to just change the subject line of the button click ? Well, he user should not be able to change updated (on button click ) subject line. That's why it (subject line update) is done on ItemSend event.
Now - my main question:
Is it possible to do at all , even with the wrapper ? All samples I found so far are reusing the same button, created once by the first opened new mail inspector. In thit case we share the same button all over all opened new mail inspectors. The change of its state is immediately seen on all windows. I tried to use one separate instance of the button object per inspector and ended up with each new button added to all inspectors. Still do not get it why they all get displayed on each window. OK , it is seems to be a particularity of underlying COM object model. So , I tried to add new toolbar to each inspector. Before doing that, on Item Open event I am looping through all already existing toolbars with the same caption, rendering its Visible and Enabled properties. That helps to display only one proper bar and button per inspector. But all other ones (toolbars) still could be back enabled by the user from inspector right click context menu. Can you think of some other approach to solve this problem ?
I could think of combination of stateless button (meaning no Icon change or Down, Up state remembering) and state change for other elements of inspector or mail item that could be visible for the user. Can I toggle , say , the color of subject line filed in the inspector ?

2. The second thing is that I would ask you for a complete listing of your wrapper and ThisApplication class code. As I am trying to solve the problem by testing both - stateless and statefull button approach, I found that can't get it properly work with stateless buton.

Now , I am finally done with putting up my question. Thanks for reading such a long post :-)!

Any input is greatly appreciated.
Thanks,
Alex

Kjell-Sverre Jerijærvi said...

1. Yes, the toolbar+button is shared between all inspectors; and as you've found out, it is very hard to do anything about it. As you say, you must keep the state separate from the button, i.e. in the wrapper or the mail item itself. I do not sure think that you can modify the standard GUI, maybe a custom form is what you need.

2. I no longer have access to the source, but use the link in the post to download Helmuts X4UTools wrapper. It has been revised after my post, and I know that it works very well.

Anonymous said...

Thanks a lot Kjell-Sverre. It will save me alot of my time in what is left of this weekend :-) !

My code was modified few diff. tryings and stopped to work properly after I switched to using the same toolbar+button. In fact I still add Buttn Click event every time on MailItem Open event for the same button and it is only one instance of open inspector where the event takes place. sometimes it is two in the row, sometimes its only one. The rest of open inspectors do not raise/intercept any click events. Seems that those events are "eaten" on run time . Could it be related to CG and the fact that I declare the button in mail item wrapper scope ? Should I declare and create it on Application (add-in ) level ?
Thanks a lot again for your time .
Alex