Der InspectorWrapper ist immer wieder gerne zitiert wenn es um Problemlösung bei der Outlook Add-in Entwicklung geht.
In diesem Beispiel möchte ich gerne nochmals eine mögliche Implementierung vorstellen.
Haupsächlich dient der InspectorWrapper dazu, mehrere geöffnete Fenster in Outlook korrekt zu identifizeren.
Dieses Beispiel wurde mit Visual Studio 2010 (Beta2) und Outlook 2007 erstellt.
Zunächst erstellen wir ein neues Outlook Add-in Projekt und speichern es unter dem Namen "InspectorWrapperExplained".

Abbildung 1: Erstellen eines Outlook Add-in Projektes mit Visual Studio 2010
Nach Fertigstellung des Assistenten erhält man eine Anwendungsmappe mit dem gewünschten Projekt und einer Klasse mit der Bezeichnung "ThisAddIn". Diese Klasse hat zwei Methoden names "ThisAddInStartup" und "ThisAddinShutDown". Eine so erstellte Anwendung wird unter Zuhilfenahme von Visual Studio Erweiterungen erstellt, welche auch als VSTO (Visual Studio Tools für Office) bezeichnet werden. In einem VSTO-Projekt gibt e simmer eine statische Eigenschaft mit dem Namen "Globals". Innerhalb der ganzen Assemply kann auf diese Eigenschaft zugegriffen werden – z.B.: um Zugriff auf die Applikations-Instanz zu erhalten. Damit wird sichergestellt, dass bei der Verwendung von Outlook Objekten keine Sicherheitswarnungen angezeigt warden – z.B. beim Lesen von Email Adressen oder dem automatischen versenden von Emails aus dem Add-in. Innerhalb vom Add-in sollte man also nur über die Globals-Variable auf das Applikationsobjekt zugreiffen – dies nur zur Erinnerung. Eines der häufigsten Probleme bei der Outlook-Anwendungsentwicklung ist das plötzliche nicht mehr funktionieren von Click events. Dies Problem werden wir unter anderem hier lösen.
Wozu ist ein InspectorWrapper gut?
Damit können gleich mehrere Probleme gelöst werden:
- identifizieren des Fensters bei mehreren Click events
- stellt sicher, dass kein event verloren geht
- stellt sicher, dass der Speicher freigegeben wird
- kann verschiedene Outlook Item typen unterschiedlich implementieren
Wie wird das ganze implementiert?
Als erstes benötigt man dazu eine Basisklasse "InspectorWrapper". Die Basisklasse bekommt zur eindeutigen Identifizierung eine Guid-Eigenschafft verpasst. Diese wird benötigt um die mehrfach vorhandene Inspector-Instanzen zu unterscheiden. Ausserdem erhält die Basisklasse eine Inspector-Eigenschft welche Zugriff auf die eigentliche Inspector-Instanz gewährt, und welche für die korrekte behandlung der Inspector-Nachrichten benötigt wird.
using Outlook = Microsoft.Office.Interop.Outlook;
using Office = Microsoft.Office.Core;
using System;
namespace InspectorWrapperExplained {
///
/// Die Basisklasse für alle InspectorWrapper
///
internal abstract class InspectorWrapper {
///
/// Eindeutige Id, identifiziert ein Inspector-Fenster
///
public Guid Id { get; private set; }
///
/// Die Outlook Inspector Instanz
///
public Outlook.Inspector Inspector { get; private set; }
///
/// .ctor
///
/// Die Outlook Inspector Instanz, welche behandelt warden soll
public InspectorWrapper(Outlook.Inspector inspector) {
Id = Guid.NewGuid();
Inspector = inspector;
}
}
}
Als nächstes benötigen wir eine korrekte Behandlung zum Aufräumen und Freigeben des Speichers. Beim Entwickeln von Office Add-ins muss peinlich genau auf das korrekte Freigeben von COM Referenzen geachtet warden, da es sonst zu ungewünschten Nebeneffekten kommen kann. Um das Problem korrekt umgehen zu können, definieren wir uns eine Nachricht, welche uns das schließen eines Inspectors mitteilt.
namespace InspectorWrapperExplained {
///
/// Nachrichtensignatur, wird benutzt um den Speicher aufzuräumen
///
/// Eindeutige id der Inspector Instanz
internal delegate void InspectorWrapperClosedEventHandler(Guid id);
///
/// Die Basisklasse für alle InspectorWrapper
///
internal abstract class InspectorWrapper {
///
/// Benachrichtigung für das InspectorWrapper.Closed Ereignis.
/// Wird ausgelöst, wenn ein Inspector Fenster geschlossen wurde.
///
public event InspectorWrapperClosedEventHandler Closed;
///
/// Eindeutige Id, identifiziert ein Inspector-Fenster
///
public Guid Id { get; private set; }
///
/// Die Outlook Inspector Instanz
///
public Outlook.Inspector Inspector { get; private set; }
///
/// .ctor
///
/// Die Outlook Inspector Instanz, welche behandelt warden soll
public InspectorWrapper(Outlook.Inspector inspector) {
Id = Guid.NewGuid();
Inspector = inspector;
// register for Inspector events here
((Outlook.InspectorEvents_10_Event)Inspector).Close += new Outlook.InspectorEvents_10_CloseEventHandler(Inspector_Close);
}
///
/// Eventhandler for the Inspector close event
///
private void Inspector_Close() {
// call the Close Method - the derived classes can implement cleanup code
// by overriding the Close method
Close();
// unregister Inspector events
((Outlook.InspectorEvents_10_Event)Inspector).Close -= new Outlook.InspectorEvents_10_CloseEventHandler(Inspector_Close);
// clean up resources and do a GC.Collect();
Inspector = null;
GC.Collect();
GC.WaitForPendingFinalizers();
// raise the Close event.
if (Closed != null) Closed(Id);
}
///
/// Derived classes can do a cleanup by overriding this method.
///
protected virtual void Close();
}
}
Das Grundgerüst des InspectorWrappers ist nun eigentlich schon fertig. Wenn wir den Wrapper aber noch um alle Nachrichten erweitern, können wir die ganze Logik und das Aufräumen des Speichers in dieser Klasse kapseln. Für die entsprechenden Nachrichten werden einfach virtuelle Methoden deklariert, welche von den konkreten Klassen überschrieben werden.
namespace InspectorWrapperExplained {
///
/// Nachrichtensignatur, wird benutzt um den Speicher aufzuräumen
///
/// Eindeutige id der Inspector Instanz
internal delegate void InspectorWrapperClosedEventHandler(Guid id);
///
/// Die Basisklasse für alle InspectorWrapper
///
internal abstract class InspectorWrapper {
///
/// Benachrichtigung für das InspectorWrapper.Closed Ereignis.
/// Wird ausgelöst, wenn ein Inspector Fenster geschlossen wurde.
///
public event InspectorWrapperClosedEventHandler Closed;
///
/// Eindeutige Id, identifiziert ein Inspector-Fenster
///
public Guid Id { get; private set; }
///
/// Die Outlook Inspector Instanz
///
public Outlook.Inspector Inspector { get; private set; }
///
/// .ctor
///
/// Die Outlook Inspector Instanz, welche behandelt warden soll
public InspectorWrapper(Outlook.Inspector inspector) {
Id = Guid.NewGuid();
Inspector = inspector;
// Registrierung für Inspector Ereignisse
((Outlook.InspectorEvents_10_Event)Inspector).Close += new Outlook.InspectorEvents_10_CloseEventHandler(Inspector_Close);
((Outlook.InspectorEvents_10_Event)Inspector).Activate += new Outlook.InspectorEvents_10_ActivateEventHandler(Activate);
((Outlook.InspectorEvents_10_Event)Inspector).Deactivate += new Outlook.InspectorEvents_10_DeactivateEventHandler(Deactivate);
((Outlook.InspectorEvents_10_Event)Inspector).BeforeMaximize += new Outlook.InspectorEvents_10_BeforeMaximizeEventHandler(BeforeMaximize);
((Outlook.InspectorEvents_10_Event)Inspector).BeforeMinimize += new Outlook.InspectorEvents_10_BeforeMinimizeEventHandler(BeforeMinimize);
((Outlook.InspectorEvents_10_Event)Inspector).BeforeMove += new Outlook.InspectorEvents_10_BeforeMoveEventHandler(BeforeMove);
((Outlook.InspectorEvents_10_Event)Inspector).BeforeSize += new Outlook.InspectorEvents_10_BeforeSizeEventHandler(BeforeSize);
((Outlook.InspectorEvents_10_Event)Inspector).PageChange += new Outlook.InspectorEvents_10_PageChangeEventHandler(PageChange);
// Am Ende wird die Initialize Methode aufgerufen. Abgeleitete Klassen sollten diese Methode überschreiben um Ihre Initialisierung durchzuführen.
Initialize();
}
///
/// Nachrichtenbehandlung für das Inspector.Close Ereignis
///
private void Inspector_Close() {
// aufrufen der Close Methode – abgeleitete Klassen müssen diese Methode überschreiben um alles aufzuräumen
Close();
// Benachrichtigungen entfernen (Referenzen auf COM Objekte)
((Outlook.InspectorEvents_10_Event)Inspector).Close -= new Outlook.InspectorEvents_10_CloseEventHandler(Inspector_Close);
((Outlook.InspectorEvents_10_Event)Inspector).Activate -= new Outlook.InspectorEvents_10_ActivateEventHandler(Activate);
((Outlook.InspectorEvents_10_Event)Inspector).Deactivate -= new Outlook.InspectorEvents_10_DeactivateEventHandler(Deactivate);
((Outlook.InspectorEvents_10_Event)Inspector).BeforeMaximize -= new Outlook.InspectorEvents_10_BeforeMaximizeEventHandler(BeforeMaximize);
((Outlook.InspectorEvents_10_Event)Inspector).BeforeMinimize -= new Outlook.InspectorEvents_10_BeforeMinimizeEventHandler(BeforeMinimize);
((Outlook.InspectorEvents_10_Event)Inspector).BeforeMove -= new Outlook.InspectorEvents_10_BeforeMoveEventHandler(BeforeMove);
((Outlook.InspectorEvents_10_Event)Inspector).BeforeSize -= new Outlook.InspectorEvents_10_BeforeSizeEventHandler(BeforeSize);
((Outlook.InspectorEvents_10_Event)Inspector).PageChange -= new Outlook.InspectorEvents_10_PageChangeEventHandler(PageChange);
// Auf null setzen und GC.Collect(); durchführen um den Inspector freizugeben
Inspector = null;
GC.Collect();
GC.WaitForPendingFinalizers();
// Close Nachricht senden.
if (Closed != null) Closed(Id);
}
protected virtual void Initialize(){}
///
/// Methode wird aufgerufen, wenn eine andere Seite des Inspectors aufgerufen wurde
///
/// Der Name der aktiven Seite als Referenz
protected virtual void PageChange(ref string ActivePageName){}
///
/// Methode wird aufgerufen, bevor die Größe des Inspectors geändert wird
///
/// Um die Größenänderung zu verhindern, Cancel auf true setzen
protected virtual void BeforeSize(ref bool Cancel){}
///
/// Methode wird aufgerufen, bevor der Inspector verschoben wird
///
/// Um das Verschieben des Inspectors zu verhindern, Cancel auf true setzen
protected virtual void BeforeMove(ref bool Cancel){}
///
/// Methode wird aufgerufen, bevor der Inspector minimiert wird
///
/// zu verhindern, Cancel auf true setzen
protected virtual void BeforeMinimize(ref bool Cancel){}
///
/// Methode wird aufgerufen, bevor der Inspector maximiert wird
///
/// Um das Maximieren zu verhindern, Cancel auf true setzen
protected virtual void BeforeMaximize(ref bool Cancel){}
///
/// Methode wird aufgerufen, wenn der Inspector deaktiviert wird
///
protected virtual void Deactivate(){}
///
/// Methode wird aufgerufen, wenn der Inspector aktiviert wird
///
protected virtual void Activate(){}
///
/// Abgeleitete Klassen sollten diese Methode überschreiben und den Speicher freigeben
///
protected virtual void Close(){}
}
}
Konkrete Wrapper für verschiedene Nachrichtenklassen
Um die korrekte Behandlung verschiedener Nachrichtenklassen zu demonstrieren, implementieren wir beispielhaft einen Wrapper für Termine und einen für Kontakte. Wir beginnen mit einer "ContactItemWrapper" Klasse.
namespace InspectorWrapperExplained {
internal class ContactItemWrapper : InspectorWrapper {
///
/// .ctor
///
/// Die Outlook Inspector Instanz, welche behandelt warden soll
public ContactItemWrapper(Outlook.Inspector inspector)
: base(inspector) {
}
///
/// Die Kontakt Instanz des Inspectors (CurrentItem)
///
public Outlook.ContactItem Item { get; private set; }
///
/// Methode wird aufgerufen wenn der Wrapper initialisiert wird.
///
protected override void Initialize() {
// Das Item des Inspectors holen und merken
Item = (Outlook.ContactItem)Inspector.CurrentItem;
// Benachrichtigungen registrieren
Item.Open += new Outlook.ItemEvents_10_OpenEventHandler(Item_Open);
Item.Write += new Outlook.ItemEvents_10_WriteEventHandler(Item_Write);
}
///
/// Diese Methode wird aufgerufen, wenn das Item gespeichert wird.
///
/// Wenn Cancel auf true gesetzt wird, wird speichern abgebrochen
void Item_Write(ref bool Cancel) {
//TODO: Etwas implementieren
}
///
/// This Method is called when the Item is visible and the UI is initialized.
///
/// Wenn Cancel auf true gesetzt wird, wird der Inspector geschlossen
void Item_Open(ref bool Cancel) {
//TODO: Etwas implementieren something
}
///
/// Diese Methode wird von der Basisklasse aufgerufen, wenn der Inspector geschlossen wurde.
/// Hier müssen alle Referenzen und der SPeicher freigegeben werden.
/// Das Userinterface ist weg, kann hier nicht mehr verwendet werden.
///
protected override void Close() {
// Benachrichtigungen nicht vergessen
Item.Write -= new Outlook.ItemEvents_10_WriteEventHandler(Item_Write);
Item.Open -= new Outlook.ItemEvents_10_OpenEventHandler(Item_Open);
// benötigt, wenn hier nur auf null gesetzt wird, bleibt eventuell eine Referenz auf den Inspector im Speicher (Garbage Collector).
Item = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}
Der Wrapper wird abgeleitet von der Basisklasse "InspectorWrapper" und implementiert eine Eigenschaft "ContactItem" welche das eigentliche Item-Objekt der Inspector-Instanz representiert. Der Konstruktor muss überschrieben, und die Inspector-Instanz dem Konstruktor der Basisklasse übergeben werden. Wenn die Basisklasse die Initialisierung beendet hat, wird eine virtuelle Methode "Initialize" aufgerufen. Diese Methode kann von den abgeleiteten Klassen überschrieben und zur weiteren Initialisierung verwendet werden. Hier wird z.B.: die Item-Eigenschaft gesetzt und sich für die Item-Benachrichtigungen registriert. Das selbe geschieht in diesem Beispiel für Termine(Appointments).
namespace InspectorWrapperExplained {
///
/// Für jede Nachrichtenklasse kann ein Wrapper mit eigener Logik definiert werden
///
internal class AppointmentItemWrapper : InspectorWrapper {
///
/// Die Termin Instanz des Inspectors (CurrentItem)
///
public Outlook.AppointmentItem Item { get; private set; }
///
/// .ctor
///
/// Die Outlook Inspector Instanz, welche behandelt warden soll
public AppointmentItemWrapper(Outlook.Inspector inspector)
: base(inspector) {
}
///
/// Methode wird aufgerufen wenn der Wrapper initialisiert wird.
///
protected override void Initialize() {
// Get the Item of the current Inspector
Item = (Outlook.AppointmentItem)Inspector.CurrentItem;
// Register for the Item events
Item.Open += new Outlook.ItemEvents_10_OpenEventHandler(Item_Open);
Item.Write += new Outlook.ItemEvents_10_WriteEventHandler(Item_Write);
}
///
/// Diese Methode wird aufgerufen, wenn das Item gespeichert wird.
///
/// Wenn Cancel auf true gesetzt wird, wird speichern abgebrochen
void Item_Write(ref bool Cancel) {
//TODO: Etwas implementieren
}
///
/// This Method is called when the Item is visible and the UI is initialized.
///
/// Wenn Cancel auf true gesetzt wird, wird der Inspector geschlossen
void Item_Open(ref bool Cancel) {
//TODO: Etwas implementieren something
}
///
/// Diese Methode wird von der Basisklasse aufgerufen, wenn der Inspector geschlossen wurde.
/// Hier müssen alle Referenzen und der SPeicher freigegeben werden.
/// Das Userinterface ist weg, kann hier nicht mehr verwendet werden.
///
protected override void Close() {
// Benachrichtigungen nicht vergessen
Item.Write -= new Outlook.ItemEvents_10_WriteEventHandler(Item_Write);
Item.Open -= new Outlook.ItemEvents_10_OpenEventHandler(Item_Open);
// Referenzen auf COM Objekte freigeben
Item = null;
// benötigt, wenn hier nur auf null gesetzt wird, bleibt eventuell eine Referenz auf den Inspector im Speicher (Garbage Collector).
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}
Hier ist eine Übersicht der Klassenhirarchie als Diagramm:

Abbildung 2: Klassendiagramm für die InspectorWrapper-Topologie
Um den richtigen InspectorWrapper für eine spezielle Nachrichtenklasse zu erzeugen, bedient man sich einer Fabrik. Diese kann in einer extra Klasse abgelegt – oder der InspectorWrapper-Klasse als statische Methode hinzugefügt warden. Der Sinn und Zweck dieser Methode ist es, die NachrichtenKlasse-Eigenschaft der "Inspector.CurrentItem"-Instanz auszulesen und einen konkreten dazu passenden Wrapper zurückzuliefern – oder null, falls kein passender Wrapper gefunden wurde.
///
/// Die Fabrik liefert die zur Nachrichtenklasse passende Wrapper Instanz – oder null wenn kein passender Wrapper gefunden wurde
///
/// Die Outlook Inspector Instanz
/// Liefert einen speziellen Wrapper oder null
public static InspectorWrapper GetWrapperFor(Outlook.Inspector inspector) {
// Die Nachrichtenklasse wird über späte Bindung (late binding) gelesen
string messageClass = inspector.CurrentItem.GetType().InvokeMember("MessageClass", BindingFlags.GetProperty, null, inspector.CurrentItem, null);
switch (messageClass) {
case "IPM.Contact":
return new ContactItemWrapper(inspector);
case "IPM.Appintment":
return new AppointmentItemWrapper(inspector);
}
return null;
}
Wie wird der Wrapper benützt?
Zuerst benötigt man die entsprechende Infrastruktur, um neue oder bereits nach dem Start von Outlook geöffneten "Inspektoren" zu bearbeiten. In der "ThisAddIn"-Klasse wird zunächst eine Variable von Typ "Outlook.Inspectors" definiert, welche eine Referenz auf die entsprechende COM-Instanz halt. Diese wird benötigt um die "NewInspector"-Nachricht zu erhalten. Die Variable wird in der "ThisAddin_StartUp"-Methode zugewiesen und die Behandlung für die "NewInspector"-Nachricht eingerichtet. Sobald die Nachricht eintrifft, möchten wir ggf. einen Wrapper erzeugen und den Inspektor bearbeiten. Hierfür definieren wir uns eine "WrapInspector"-Methode, welche einen Inspector als Argument bekommt. Die Methode kann direkt in der Nachrichtenbehandlung für die "NewInspector" Nachricht definiert werden. Die bereits nach dem Start von Outlook geöffneten Inspektoren werden in einer foreach-Schleife auf die selbe Art behandelt. Bei einer "NewInspector"-Nachricht versuchen wir einen Wrapper zu erstellen, und bei Erfolg registrieren wir uns für die "InspectorWrapper.Close" Nachricht uns merken uns die Instanz des Wrappers im Speicher. Sobald der Wrapper eine "Close" Nachricht auslöst, entfernen die die Wrapper-Instanz aus dem Speicher. So sieht die Implementierung der "ThisAddin"-Klasse aus:
using System;
using System.Collections.Generic;
using Outlook = Microsoft.Office.Interop.Outlook;
using Office = Microsoft.Office.Core;
namespace InspectorWrapperExplained
{
public partial class ThisAddIn
{
///
/// Referenz auf die Application.Inspectors Instanz
/// Wird benötigt um Benachrichtigungen erhalten
///
private Outlook.Inspectors _inspectors;
///
/// Ein Dictionary wird verwendet um eine Referenz auf die entsprechenden Wrapper Instanzen zu hslten
///
private Dictionary<Guid, InspectorWrapper> _wrappedInspectors;
///
/// Startup Methode wird aufgerufen,wenn das Add-in von Outlook geladen wird
///
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
_wrappedInspectors = new Dictionary<Guid, InspectorWrapper>();
_inspectors = Globals.ThisAddIn.Application.Inspectors;
_inspectors.NewInspector += new Outlook.InspectorsEvents_NewInspectorEventHandler(WrapInspector);
// Bereits geladene Inspektoren warden ebenfalls behandelt
// (z.B.: Doppelklick auf eine .msg Datei)
foreach (Outlook.Inspector inspector in _inspectors) {
WrapInspector(inspector);
}
}
///
/// Kapselt eine Inspector Instanz falls nötig und merkt sich die Instanz im Speicher
///
/// Die Outlook Inspector Instanz
void WrapInspector(Outlook.Inspector inspector) {
InspectorWrapper wrapper = InspectorWrapper.GetWrapperFor(inspector);
if (wrapper != null) {
// Für die Close Nachricht registrieren
wrapper.Closed += new InspectorWrapperClosedEventHandler(wrapper_Closed);
// Inspector im Speicher merken
_wrappedInspectors[wrapper.Id] = wrapper;
}
}
///
/// Methode wird aufgerufen wenn ein Inspector geschlossen wurde
/// Entfernt die Referenz aus dem Speicher
///
/// Die eindeutige ID des geschlossenen Inspektors
void wrapper_Closed(Guid id) {
_wrappedInspectors.Remove(id);
}
///
/// Methode wird aufgerufen wenn Outlook das Add-in entfernt
///
private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
{
// Aufräumarbeiten, COM Referenzen und Speicher freigeben
_wrappedInspectors.Clear();
_inspectors.NewInspector -=new Outlook.InspectorsEvents_NewInspectorEventHandler(WrapInspector);
_inspectors = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
#region VSTO generated code
///
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///
private void InternalStartup()
{
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
#endregion
}
}
Fazit
Mit der "InspectorWrapper"-Vorlage wird es einfacher mit mehreren Fenstern innerhalb der Outlook-Anwendung umzugehen. Mit relativ wenig Aufwand lassen sich verschiedene Item-Typen unterschiedlich behandeln. Ausserdem kapselt der InspectorWrapper die Nachrichtenbehandlung der Inspetoren von Benutzer und kann dazu beitragen, dass keine COM Referenzen offen bleiben.