Multiple Instances of Custom Form Regions

In my last post, I considered the final state of the FormRegionStartup interface, with the 2 new methods introduced right at the last minute. This is barely documented (hence my post). Custom form regions in general are barely documented, and another issue that occurs to me is the problem of multiple instances.

So, you’ve written a class to implement the FormRegionStartup interface, and you know that you typically implement the BeforeFormRegionShow method to hook up the form region controls to references in your add-in. Let’s say you have a simple form region with one TextBox and one CommandButton:

Here’s how you could write your form region class, if you were keeping it very simple and only supported one instance of the form region:

public class FormRegionHelper : Outlook.FormRegionStartup

{

    private Outlook.OlkTextBox TextBox1;

    private Outlook.OlkCommandButton CommandButton1;

    public object GetFormRegionStorage(string FormRegionName, object Item, int LCID,

        Outlook.OlFormRegionMode FormRegionMode, Outlook.OlFormRegionSize FormRegionSize)

    {

        Application.DoEvents();

        switch (FormRegionName)

        {

            case "MyFormRegion":

                return Properties.Resources.MyFormRegion;

            default:

                return null;

        }

    }

    public void BeforeFormRegionShow(Outlook.FormRegion FormRegion)

    {

        try

        {

            UserForm form = FormRegion.Form as UserForm;

            Controls formControls = form.Controls;

            TextBox1 = formControls.Item("OlkTextBox1") as Outlook.OlkTextBox;

            CommandButton1 = formControls.Item("CommandButton1") as Outlook.OlkCommandButton;

            CommandButton1.Click +=

new Outlook.OlkCommandButtonEvents_ClickEventHandler(CommandButton1_Click);

        }

        catch (Exception ex)

        {

            MessageBox.Show(ex.ToString());

        }

    }

    void CommandButton1_Click()

    {

        MessageBox.Show(TextBox1.Text);

    }

    public object GetFormRegionIcon(

string FormRegionName, int LCID, Outlook.OlFormRegionIcon Icon)

    {

        throw new Exception("The method or operation is not implemented.");

    }

    public object GetFormRegionManifest(string FormRegionName, int LCID)

    {

        throw new Exception("The method or operation is not implemented.");

    }

}

In the example above, I’ve used the old technique to specify the form region manifest and icons – this is just so I can keep this listing short, because these methods are not relevant to this discussion. The point I want to focus on here is how the BeforeFormRegionShow method is implemented. Notice that I have a class field for the TextBox and the CommandButton, which I initialize in the BeforeFormRegionShow. You can see that when the user clicks the CommandButton in my form, I retrieve the Text value from the TextBox and messagebox it.

So far, so good, but remember this is a very simple example. What happens if the user has two or more instances of this particular inspector open, with this custom form region displayed? A moment’s thought should tell you that the last one wins. That is, the code above is overwriting the TextBox and CommandButton variables with the latest instance of the form region. In other words, our class that implements FormRegionStartup will only be instantiated once, but the BeforeFormRegionShow method will be called for each instance of the custom form region that the user opens.

Clearly, what we need to do is to separate out the required implementation of the FormRegionStartup interface from the references to the control instances. In other words, we need 2 classes. Specifically, we need a class that wraps the controls , independently of our class that implements FormRegionStartup.

 Here’s an example – the FormRegionControls class wraps the references to the controls on the custom form region. We'll instantiate a fresh instance of this class for each custom form region that gets opened. This way, we ensure that any UI response is specific to this instance (eg, when the user clicks our CommandButton, we can fetch the TextBox text for this same instance). The constructor takes in the form region instance as its only parameter, and fetches the controls from that. We also define a Close event, which we use to tell the parent class that this form region is closing.

public class FormRegionControls

{

    private Outlook.OlkTextBox textBox1;

    private Outlook.OlkCommandButton commandButton1;

    public event EventHandler Close;

    public FormRegionControls(Outlook.FormRegion region)

    {

        // Fetch the controls from the form, to initialize our managed references.

        UserForm form = region.Form as UserForm;

        Controls formControls = form.Controls;

        textBox1 = formControls.Item("TextBox1") as Outlook.OlkTextBox;

        commandButton1 = formControls.Item("CommandButton1") as Outlook.OlkCommandButton;

        commandButton1.Click +=

new Outlook.OlkCommandButtonEvents_ClickEventHandler(commandButton1_Click);
 region.Close += new Outlook.FormRegionEvents_CloseEventHandler(Region_Close);
}

    void commandButton1_Click()

    {

        MessageBox.Show(textBox1.Text);

    }

    void Region_Close()

    {

        // Unhook the Click event sink, clean up all OM references,

        // and notify our parent we're closing.

        commandButton1.Click -=

new Outlook.OlkCommandButtonEvents_ClickEventHandler(commandButton1_Click);

        commandButton1 = null;

        textBox1 = null;

        if (Close != null)

        {

            Close(this, EventArgs.Empty);

        }

    }

}

Here’s the modified FormRegionHelper class (the class that implements FormRegionStartup), which makes use of this controls wrapper class. We allow for multiple instances of the custom form region, so we set up a collection for them. Every time BeforeFormRegionShow gets called, we create a new instance of the controls wrapper class, passing in the specific form region the user has opened. We sink the Close event on the controls wrapper to remove this instance from the collection.

[ComVisible(true)]

public class FormRegionHelper : Outlook.FormRegionStartup

{

    // We allow for multiple instances of the custom form region, so we need

    // somewhere to cache these.

    private List<FormRegionControls> openRegions = new List<FormRegionControls>();

    public object GetFormRegionStorage(string FormRegionName, object Item, int LCID,

        Outlook.OlFormRegionMode FormRegionMode, Outlook.OlFormRegionSize FormRegionSize)

    {

        Application.DoEvents();

        switch (FormRegionName)

        {

            case "MyFormRegion":

                return Properties.Resources.MyFormRegion;

            default:

                return null;

        }

    }

    public void BeforeFormRegionShow(Outlook.FormRegion Region)

    {

        // Create a new wrapper for the form region controls, hook up the Closed

        // event, and add it to our collection.

        FormRegionControls regionControls = new FormRegionControls(Region);

        regionControls.Close += new EventHandler(regionControls_Close);

        openRegions.Add(regionControls);

    }

    void regionControls_Close(object sender, EventArgs e)

    {

        // When the user closes this form region, we remove the controls wrapper

        // from our collection.

        openRegions.Remove(sender as FormRegionControls);

  }

    public object GetFormRegionIcon(

string FormRegionName, int LCID, Outlook.OlFormRegionIcon Icon)

    {

        throw new Exception("The method or operation is not implemented.");

    }

    public object GetFormRegionManifest(string FormRegionName, int LCID)

    {

        throw new Exception("The method or operation is not implemented.");

    }

}

Even though, again, the documentation is a little thin on the ground here, in this particular case, most people would have figured this out eventually – it’s really pretty simple.