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.

To enforce business rules, you need to begin by developing an Outlook inspector wrapper template. For more information about how to implement an inspector wrapper template, see Developing an Inspector Wrapper for Outlook 2010.

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>
internal delegate void InspectorWrapperClosedEventHandler(Guid id);

// The base class for all inspector wrappers.
internal abstract class InspectorWrapper {

    // Event notifier for the InspectorWrapper.Closed event.
    // Is raised when an inspector has been closed.
   
    public event InspectorWrapperClosedEventHandler Closed;

    // The unique ID that identifies the inspector window.
    public Guid Id { get; private set; }

    // The Outlook Inspector instance.
    public Outlook.Inspector Inspector { get; private set; }

    // .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.
    private void 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.
    protected virtual void Initialize() { }

    // Method is called when another page of the inspector has been selected.
    // <param name="ActivePageName">The active page name by reference</param>
    protected virtual void PageChange(ref string ActivePageName) { }

    // Method is called before the inspector is resized.
    // <param name="Cancel">To prevent resizing set Cancel to true</param>
    protected virtual void BeforeSize(ref bool Cancel) { }

    // Method is called before the inspector is moved around.
    // <param name="Cancel">To prevent moving set Cancel to true</param>
    protected virtual void BeforeMove(ref bool Cancel) { }

    // Method is called before the inspector is minimized.
    // <param name="Cancel">To prevent minimizing set Cancel to true</param>
    protected virtual void BeforeMinimize(ref bool Cancel) { }

    // Method is called before the inspector is maximized.
    // <param name="Cancel">To prevent maximizing set Cancel to true</param>
    protected virtual void BeforeMaximize(ref bool Cancel) { }

    // Method is called when the inspector is deactivated.
    protected virtual void Deactivate() { }

    // Method is called when the inspector is activated.
    protected virtual void Activate() { }

    // Derived classes can do a clean-up by overriding this method.
    protected virtual void 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>
    public static 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":
                return new ContactItemWrapper(inspector);
            case "IPM.Journal":
                return new ContactItemWrapper(inspector);
            case "IPM.Note":
                return new MailItemWrapper(inspector);
            case "IPM.Post":
                return new PostItemWrapper(inspector);
            case "IPM.Task":
                return new 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) {
            return new AppointmentItemWrapper(inspector);
        }

        // Or check the interface type of the item.
        if (inspector.CurrentItem is Outlook.ContactItem) {
            return new ContactItemWrapper(inspector);
        }

        // No wrapper found.
        return null;
    }
}

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).

internal class 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.
    private bool _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; private set; }

    // Method is called when the wrapper has been initialized.
    protected override void 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(ref bool 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(ref bool 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(ref bool 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.
    protected override void 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.
    private void _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>();

    private void 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));
    }

    public bool Validate() {
        // If every business rule is valid, return true.
        if (_BusinessRules.TrueForAll(X4UBusinessRule.IsRuleValid)) return true;

        // 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());

        return false;
    }

    public bool BusinessRuleCheck_OfficeLocationRequiredAndUppercase() {
        return (!string.IsNullOrEmpty (Item.OfficeLocation) && Item.OfficeLocation == Item.OfficeLocation.ToUpper());
    }

    public bool BusinessRuleCheck_CompanyNameMinimum3Letters() {
        return (Item.CompanyName != null && Item.CompanyName.Length > 2);
    }

    #endregion

}

When an item is opened and modified, the user has three choices:

  • Save the data and close.

  • Discard all changes and close.

  • Cancel the action and go back to the form.

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.
private bool _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.

// 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(ref bool 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;
    }

}
Figure 1. Message box that prompts to save changes

Message box that prompts to save changes

To close the form, a timer needs to be started, as shown in the following code. This way all references can be released, and the form can be closed later in the timer event.

// When the form should be closed by code,
// you need to use a timer to safely release the item references.
private void _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();

}

A second call to GC.Collect() is required, as described in the blog post The Designer Process that Would Not Terminate by Geoff Darst.

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>
public delegate bool 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.

// This class defines a business rule.
public class X4UBusinessRule {

    // The name of the business rule.
    public string Name { get; private set; }

    // A description displayed to the user if the rule is not met.
    public string Description { get; private set; }        
    
    // A function pointer that points to the method that checks if the business rule is valid.
    public BusinessRuleCheckMethod IsValid { get; private set; }

    // .ctor
    // <param name="name">The name</param>
    // <param name="description">The description</param>
    // <param name="ruleValidator">Function pointer to the validation method</param>
    public X4UBusinessRule(string name, string description, BusinessRuleCheckMethod ruleValidator) {
        IsValid = ruleValidator;
        Name = name;
        Description = description;
    }

    // This method executes the method that the rule points to and returns the result.
    // <param name="rule">The business rule to check</param>
    // <returns>True if the rule is valid</returns>
    public static bool IsRuleValid(X4UBusinessRule rule) {
        return rule.IsValid();
    }
}

Now you will set up the rule check in the ContactItemWrapper class. The following code defines two business rule check methods.

public bool BusinessRuleCheck_OfficeLocationRequiredAndUppercase() {
    return (!string.IsNullOrEmpty (Item.OfficeLocation) && Item.OfficeLocation == Item.OfficeLocation.ToUpper());
}

public bool BusinessRuleCheck_CompanyNameMinimum3Letters() {
    return (Item.CompanyName != null && Item.CompanyName.Length > 2);
}

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.

public bool Validate() {
    // If every business rule is valid, return true.
    if (_BusinessRules.TrueForAll(X4UBusinessRule.IsRuleValid)) return true;

    // 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());

    return false;
}

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(ref bool 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(ref bool 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>();
private void 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.

Figure 2. Message box showing that business rules are not valid

Message box showing invalid business rules

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.

Show:
© 2014 Microsoft