Wednesday, August 10, 2005
Use a toolbar in multiple Outlook 2003 inspectors with VSTO
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.
Saturday, August 06, 2005
.NET add-ins for both Office 2000 and XP/2003
This customer unfortunately has Office 2000 and Outlook 2000 (version 9.x), and has no money or willingness to upgrade to Office 2003. Thus, I had to figure out how to make the .NET add-ins support Office 2000. It turned out that this is actually quite possible, following these guidelines:
- Use the Office XP PIAs, always install them to GAC (download from MSDN)
- Use the Office object model interfaces, not the classes
- Implement version switch code whenever Office 9 methods have different signatures than Office 10/11
- Use .NET reflection and .GetType().InvokeMember(...) to call the Office 9 methods that are different from Office 10/11
The Office object model provides you with both interfaces and classes, typically in pairs. E.g. Application/ApplicationClass, Document/DocumentClass, MailItem/MailItemClass. Always declare your object references using the interface, not the class. The class is version specific, and you might get cast errors when your add-in runs on a different Office version.
Some methods have different number of parameters in Office 2000 than in XP/2003. This applies e.g. to Word.Documents.Open(...) and Word.Document.SaveAs(...). Thus, you cannot use these methods directly in your code as it will cause run-time errors when loaded in e.g. Word 2000. Note that the code will compile OK, afterall the code references the Office XP PIAs. Your code must check the version number at run-time and switch between calling the Office XP/2003 or the Office 2000 methods.
To be able to call some of the Office 2000 methods from your code, you must use .NET reflection to call the methods using .InvokeMember(...), which is similar to COM "late binding". The code will look like this:
string version = this._application.version;
if(version.StartsWith("9."))
{
Object[] params = new Object[]{fileUrl, Type.Missing, ...};
doc.GetType().InvokeMember("SaveAs", BindingFlags. InvokeMethod, null, doc, params);
}
else
{
doc.SaveAs(fileUrl, ref param1, ref param2, ...);
}
Deploying at this customer also required some changes to our installers. The .NET 1.1 add-ins (not VSTO 2005 add-ins) read settings from our Objectware.OfficeAddin.dll.config file which has to be installed to the Office executable folder, which differs between the versions:
C:\program files\microsoft office\
plus the applicable folder office\ or office10\ or office11\
In addition, this customer runs both Windows XP and Windows 2000, which requires the add-in registry settings to be modified to refer to the correct location of MSCorEE.DLL:
C:\winNT\system32\MSCorEE.DLL for Win2000Pro
C:\windows\system32\MSCorEE.DLL for WinXPPro
A final issue about using .NET web-services in your Office add-in: if the web-service uses serializable objects as parameters in the WSDL, the .NET framework will try to auto-generate and -compile classes for these objects on the client side at run-time. As your add-in runs within the Office process, this will most likely cause hard to track and resolve exceptions. I recommend using only basic .NET value types and array types (e.g. ArrayList) in any web-service that is to be consumed by an Office add-in.
Office 2000 object model reference at MSDN.
Office XP object model reference at MSDN.
Office 2003 object model reference at MSDN.
(Navigate to the VBA language reference using the treeview if the link does not)
Sunday, July 31, 2005
Summer holiday scope complete
Now it's back to work implementing MSCRM and SharePoint, but it is only six weeks until we go hunting grouse (Lagopus Lagopus) in Finnmark.
PS! Congrats to my colleague Mads Nissen with becoming a Microsoft MVP.
Tuesday, July 05, 2005
Using SharePoint lists.asmx web service
SharePoint (WSS 2.0) provides several web services that gives you access to different parts of the object model and its data, without the need to use the object model directly and without worrying where the data is stored in a farm deployment. Getting started is a bit tricky, but here is a list of articles that will get you started:
- Introducing SharePoint Web Services on .NET Developer's Journal
- Using data from SharePoint lists by Paul Ballard (using XML/XPath, DataSet or entity objects)
The real column names are those you see in the CAML output area of the U2U tool, and you must use these column names in the XML parameters of the web service methods (e.g. 'Name' is really called 'Title'). If you do not get the name right or reference the wrong list, you will get an error like this:
Exception of type Microsoft.SharePoint.SoapServer.SoapServerException was thrown.
<detail> <errorstring xmlns="...">No such field name.
No field was found with that name. Check the name, and try again.</errorstring><errorcode xmlns="...">0x81020014</errorcode</detail>
Note how easily Microsoft has left out an important piece of information: which field name ?
An important detail when programming against the SharePoint web services is to remember to set the .Url property of your web reference to point to the correct WSS team site. The lists.asmx web service is a virtual service and is by SharePoint magic available in all WSS sites, not only in the IIS virtual directory /_vti_bin/. In addition, remember to apply correct credentials for authentication.
Set the .Url like this (avoid getting //_vti_bin/):
_svcList.Url = siteUrl + "/_vti_bin/lists.asmx";
_svcList.Credentials = System.Net.CredentialCache.DefaultCredentials;
The standard SDK documentation of the lists web service is available on MSDN, along with the rest of the WSS web services.