Enforcing Business Rules in Outlook 2010

Office 2010

Summary: This article explains how to enforce business rules in Microsoft Outlook 2010 by using an add-in created with Microsoft Office development tools in Microsoft Visual Studio 2010. The article contains a sample application that validates two fields in contact forms and displays a message box to the user if the requirements are not met.

The key to the code sample described in this article is to handle each Outlook item individually. When an item is opened within Outlook, it is monitored by the Inspector interface. With the wrapper code, it is easy to handle all the inspectors gracefully. The item behind the inspector is exposed by the Inspector.CurrentItem property. In the code sample in this article, two business rules are enforced for contacts.

A ContactItem interface exposes some events that you can use to implement a small framework that checks whether all rules are met before a contact form is closed, or when someone is going to save the contact. To achieve this functionality, you need to monitor the write and the close events of all contact items. The write and the close events of the ContactItem have a Cancel argument that is passed by reference. When you set the Cancel argument to true, the operation is interrupted.

To implement an inspector wrapper, first create a new Outlook Add-in project in Microsoft Visual Studio. (You will find the full sample code for Visual Studio 2010 at Outlook 2010: Enforcing Business Rules in the MSDN Samples Gallery.) After you create the add-in, you must implement the inspector wrapper code to handle all items individually. The following code example shows the base class, InspectorWrapper, which handles all events individually for all open inspectors. To handle ContactItem objects, you need to implement a ContactItemWrapper class that is derived from the InspectorWrapper class to handle ContactItem events and business logic.

// Eventhandler used to correctly clean up resources.// <param name="id">The unique ID of the inspector instance</param>internaldelegatevoid InspectorWrapperClosedEventHandler(Guid id);
// The base class for all inspector wrappers.internalabstractclass InspectorWrapper {
// Event notifier for the InspectorWrapper.Closed event.// Is raised when an inspector has been closed.publicevent InspectorWrapperClosedEventHandler Closed;
// The unique ID that identifies the inspector window.public Guid Id { get; privateset; }
// The Outlook Inspector instance.public Outlook.Inspector Inspector { get; privateset; }
// .ctor// <param name="inspector">The Outlook Inspector instance that should be handled</param>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);
((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);
// Only Outlook 2007((Outlook.InspectorEvents_10_Event)Inspector).PageChange += new Outlook.InspectorEvents_10_PageChangeEventHandler(PageChange);// Initialize is called to give the derived wrappers a chance to do initialization.
Initialize();
}
// Event handler for the Inspector Close event.privatevoid Inspector_Close() {
// Call the Close method - the derived classes can implement clean-up code// by overriding the Close method.
Close();
// Unregister Inspector events.
((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);
// Only Outlook 2007((Outlook.InspectorEvents_10_Event)Inspector).PageChange -= new Outlook.InspectorEvents_10_PageChangeEventHandler(PageChange);// Clean up resources and do a GC.Collect();
Inspector = null;
GC.Collect();
GC.WaitForPendingFinalizers();
// Raise the Close event.if (Closed != null) Closed(Id);
}
// Method is called after the internal initialization of the wrapper.protectedvirtualvoid Initialize() { }
// Method is called when another page of the inspector has been selected.// <param name="ActivePageName">The active page name by reference</param>protectedvirtualvoid PageChange(refstring ActivePageName) { }
// Method is called before the inspector is resized.// <param name="Cancel">To prevent resizing set Cancel to true</param>protectedvirtualvoid BeforeSize(refbool Cancel) { }
// Method is called before the inspector is moved around.// <param name="Cancel">To prevent moving set Cancel to true</param>protectedvirtualvoid BeforeMove(refbool Cancel) { }
// Method is called before the inspector is minimized.// <param name="Cancel">To prevent minimizing set Cancel to true</param>protectedvirtualvoid BeforeMinimize(refbool Cancel) { }
// Method is called before the inspector is maximized.// <param name="Cancel">To prevent maximizing set Cancel to true</param>protectedvirtualvoid BeforeMaximize(refbool Cancel) { }
// Method is called when the inspector is deactivated.protectedvirtualvoid Deactivate() { }
// Method is called when the inspector is activated.protectedvirtualvoid Activate() { }
// Derived classes can do a clean-up by overriding this method.protectedvirtualvoid Close() { }
// This factory method returns a specific InspectorWrapper or null if not handled.// <param name="inspector">The Outlook Inspector instance</param>// <returns>Returns the specific Wrapper or null</returns>publicstatic InspectorWrapper GetWrapperFor(Outlook.Inspector inspector) {
// Retrieve the message class using late binding.string messageClass = (string)inspector.CurrentItem.GetType().InvokeMember("MessageClass", BindingFlags.GetProperty, null, inspector.CurrentItem, null);
// Depending on the messageClass, you can instantiate different wrappers// explicitly for a given MessageClass by// using a switch statement.switch (messageClass) {
case"IPM.Contact":
returnnew ContactItemWrapper(inspector);
case"IPM.Journal":
returnnew ContactItemWrapper(inspector);
case"IPM.Note":
returnnew MailItemWrapper(inspector);
case"IPM.Post":
returnnew PostItemWrapper(inspector);
case"IPM.Task":
returnnew TaskItemWrapper(inspector);
}
// Or check if the messageClass begins with a specific fragment.// if (messageClass.StartsWith ("IPM.Contact.XXXX")){// return new CustomItemWrapper(inspector);//}// Or check the interface type of the item.if (inspector.CurrentItem is Outlook.AppointmentItem) {
returnnew AppointmentItemWrapper(inspector);
}
// Or check the interface type of the item.if (inspector.CurrentItem is Outlook.ContactItem) {
returnnew ContactItemWrapper(inspector);
}
// No wrapper found.returnnull;
}
}

The following code example shows the ContactItemWrapper class. It is derived from the InspectorWrapper and is used to handle all contacts in your application. How can business rules be enforced there? If you study the code, you can see that the Initialize method registers for the open, write, and close events of the ContactItem. In C#, the close event needs a special syntax, because there is already a close method for the ContactItem. To register for this event, you need to cast directly to the ItemEvents_10_Event interface. After registering the events, a method named SetupBusinessRules is called (this is discussed in more detail later in this article).

internalclass ContactItemWrapper : InspectorWrapper {
// A timer is used to close the form by code.private Timer _closeTimer;
// A flag is used to avoid releasing the item when the user cancels the Close event.privatebool _releaseItemOnClose = true;
// .ctor// <param name="inspector">The Outlook Inspector instance that should be handled</param>public ContactItemWrapper(Outlook.Inspector inspector)
: base(inspector) {
}
// The Object instance behind the inspector (CurrentItem).public Outlook.ContactItem Item { get; privateset; }
// Method is called when the wrapper has been initialized.protectedoverridevoid Initialize() {
// Get the Item of the current Inspector.
Item = (Outlook.ContactItem)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);
((Outlook.ItemEvents_10_Event)Item).Close += new Microsoft.Office.Interop.Outlook.ItemEvents_10_CloseEventHandler(Item_Close);
SetupBusinessRules();
}
// This Method is called when the item is going to be closed.// <param name="Cancel">Set Cancel to true to interrupt the action</param>void Item_Close(refbool Cancel) {
// If the item has no changes, just exit.if (Item.Saved) return;
// Display a message box to the user that asks the user to save.// Yes: save changes and close.// No: discard changes and close.// Cancel: return to the form.
DialogResult result = MessageBox.Show("Save changes?", "Test Addin", MessageBoxButtons.YesNoCancel);
switch (result) {
case DialogResult.Yes:
// Call the Validate method only when the user wants to change.if (Validate()) {
Item.Save();
} else {
Cancel = true;
}
break;
case DialogResult.No:
_releaseItemOnClose = false; // Set the release flag to false to avoid releasing the item.
Cancel = true;
// A timer is used to safely close the form.
_closeTimer = new Timer();
_closeTimer.Tick += new EventHandler(_closeTimer_Tick);
_closeTimer.Start();
break;
case DialogResult.Cancel:
// Cancel the Close event and return to the form.
Cancel = true;
break;
default:
break;
}
}
// This method is called when the item is saved.// <param name="Cancel">When set to true, the save operation is cancelled</param>void Item_Write(refbool Cancel) {
if (!Item.Saved) {
Cancel = !Validate();
}
}
// This method is called when the item is visible and the UI is initialized.// <param name="Cancel">When you set this property to true, the inspector is closed.</param>void Item_Open(refbool Cancel) {
//TODO: Implement something
}
// The Close method is called when the inspector has been closed.// Do your clean-up tasks here.// The UI is gone; cannot access it here.protectedoverridevoid Close() {
// Unregister events.
Item.Write -= new Outlook.ItemEvents_10_WriteEventHandler(Item_Write);
Item.Open -= new Outlook.ItemEvents_10_OpenEventHandler(Item_Open);
((Outlook.ItemEvents_10_Event)Item).Close -= new Microsoft.Office.Interop.Outlook.ItemEvents_10_CloseEventHandler(Item_Close);
_BusinessRules.Clear();
_BusinessRules = null;
if (_releaseItemOnClose) {
// Required; just setting to NULL may keep a reference in the memory of the garbage collector.
Item = null;
GC.Collect();
GC.WaitForPendingFinalizers();
// A second call to GC.Collect() is required as you can read in the following Geoff Darst blog entry:// http://blogs.msdn.com/geoffda/archive/2007/08/31/the-designer-process-that-would-not-terminate.aspx
GC.Collect();
}
}
// When the form should be closed by code,// you need to use a timer to safely release the item references.privatevoid _closeTimer_Tick(object sender, EventArgs e) {
_closeTimer.Stop();
_closeTimer.Dispose();
Item.Close(Outlook.OlInspectorClose.olDiscard);
// Calling GC.Collect in the Close() method does not release the item.// You need to release it here.
Item = null;
GC.Collect();
GC.WaitForPendingFinalizers();
// A second call to GC.Collect() is required as you can read in the following Geoff Darst blog entry:// http://blogs.msdn.com/geoffda/archive/2007/08/31/the-designer-process-that-would-not-terminate.aspx
GC.Collect();
}
#region business rule check methods
List<X4UBusinessRule> _BusinessRules = new List<X4UBusinessRule>();
privatevoid SetupBusinessRules() {
_BusinessRules.Add(new X4UBusinessRule("LastNameUppercase", "The Office Location is required and must be Uppercase.", this.BusinessRuleCheck_OfficeLocationRequiredAndUppercase));
_BusinessRules.Add(new X4UBusinessRule("CompanyName3Letters", "The Company Name must have a minimum length of 3.", this.BusinessRuleCheck_CompanyNameMinimum3Letters));
}
publicbool Validate() {
// If every business rule is valid, return true.if (_BusinessRules.TrueForAll(X4UBusinessRule.IsRuleValid)) returntrue;
// If not every business rule is valid,// show a warning to the user.
StringBuilder message = new StringBuilder(500);
message.AppendLine("You can't save this Item, because the following requirements are not met:");
foreach (X4UBusinessRule rule in _BusinessRules) {
if (!rule.IsValid()) message.AppendLine(rule.Description);
}
// Display a message to the user specifying what is wrong.
MessageBox.Show (new OutlookWin32Window(Inspector), message.ToString());
returnfalse;
}
publicbool BusinessRuleCheck_OfficeLocationRequiredAndUppercase() {
return (!string.IsNullOrEmpty (Item.OfficeLocation) && Item.OfficeLocation == Item.OfficeLocation.ToUpper());
}
publicbool BusinessRuleCheck_CompanyNameMinimum3Letters() {
return (Item.CompanyName != null && Item.CompanyName.Length > 2);
}
#endregion
}

To close the form appropriately and safely release all references to the Outlook item, you need to implement some logic. You need a timer for releasing the item and closing the form by code, and a Boolean flag that is used to decide whether the item should be released from memory, as shown in the following code.

// A timer is used to close the form by code.private Timer _closeTimer;
// A flag is used to avoid releasing the item when the user cancels the Close event.privatebool _releaseItemOnClose = true;

In the Item_Close() event, you first check whether the item has been modified. When there are no changes, you can close the item without further action. If the item has changes, you display a message (shown in Figure 1) to the user and ask whether the changes should be saved. The Validate() method is called only when the user has selected to save the item. The following code shows how to prompt the user to save any changes, and then closes the form.

// When the form should be closed by code,// you need to use a timer to safely release the item references.privatevoid _closeTimer_Tick(object sender, EventArgs e) {
_closeTimer.Stop();
_closeTimer.Dispose();
Item.Close(Outlook.OlInspectorClose.olDiscard);
// Calling GC.Collect in the Close() method does not release the item.// You need to release it here.
Item = null;
GC.Collect();
GC.WaitForPendingFinalizers();
// Call GC.Collect() a second time.
GC.Collect();
}

To create a business rule, you first need to define a business rule class. The class has a name for identification and a description that should be displayed to the user if the rule is not validated successfully. It also has a pointer to a function that checks the business rule and returns true if it is valid. The method signature of such a rule check is defined as a delegate, as shown in the following code.

// Delegate declares the method signature that checks the business rule and returns true or false.// <returns>True if the rule is OK</returns>publicdelegatebool BusinessRuleCheckMethod();

In the constructor of the business rule, as shown in the following code, you set up the name and description of the rule, and the pointer to the method that validates the rule. There is another static method, IsRuleValid, that just calls the function where the pointer points to and returns the result.

The first method checks whether the OfficeLocation property of the item is not empty and uppercase. The second checks whether the minimum length of the CompanyName property is three letters. You can define any kind of check—just be sure you return true when the rule is validated successfully.

To determine how and when the rules are validated, you define a Validate method that is called in the write and close events of the ContactItem.

The rules are stored in a list, so you can use the TrueForAll statement to check all rules at once. If one of the rules fails, the code displays a message to the user and returns the cause of the error. The following code shows the Validate method.

publicbool Validate() {
// If every business rule is valid, return true.if (_BusinessRules.TrueForAll(X4UBusinessRule.IsRuleValid)) returntrue;
// If not every business rule is valid,// show a warning to the user.
StringBuilder message = new StringBuilder(500);
message.AppendLine("You can't save this Item, because the following requirements are not met:");
foreach (X4UBusinessRule rule in _BusinessRules) {
if (!rule.IsValid()) message.AppendLine(rule.Description);
}
// Display a message to the user specifying what's wrong.
MessageBox.Show (new OutlookWin32Window(Inspector), message.ToString());
returnfalse;
}

The Validate method is easy to implement. It is executed in the Item.Write and Item.Close events. In this method, you can see that the Saved state of the item is checked. When the item is not modified, there is no need for a rule check. If the Validate method returns false, the write or close operation is canceled. The event handlers are defined as you can see in the following code.

// This method is called when the item is going to be closed.// <param name="Cancel"></param>void Item_Close(refbool Cancel) {
if (!Item.Saved) {
Cancel = !Validate();
}
}
// This method is called when the item is saved.// <param name="Cancel">When set to true, the save operation is canceled</param>void Item_Write(refbool Cancel) {
if (!Item.Saved) {
Cancel = !Validate();
}
}

To add function pointers to the business rule, you need to add just a single line of code. Add a new instance of a BusinessRule class to the list and pass the name, description, and the method as parameters to the constructor.

List<X4UBusinessRule> _BusinessRules = new List<X4UBusinessRule>();
privatevoid SetupBusinessRules() {
_BusinessRules.Add(new X4UBusinessRule("OfficeLocationRequiredAndUppercase", "The Office Location is required and must be uppercase.", this.BusinessRuleCheck_OfficeLocationRequiredAndUppercase));
_BusinessRules.Add(new X4UBusinessRule("CompanyName3Letters", "The Company Name must have a minimum length of 3.", this.BusinessRuleCheck_CompanyNameMinimum3Letters));
}

The code is executed whenever you modify or create a contact form and you try to save or close the form. When a rule is not valid, a message box is displayed to the user, as shown in Figure 2.

This sample described in this article makes it easy to enforce business rules in Outlook by using an add-in created with Office development tools in Visual Studio 2010. You can create rules for different types of forms and items.

Helmut Obertanner is a Microsoft Most Valuable Professional and has expertise in Microsoft Outlook and Microsoft Office development tools in Microsoft Visual Studio. Helmut maintains a professional site at www.outlooksharp.de.