Pieter van der Westhuizen

Connecting Outlook appointments with Freshbooks web-service data, part 4

In this, our fourth and final part on how to write an Outlook add-in for Freshbooks web-service, we’ll explore how to develop a custom form region to connect an Outlook appointment with the web-service’s Timesheet entry.

Our intended end result will look similar to the following image. You will notice how we’ve used an Add-in Express custom form region to add a Project selection button and a Task selection combo box.

Connecting Outlook appointments with the Freshbooks web-service data

We’ll also add a custom ribbon button to save the appointment as a timesheet entry to Freshbooks. The Outlook appointment above will look as follows inside Freshbooks:

The Outlook appointment integrated into the Freshbooks web-service

Importing the web-service projects and tasks into Outlook

In order to be able to give the user the ability to select a project and task for the Outlook appointment item, we first need to import some Freshbooks data into Outlook. In the previous article, we’ve created an Outlook Explorer Ribbon for the Freshbooks Projects folder and added an “Import” button to it.

The custom Outlook Explorer Ribbon for the Freshbooks Projects folder

Today, we’ll add some code to its OnClick event that will connect to the Freshbooks web-service and download all the projects into our Outlook folder.

private void adxImportFreshbooksProjectRibbonButton_OnClick(object sender, IRibbonControl control, bool pressed)
{
    Outlook.Explorer currExplorer = null;
    Outlook.Folder currFolder = null;
    Outlook.Items folderItems = null;
    Outlook._PropertyAccessor propertyAccessor = null;
    Outlook.ContactItem projectItem = null;
 
    try
    {
        currExplorer = OutlookApp.ActiveExplorer();
        currFolder = currExplorer.CurrentFolder as Outlook.Folder;
        folderItems = currFolder.Items;
        currFolder = currExplorer.CurrentFolder as Outlook.Folder;
        propertyAccessor = currFolder.PropertyAccessor;
        var msgClass = propertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x36E5001F");
 
        if (msgClass.Equals("IPM.Contact.Freshbooks.Project"))
        {
            var api = new FreshbooksApi("Your Account Name", "Your Consumer Key");
            api.UseLegacyToken("Your User Token");
 
            var projectsRequest = new ProjectsRequest();
            projectsRequest.PerPage = 100;
            var projectsResponse = api.Project.List(projectsRequest);
            foreach (var project in projectsResponse.Projects.ProjectList)
            {
                projectItem = folderItems.Add(Outlook.OlItemType.olContactItem) as Outlook.ContactItem;
                projectItem.Email1Address = project.ProjectId.Value.ToString() + "@example.com";
                projectItem.MessageClass = "IPM.Contact.Freshbooks.Project";
                projectItem.FullName = project.Name;
                projectItem.SetProperty("fb-id", project.ProjectId.Value, Outlook.OlUserPropertyType.olNumber);
                projectItem.SetProperty("fb-description", project.Description, Outlook.OlUserPropertyType.olText);
                projectItem.SetProperty("fb-rate", project.Rate, Outlook.OlUserPropertyType.olCurrency);
                projectItem.SetProperty("fb-hourbudget", project.HourBudget, Outlook.OlUserPropertyType.olCurrency);
                projectItem.SetProperty("fb-billmethod", project.BillMethod, Outlook.OlUserPropertyType.olText);
 
                var tasksXml = LoadProjectTasks(project.Tasks);
                projectItem.SetProperty("fb-tasks", tasksXml, Outlook.OlUserPropertyType.olText);
                projectItem.Save();
                Marshal.ReleaseComObject(projectItem);
            }
        }
 
    }
    finally
    {
        if (propertyAccessor != null) Marshal.ReleaseComObject(propertyAccessor);
        if (folderItems != null) Marshal.ReleaseComObject(folderItems);
        if (currFolder != null) Marshal.ReleaseComObject(currFolder);
        if (currExplorer != null) Marshal.ReleaseComObject(currExplorer);
    }
}

In the preceding code, we connected to Freshbooks and retrieved a list of projects. We then iterated through these projects and created a new Outlook Contact for each. Please note, in order for a contact to show up in an Outlook Address book, it must have an e-mail address, so we need to set the Contact Item’s Email1Address property accordingly.

We also saved the Freshbook project’s associated tasks to a custom property called, fb-tasks, on the Outlook Contact item. The task data is saved as XML inside this property. We loaded the task using the LoadProjectTasks method. The code for this method follows herewith:

private string LoadProjectTasks(ProjectTasks tasks)
{
    string xml = "";
    var api = new FreshbooksApi("Your Account Name", "Your Consumer Key");
    api.UseLegacyToken("Your User Token");
 
    foreach (var task in tasks.TaskList)
    {
        xml += "";
 
        TaskIdentity taskIdentity = new TaskIdentity();
        taskIdentity.TaskId = task.TaskId;
        var taskResponse = api.Task.Get(taskIdentity);
        xml += string.Format("<task_id>{0}  ", taskResponse.Task.TaskId);
        xml += string.Format("{0}  ", taskResponse.Task.Name);
        xml += string.Format("{0}  ", taskResponse.Task.Description);
        xml += string.Format("{0}  ", taskResponse.Task.Rate);
        xml += "";
    }
    xml += "";
    return xml;
}

Creating a form region to assign Freshbooks data to an Outlook Appointment

Next, we need to add a new form region to the bottom of the Outlook Appointment Inspector which will enable the user to select a Freshbooks Project and Task to associate with said appointment.

First, add a new ADX Outlook Form item to your Outlook add-in project.

Add a new ADX Outlook Form item to your Outlook add-in project.

Design your Outlook form to resemble the following image:

Design your Outlook form.

Add a Click event handler for the search button by double-clicking on it on the forms designer and add the following code:

private void btnSearch_Click(object sender, EventArgs e)
{
    Outlook.Application outlookApp = null;
    Outlook._Inspector currInsp = null;
    Outlook.NameSpace session = null;
    Outlook.AddressLists addressLists = null;
    Outlook.AddressList projectsList = null;
    Outlook.SelectNamesDialog selectDialog = null;
    Outlook.Recipients recipients = null;
    Outlook.Recipient recipient = null;
    Outlook.ContactItem projectContact = null;
    Outlook.AppointmentItem currAppointment = null;
    int projectId = 0;
 
    try
    {
        outlookApp = (Outlook.Application)this.OutlookAppObj;
        session = outlookApp.Session;
        addressLists = session.AddressLists;
        projectsList = addressLists["Projects"];
        currInsp = outlookApp.ActiveInspector();
        currAppointment = currInsp.CurrentItem as Outlook.AppointmentItem;
 
        if (projectsList != null)
        {
            selectDialog = session.GetSelectNamesDialog();
            selectDialog.InitialAddressList = projectsList;
            selectDialog.ShowOnlyInitialAddressList = true;
            selectDialog.AllowMultipleSelection = false;
            selectDialog.SetDefaultDisplayMode(Outlook.OlDefaultSelectNamesDisplayMode.olDefaultSingleName);
            selectDialog.Display();
            recipients = selectDialog.Recipients;
            if (recipients.Count > 0)
            {
                recipient = recipients[1];
                if (recipient != null)
                {
                    projectContact = recipient.AddressEntry.GetContact();
                    if (projectContact != null)
                    {
                        projectId = Convert.ToInt32(projectContact.GetPropertyValue("fb-id"));
                        currAppointment.SetProperty("fb-project-id", projectId, Outlook.OlUserPropertyType.olNumber);
                        txtProject.Text = projectContact.FullName;
 
                        string xml = projectContact.GetPropertyValue("fb-tasks").ToString();
                        LoadTasks(xml);
                    }
                }
            }
        }
    }
    finally
    {
        if (currInsp != null) Marshal.ReleaseComObject(currInsp);
        if (currAppointment != null) Marshal.ReleaseComObject(currAppointment);
        if (projectContact != null) Marshal.ReleaseComObject(projectContact);
        if (recipient != null) Marshal.ReleaseComObject(recipient);
        if (recipients != null) Marshal.ReleaseComObject(recipients);
        if (selectDialog != null) Marshal.ReleaseComObject(selectDialog);
        if (projectsList != null) Marshal.ReleaseComObject(projectsList);
        if (addressLists != null) Marshal.ReleaseComObject(addressLists);
        if (session != null) Marshal.ReleaseComObject(session);
    }
}

In the code above, we showed an Outlook Select Names dialog window when the user clicks on the search button. The dialog can be shown by calling the GetSelectNamesDialog method of the Outlook.NameSpace object.

Show an Outlook Select Names dialog window when the user clicks on the search button.

After the user selected a Project from the list, we saved its Freshbooks ID to a custom property on the Outlook Appointment item and we also loaded the associated list of tasks into the Task combo box. This is accomplished using the LoadTasks method provided below:

private void LoadTasks(string xml)
{
    var str = XElement.Parse(xml);
    var result = str.Elements("task");
    var tasks = new List();
    foreach (var item in result)
    {
        var task = new Task();
        task.Id = Convert.ToInt32(item.Element("task_id").Value);
        task.Name = item.Element("name").Value;
        task.Rate = Convert.ToDecimal(item.Element("rate").Value);
        tasks.Add(task);
    }
 
    cboTask.DataSource = tasks;
    cboTask.ValueMember = "Id";
    cboTask.DisplayMember = "Name";
}

We also saved the selected task to another custom property on the Appointment item, after the user has selected a new value from the combo box:

private void cboTask_SelectedValueChanged(object sender, EventArgs e)
{
    Outlook.Application outlookApp = null;
    Outlook._Inspector currInsp = null;
    Outlook.AppointmentItem currAppointment = null;
 
    try
    {
        outlookApp = (Outlook.Application)this.OutlookAppObj;
        currInsp = outlookApp.ActiveInspector();
        currAppointment = currInsp.CurrentItem as Outlook.AppointmentItem;
 
        Task selectedTask = (Task)cboTask.SelectedItem;
        currAppointment.SetProperty("fb-task-id", selectedTask.Id, Outlook.OlUserPropertyType.olNumber);
 
    }
    finally
    {
        if (currInsp != null) Marshal.ReleaseComObject(currInsp);
        if (currAppointment != null) Marshal.ReleaseComObject(currAppointment);
    }
}

Next, switch to the AddinModule designer surface and select the freshbooksFormsManager component we’ve added in the previous articles. Add a new item to the Forms Manager and set the following properties:

  • FormClassName  – Freshbooks_Addin.frmAppointment
  • InspectorItemTypes – Appointment
  • InspectorLayout – BottomSubpane

Saving an Outlook appointment to the Freshbooks web-service

Once we have all the necessary functionality in place, the final step in our process is to add a custom Ribbon button to the Outlook Appointment Inspector in order to create a new Timesheet entry in Freshbooks when the user clicks on it.

Add a new ADXRibbonTab component to the AddinModule designer surface and add a new Ribbon Group and Ribbon button to it.

Add a new Ribbon Group and Ribbon button to your custom Ribbon tab.

We want the button to be visible on the standard Event tab of the Appointment Inspector ribbon. To do this, set the ribbon’s IdMso property to TabAppointment and its Ribbons property to OutlookAppointment. Select the Ribbon Group and set the InsertAfterIdMso property to GroupActions.

Finally, add an event handler for the button’s OnClick event and add the following code to it:

private void adxSaveTimesheetRibbonButton_OnClick(object sender, IRibbonControl control, bool pressed)
{
    Outlook._Inspector currInspector = null;
    Outlook.AppointmentItem currAppointment = null;
 
    try
    {
        currInspector = OutlookApp.ActiveInspector();
        currAppointment = currInspector.CurrentItem as Outlook.AppointmentItem;
        currAppointment.Save();
 
        var api = new FreshbooksApi("Your Account Name", "Your Consumer Key");
        api.UseLegacyToken("Your User Token");
 
        var timeEntry = new TimeEntry();
        timeEntry.ProjectId = new ProjectId(ulong.Parse(currAppointment.GetPropertyValue("fb-project-id").ToString()));
        timeEntry.TaskId = new TaskId(ulong.Parse(currAppointment.GetPropertyValue("fb-task-id").ToString()));
        if (currAppointment.Body != null)
            timeEntry.Notes = currAppointment.Body;
 
        timeEntry.Hours = new TimeSpan(0, currAppointment.Duration, 0);
        timeEntry.Date = currAppointment.Start.Date;
 
        var timeEntryRequest = new TimeEntryRequest();
        timeEntryRequest.TimeEntry = timeEntry;
        api.TimeEntry.Create(timeEntryRequest);
 
        currInspector.Close(Outlook.OlInspectorClose.olSave);
    }
    finally
    {
        if (currAppointment != null) Marshal.ReleaseComObject(currAppointment);
        if (currInspector != null) Marshal.ReleaseComObject(currInspector);
    }
}

The code above will create a new Timesheet entry in Freshbooks using the Project and Task ID value stores in the Appointment’s custom properties. We also set the number of hours for the timesheet entry equal to the Appointment’s Hours property. Lastly, we automatically close the Outlook Appointment Inspector after saving the timesheet entry.

There you go! After completing the steps outlined in part one to four of this series, you’ll be well on your way in creating a fully functioning Microsoft Outlook Add-in for Freshbooks.

Thank you for reading. Until next time, keep coding!

Available downloads:

This sample Outlook add-in was developed using Add-in Express for Office and .net:

Freshbooks Outlook add-in (C#)

How to integrate Outlook add-in with the Freshbooks web-service

Post a comment

Have any questions? Ask us right now!