Wednesday, December 28, 2011

Room Finder - Outlook 2007 Add-in

Often, when we want to book a meeting room, we end up gathering all the rooms in the vicinity, removing those that are already booked, and then narrowing down our choices based on our preferred rooms. This sounds like quite a lot of manual effort each time we want to book a room. Fortunately, the Outlook APIs are good enough to allow us to automate most of this.

Introducing, the Room Finder - an add-in for Outlook 2007 users.

Sources of inspiration:
1. A large retailer's IT company that I used to work with had a Room Finder utility added in Outlook. This was back in 2008. I wasnt allowed access to the source code, though. So, I had to create one on my own, based on my experiences as an end-user.

2. Outlook 2010 has this feature. So, for those who haven't got a chance to use Outlook 2010 yet, this is a feature that you would be using once the upgrade happens. Using this add-in in Outlook 2007 makes you get used to this feature.

Steps to create the Room Finder add-in:
1. Using Visual Studio 2010, create an Outlook 2007 Add-in project

2. Add an Outlook Form Region.

Name it RoomFinderFormRegion instead of the default FormRegion1.
How would you like to create this form region?
Select 'Design a new form region'
What type of form region do you want to create?
Select 'Adjoining' type. Replacement is not supported for built-in forms.
What name, title and description do you want to use?
Name it 'Room Finder'
In which display modes should this form region appear?
Turn on the first checkbox and turn off the other 2. We only want this to be available in compose mode.
Which standard message classes will display this form region?
Turn on the first one checkbox for Appointment. Turn off the others, including the default one for Mail Message. We only want this for Appointments / Meeting Requests.
We don't need to specify any custom message classes either.
Click on Finish
(Optional) If you used the default name of FormRegion1 and want to rename it to RoomFinderFormRegion, rename it on the file instead of the class. This keeps the file and the class name in sync. For sake of completeness, there are some more regions where this name would need to be udpated. Find and replace all in the solution for 'FormRegion1'. You would find a string literal, some comments, a corresponding factory class and event handlers where this needs to be updated.
(Note) A pfx key would also be added automatically to the project. You don't need to worry about that.

3. Open the RoomFinderFormRegion in design mode. Add a normal (Windows Forms) Button from the toolbox. Change the Name and Text properties of this button to 'btnSuggest' and 'Su&ggest me a room...' respectively. The & is obviously just to make the g a hot-key so that one can get to it sooner by using the Alt-G key combination.
Ensure that you reduce any whitespace on the form region, except for the button. Resize the form region vertically. Resizing horizontally is not required, as the form region would occupy the entire width of the appointment item anyway.

4. Add a button click event handler. Include the following code in the event-handler.
            try
            {
                var item = (Outlook.AppointmentItem)this.OutlookItem;
                var mapi = Globals.ThisAddIn.Application.GetNamespace("MAPI");
                mapi.Logon();
                // get appointment time info
                DateTime startOn = item.Start;
                DateTime endOn = item.End;
                //TODO: sort rooms based on floor, user preferences & people count
                string identifiedRoom = string.Empty;
                foreach (var room in rooms)
                {
                    //get meeting room's calendar properties
                    var resource = mapi.CreateRecipient(room);
                    bool isFree = resource.AddressEntry.GetFreeBusy(startOn, endOn);
                    if (isFree)
                    {
                        identifiedRoom = room;
                        break;
                    }
                }
                mapi.Logoff();
                if (!string.IsNullOrEmpty(identifiedRoom))
                {
                    item.Resources = identifiedRoom;
                    item.Location = identifiedRoom;
                }
                else
                {
                    MessageBox.Show("No rooms found for this time period! Please consider scheduling the appointment at another time.");
                }
            }
            catch (System.Exception ex)
            {
                MessageBox.Show(string.Format("Error: {0}", ex.ToString(), "Error!", MessageBoxButtons.OK, MessageBoxIcon.Exclamation));
            }
This code won't compile yet. Complete the following steps for that.

5. Add the following class-level member variable to the RoomFinderFormRegion class.
        private string[] rooms = new string[] {
            "Conf Room 1",
            "Conf Room 2",
        };
The array needs to contain all the rooms that we are intersted in. For each room, the string can either be the display name, alias or the full SMTP e-mail address of the meeting room.

6. Add a using statement for the MessageBox.
You can see a few more that were automatically added to the RoomFinderFormRegion class. Use Remove and Sort to remove the redundant ones. You will find that these are the only ones we need for now:
using System;
using System.Windows.Forms;
using Outlook = Microsoft.Office.Interop.Outlook;

7. If you attempt to use the in-built AddressEntry.GetFreeBusy method, it doesn't feel a lot DotNetty. It returns a string, with each character being a free/busy indicator for a certain time interval, the default being 30 minutes. The appointment, on the other hand, already has the start and end datetime values specified. So, what we actual need is for the AddressEntry object to give us a method that would tell us if the resource (meeting room) is available during that time. Since the AddressEntry object doesnt have a method of that kind, we use extensions. And for that, we need to put that in a separate static class, as shown here.
    public static class AppointmentHelper
    {
        private const int DEFAULT_INTERVAL = 30;
        public static bool GetFreeBusy(this AddressEntry addressEntry, DateTime start, DateTime end)
        {
            string freeBusyInfo = addressEntry.GetFreeBusy(start.Date, DEFAULT_INTERVAL, false);
            int position = (int) start.TimeOfDay.TotalMinutes / DEFAULT_INTERVAL;
            TimeSpan ts = (end - start);
            int blocks = (int) ts.TotalMinutes / DEFAULT_INTERVAL;
            return freeBusyInfo.Substring(position, blocks).All(c => c.Equals('0'));
        }
    }

8. Build the code. Ensure that the Outlook client has been closed. You can now run/debug the code. When you create a new appointment, you can now see the new region at the bottom part of the item window.
Click on the button in this Room Finder region to invoke the code that looks for a room which is free during the time selected in the appointment. If no rooms could be identified, a corresponding message is shown. Once a room is identified from the order in which it is present in the array, the search is called off, and the room is added as a resource to the appointment item.

9. Notice that I have left a TODO comment in the code above, which states that another feature could be built into this to sort the rooms based on location/floor, user preferences & people count. For instance, you wouldn't want to book a room which is good enough for 28 people if only 3 of you are going to use it. All this logic would execute to give a sorted list of the rooms array mentioned earlier. The rest of the code remains the same.

10. It is typical to expect the list of rooms to be configured externally instead of being hard-coded as an array of strings.
Add an application configuration file to the project. Include a key named Rooms and a value having a semi-colon delimited string of conf rooms. This is what it would look like.
<configuration>
  <appSettings>
    <add key="Rooms" value="Conf Room 1; Conf Room 2"/>
  </appSettings>
</configuration>

Add a reference to System.Configuration, and add a using statement for the same namespace. Add the following code at the top of the button click event hander, making sure this is still within the try block.
                //TODO: provide a way to edit and store this from Outlook
                string roomsDelimited = ConfigurationManager.AppSettings["Rooms"];
                rooms = roomsDelimited.Split(';');
You can also remove the array initializer since it is now being done only on the button click. The array definition can remain there.
private string[] rooms;
Again, I have left a TODO comment here since managing rooms from Outlook (another button in the add-in, plus storing this data in the mailbox) would be a better approach than having to manage it thru the config file.

11. Note that once you start the project, it will publish this add-in to Outlook. Any changes that you make to the add-in reflect in Outlook during the next launch. Even if you are launching Outlook separately after that, these changes are reflected.
To remove a certain add-in locally, you can use Tools → Trust Center → Add-ins.
Click on Go, select the add-in you don't want anymore (ignore the checkboxes - those are only for enable/disable) and click on Remove.

12. To deploy/distribute & for others to install
Use the Publish wizard. This also creates the setup file.
I suggest publishing to a website so that you can put in updates to the same place.
The default behavior is for the add-in to check for updates every 7 days.

References:
http://msdn.microsoft.com/en-us/library/bb410039(v=office.12).aspx
http://www.eggheadcafe.com/microsoft/Outlook-Program-Forms/35554170/how-to-read-single-instance-of-recursive-meetings-in-outlook.aspx
http://www.tech-recipes.com/rx/1959/outlook_2007_disabling_enabling_add_ins/
http://www.codeproject.com/KB/office/Outlook_Add-in.aspx
http://msdn.microsoft.com/en-us/library/bb147704(v=office.12).aspx
http://msdn.microsoft.com/en-us/library/ms526723(v=exchg.10).aspx
http://support.microsoft.com/kb/310259

5 comments:

  1. Hi Sudhi,

    Thank you for the code.
    I am having difficulty adding the people count option to the above code.
    I am using the configuration file and everything works fine except the fact that I am selecting a big room for a few people based on availability only.
    How can I add the people count so that the right size of the room is selected?

    thanks much.

    ReplyDelete
  2. Hi codeman, thanks for taking the trouble to go thru this article and responding to it.

    People count is available from item.Recipients.Count, where from step 4 above, item is defined as
    var item = (Outlook.AppointmentItem)this.OutlookItem;
    Does this not work for you?

    ReplyDelete
  3. Hi Sudhil,

    I am still using your plug-in and wanted to know if there is away to have the program dynamically select the room based on the number of participants and available room? Right now if I change the number of participant I have to delete the room, click on the button again.

    Basically I am trying to see if I can simplify this. I think this option is there for Outlook 2010. I am using outlook 2007 with Exchange 2003.

    Any idea that you can assist me in the right direction?

    Thanks Sudhil.

    ReplyDelete
  4. Can you post the project completed in zip

    ReplyDelete
  5. I get an error:

    bool isFree = resource.AddressEntry.GetFreeBusy(startOn, endOn);

    ReplyDelete