Creating a Simple Time-Reporting Tool Based on the Outlook 2010 Calendar
Summary: Learn how to create an add-in to report on time spent in tasks that are tracked in the Microsoft Outlook 2010 calendar. This add-in uses categories that you assign to appointment items for time-tracking purposes.
Applies to: Microsoft Outlook 2010
Published: April 2011
Provided by: Jonathan Fingold, SDK Bridge LLC | Angela Chu-Hatoun, Microsoft Corporation
This Visual How To describes a time-reporting add-in for Microsoft Outlook 2010 that uses the categories assigned to calendar items to summarize time that is spent in an arbitrary period. The add-in functions well for time tracking, with several important caveats (which are described in the Read It section). This Visual How To describes what the tool does and explains the add-in code, with a focus on filtering appointment items and accessing the appointment categories and associated appointment properties. This code sample that accompanies this Visual How To uses a C# Outlook add-in project written in Visual Studio 2010 and assumes that you are already familiar with C# and creating custom forms and add-ins for Outlook. The add-in has several classes of interest:
RequestSummaryForm Windows Form The following code creates and displays the Windows Form, The event handler for the Generate Report button performs some validation, prompts the user for the output file, and then generates the report. The beginning time is forced to 12:00 A.M. on the /// <summary>A form for requesting a new schedule report.</summary> /// <remarks>Collects the from and to dates, the categories on which to /// report, whether to include daily subtotals in the report, and the /// path of the file to which to save the report.</remarks> public partial class RequestSummaryForm : Form { //... private void RequestSummaryForm_Load(object sender, EventArgs e) { // Initialize the from- and to- DateTimePicker initial dates. fromDateTimePicker.Value = toDateTimePicker.Value = DateTime.Now; // Initialize the category selection CheckedListBox. categoriesCheckedListBox.SelectionMode = SelectionMode.One; categoriesCheckedListBox.Items.AddRange( GetCategoryList().ToArray()); prevCategoryIndex = categoriesCheckedListBox.SelectedIndex; ignoreDateChangeEvents = false; } /// <summary>Gets a list of category names for the current Outlook /// session.</summary> private List<string> GetCategoryList() { List<string> categories = new List<string>(); Outlook.NameSpace session = Globals.ThisAddIn.Application.Session; foreach (Outlook.Category category in session.Categories) { string trimmed = category.Name.Trim(); if (!categories.Contains(trimmed)) { categories.Add(trimmed); } } categories.Sort(); return categories; } //... private void generateReportButton_Click(object sender, EventArgs e) { if (fromDateTimePicker.Value.Date > toDateTimePicker.Value.Date) { MessageBox.Show("The to date is earlier than the from date " + "for the report. No report generated."); return; } if (categoriesCheckedListBox.CheckedItems.Count == 0) { MessageBox.Show( "No categories selected. No report generated."); return; } // Get the path for the output file to create. DialogResult result = this.saveFileDialog1.ShowDialog(); if (result == DialogResult.OK) { Cursor = Cursors.WaitCursor; // Change the reporting period to be from 12 A.M. on the start // date to 12 A.M. on the day after the end date. DateTime fromDate = fromDateTimePicker.Value.Date; DateTime toDate = toDateTimePicker.Value.Date.AddDays(1); // Create and save the time tracking report. ScheduleReportGenerator.GenerateReport( fromDate, toDate, Categories, dailySubtotalsCheckBox.Checked, saveFileDialog1.FileName); Cursor = DefaultCursor; result = MessageBox.Show("Report was generated successfully."); this.Close(); } } //... /// <summary>Gets the categories on which to report.</summary> /// <remarks>Returns the categories that are checked in the categories /// CheckedListBox.</remarks> private HashSet<string> Categories { get { HashSet<string> categories = new HashSet<string>(); foreach (string category in categoriesCheckedListBox.CheckedItems) { categories.Add(category); } return categories; } } } ScheduleReportGenerator Class In addition to defining the The Outlook AppointmentItem interface has a Categories property that returns a string that contains the category names assigned to the appointment. The /// <summary>The separator character used in the category list.</summary> private static readonly string[] listSeparator = new string[] { CultureInfo.CurrentCulture.TextInfo.ListSeparator }; /// <summary>Gets the set of categories assigned to an Outlook calendar /// item.</summary> /// <param name="appointment">The item from which to get the categories. /// </param> /// <returns>The categories assigned to the item.</returns> private static HashSet<string> GetAppointmentCategories( Outlook.AppointmentItem appointment) { if (string.IsNullOrEmpty(appointment.Categories)) { return new HashSet<string>(); } string[] rawCategories = appointment.Categories.Split( listSeparator, StringSplitOptions.RemoveEmptyEntries); HashSet<string> categories = new HashSet<string>(); foreach (string rawCategory in rawCategories) { string category = rawCategory.Trim(); if (category.Length > 0) { categories.Add(category); } } return categories; } The To iterate recurring appointments in a Calendar folder, first obtain the Items collection for the folder, sort the Items collection by the Start property, and then set the IncludeRecurrences property for the Items collection to true. Then, apply the filter to the updated collection to get the relevant appointments. To include appointments that overlap or span the reporting period, the filter includes any unique or recurring appointment that starts before the end of the report and that ends after the beginning of the report. For more information about filtering appointments, see How to: Search and Obtain Appointments in a Time Range. public static Outlook.Items GetCalendarItemsInTimeFrame( DateTime reportStart, DateTime reportEnd) { Outlook.NameSpace session = Globals.ThisAddIn.Application.Session; Outlook.Folder calendar = session.GetDefaultFolder( Outlook.OlDefaultFolders.olFolderCalendar) as Outlook.Folder; // Specify the filter this way to include appointments that // overlap with the specified date range but do not necessarily // fall entirely within the date range. // Date values in the filter must not include seconds. string filter = string.Format( "[Start] < '{0}' AND [End] > '{1}'", reportEnd.ToString("MM/dd/yyyy hh:mm tt"), reportStart.ToString("MM/dd/yyyy hh:mm tt")); // Include recurring calendar items. Outlook.Items calendarItems = calendar.Items; calendarItems.Sort("[Start]", Type.Missing); calendarItems.IncludeRecurrences = true; calendarItems = calendarItems.Restrict(filter); return calendarItems; } Because the code sample uses the comma-separated value (CSV) format to save the report data, and the appointment subject and category names can contain commas and quotation marks, which are special characters in CSV format, the following code defines the /// <summary>Returns an escaped string that represents the original /// value in CSV format.</summary> /// <param name="s">The string value to encode.</param> /// <remarks>Adds leading and trailing quotation marks and escapes /// internal quotation marks by replacing them with two consecutive /// quotation marks. This is necessary when a field contains any /// quotation marks or commas./// </remarks> private static string CsvEscape(string s) { return string.Format("\"{0}\"", s.Replace("\"", "\"\"")); } ScheduleItem Inner Class The
/// <summary>Represents an Outlook calendar item or appointment in /// the context of the report to generate.</summary> internal class ScheduleItem { private ScheduleReport report; private string subject; private DateTime start; private DateTime end; private HashSet<string> relevantCategories; private double duration; public ScheduleItem( ScheduleReport report, Outlook.AppointmentItem appointment) { this.report = report; this.subject = appointment.Subject; this.start = appointment.Start; this.end = appointment.End; this.relevantCategories = GetAppointmentCategories(appointment); this.relevantCategories.IntersectWith( report.ReportingCategories); this.duration = appointment.Duration / 60.0; } /// <summary>Returns the appointment information in CSV format. /// </summary> /// <remarks>Outputs subject, hours per category (multiple), /// (empty), reported hours, (empty), start date, start time, /// end date, end time, appointment duration.</remarks> public string ToCsvString() { StringBuilder sb = new StringBuilder(); sb.Append(CsvEscape(subject)); foreach (string category in report.ReportingCategories) { double categoryHours = relevantCategories.Contains(category) ? HoursToReportPerCategory : 0.0; sb.Append("," + categoryHours.ToString(durationFormat)); } sb.Append(",," + HoursWithinReportingPeriod.ToString(durationFormat)); sb.Append(",," + start.ToString("MM/dd/yyyy")); sb.Append("," + start.ToString("HH:mm")); sb.Append("," + end.ToString("MM/dd/yyyy")); sb.Append("," + end.ToString("HH:mm")); sb.Append("," + duration.ToString(durationFormat)); return sb.ToString(); } public string Subject { get { return subject; } } /// <summary>Gets the starting time of the appointment, clipped /// to the start date of the report.</summary> public DateTime ClippedStart { get { return (start > report.Start) ? start : report.Start; } } /// <summary>Gets the ending time of the appointment, clipped /// to the end date of the report.</summary> public DateTime ClippedEnd { get { return (end < report.End) ? end : report.End; } } public double HoursWithinReportingPeriod { get { return (ClippedEnd - ClippedStart).TotalHours; } } public HashSet<string> RelevantCategories { get { return relevantCategories; } } public double HoursToReportPerCategory { get { return (this.relevantCategories.Count > 0) ? HoursWithinReportingPeriod / this.relevantCategories.Count : 0; } } } ScheduleReport Inner Class The Again, if you are adapting this add-in for use in your environment, you will want to review the business logic and update it appropriately for your scenario. However, within this class, the appointment data is abstracted away from the original Outlook interop classes. /// <summary>Represents the data and logic for generating a time- /// tracking report.</summary> internal class ScheduleReport { /// <summary>The header row for appointment information, in CSV /// format.</summary> private readonly string csvHeader; private DateTime reportStart; private DateTime reportEnd; private List<string> reportingCategories; private List<ScheduleItem> items; private bool includeDailySubtotals; /// <summary>Initializes a new time tracking report.</summary> /// <param name="reportStart">The beginning date and time for the /// report.</param> /// <param name="reportEnd">The ending date and time for the /// report.</param> /// <param name="categories">The Outlook categories for which to /// collect data.</param> /// <param name="includeDailySubtotals">Indicates whether to /// include daily subtotals in the generated report.</param> public ScheduleReport(DateTime reportStart, DateTime reportEnd, HashSet<string> reportingCategories, bool includeDailySubtotals) { this.reportStart = reportStart; this.reportEnd = reportEnd; this.reportingCategories = new List<string>(reportingCategories); this.reportingCategories.Sort(); this.items = new List<ScheduleItem>(); this.includeDailySubtotals = includeDailySubtotals; // Construct the CSV header string. StringBuilder sb = new StringBuilder("Categories"); foreach (string category in reportingCategories) { sb.AppendFormat(",{0}", CsvEscape(category)); } sb.Append( ",,Reported Hours,,Start Date,Start Time,End Date,End Time,Duration"); csvHeader = sb.ToString(); } //... public void Add(ScheduleItem item) { if (item != null) { items.Add(item); } } internal void CreateCsvFile(string filepath) { try { using (TextWriter writer = new StreamWriter(filepath)) { GenerateReport(writer); } } catch { // Insert exception handling code here. } } private void GenerateReport(TextWriter writer) { writer.WriteLine("Hours by Category from {0} through {1}", reportStart, reportEnd); writer.WriteLine(); writer.WriteLine(csvHeader); writer.WriteLine(); if (items.Count > 0) { Dictionary<string, double> grandTotals = new Dictionary<string, double>(); foreach (string category in reportingCategories) { grandTotals[category] = 0.0; } Dictionary<int, Dictionary<string, double>> dailySubtotals = new Dictionary<int, Dictionary<string, double>>(); for (int dayOffset = 0; dayOffset <= (reportEnd - reportStart).Days; dayOffset++) { dailySubtotals[dayOffset] = new Dictionary<string, double>(); foreach (string category in reportingCategories) { dailySubtotals[dayOffset][category] = 0.0; } } foreach (ScheduleItem item in items) { AddItemHoursToTotals(grandTotals, dailySubtotals, item); } WriteReportData(writer, grandTotals, dailySubtotals); } else { writer.WriteLine("No items"); } writer.Close(); } private void WriteReportData( TextWriter writer, Dictionary<string, double> grandTotals, Dictionary<int, Dictionary<string, double>> dailySubtotals) { double totalHoursInReportedInPeriod = grandTotals.Sum(x => x.Value); WriteTotalsForPeriod( writer, grandTotals, totalHoursInReportedInPeriod); WritePercentsPerPeriod( writer, grandTotals, totalHoursInReportedInPeriod); List<int> dayOffsets = new List<int>(dailySubtotals.Keys); dayOffsets.Sort(); if (includeDailySubtotals) { WriteDailySubtotals(writer, dailySubtotals, dayOffsets); } writer.WriteLine(); WriteAppointmentDetails(writer); } //... } } This add-in is meant to demonstrate some key operations:
This add-in reports the time that is spent in appointments, based on the categories assigned to them. This methodology assumes that you create appointments in your default calendar to reflect the time that is spent on various tasks. It also assumes that you assign categories to those appointments to signify the categories of time that you want to include in the report. The report is written to a file in CSV format. To generate accurate reports by using this time reporting tool, a good practice is to represent each period of time you want to report on by one appointment and not multiple overlapping appointments. For example, if you spend an hour on work that applies to categories A and B, create one appointment for that hour and assign the appointment with categories A and B. Do not use two separate appointments for that same hour, one for category A, and the other for category B. For more information, see the Considerations for Tracking Appointments section. Example Appointments For this article, assume that you have several appointments on your calendar for the week of November 1, 2010, through November 5, 2010:
The resulting week in your calendar should resemble Figure 1. Figure 1. Example appointments Accessing the Tool To access the tool, click the Generate Report button on the Add-Ins ribbon. The reporting tool displays a Windows Form (shown in Figure 2) that lets you select the date range for the report, the categories to include in the report, and whether to include daily subtotals. Figure 2. Generate Time Tracking Report dialog box To reproduce the report from the video, first add the example appointments to your calendar. Then access the tool and enter the following on the form:
The tool displays a Select Output File dialog box (shown in Figure 3), and saves the report in CSV format. Figure 3. Select Output File dialog box Reviewing the Report To examine the report, start Excel and open the .csv file. The report data does not contain formatting, because it was saved in CSV format. In Figure 4, the first few cells of the first row have been merged, and the columns have been expanded to make the report more readable. Figure 4. Reported data The report has columns for each selected category. The report contains both period and daily totals for each category, in addition to a list of the individual appointments. Appointment detail is grouped by the appointment's start date, so the two day "Sales training" appointment appears under 11/02/2010. If an item has multiple categories assigned to it, the appointment hours are divided equally between the assigned categories, as can be seen for the Write agenda and project plan task on November 2. There is one issue about how Outlook handles categories that this article does not address. If a user assigns a category to an appointment, and later deletes the category but does not update the associated appointment, the set of categories that the Outlook session returns does not contain that particular category. However, the appointment item still reports that category as one of the categories assigned to the item. Considerations for Additional Business Logic The add-in makes some simplistic assumptions about the business logic for generating the report, and you will have to review and adapt the business logic before you use this tool. Here are some of the assumptions, together with some of the artifacts they generate.
Considerations for Tracking Appointments Because the add-in does not account for overlapping calendar items, it reports 24.5 hours for November 1. The full-day appointment Snowshoeing is 24 hours long, and the recurring Check messages item is also reported for the day, accounting for the additional half hour. To avoid this issue in general, do not include two or more overlapping appointments that contain categories on which you report. For example, if you would not include the vacation category in your report, this example would only show 0.5 hours for November 1. Alternatively, if you cancel the single occurrence of Check messages on November 1 on your calendar, this example would show 24 hours for that day. If you decide to use this add-in without modifications, a good practice is to avoid overlapping appointments that contain categories on which you are reporting. If you have a period during which you have to schedule multiple tasks, consider creating one appointment and assigning multiple categories to it, instead of creating multiple concurrent appointments, each with its own set of categories. Another adaptation would be not to use full-day or multiple-day appointments for time that you want to accurately track. For example, the Sales training appointment spans from 1:00 P.M, Tuesday, November 2, through noon on Wednesday, November 3. The add-in reports 11 hours for this appointment on November 2, and 12 hours on November 3, including non-business hours during which the training would likely not occur. Instead, track this event as two separate appointments that include only the hours in which you were in training. | Watch the video ![]() Length: 10:16 |
Note:
.jpg)
.jpg)