It seems as per Code project policy I can't distribute the exe in the installer. Some people have complained not to be able to install it. The source code is also availble here which you can import into your visual studio -
This article is divided into the following main sections for better understanding -
- Introduction
- Feature Details
- Background
- Using the Code
- Points of Interest
Introduction
Many times when I attend meetings I have faced a boring but quite necessary challenge - writing minutes of meeting. We all know how important it is to circulate meeting minutes among stakeholders, but at times it becomes an inefficient task. On top of it, writing MOM is not the end of the task - in most cases we need to schedule a next followup meeting and also distribute todo list among attendees of the meeting.
Will it not be nice if we had an in-built module inside Outlook which lets us do just that? Imagine being able to right click on a Outlook meeting and click a menu like this -
clicking this Write MOM menu opens a form, where you can write the minutes, mark attendees, schedule the next followup meeting, assign task and send the MOM to everyone - all in a single form. To save your time, this tool prepopulates lot of data into the minute body (e.g. subject, date/time, meeting room etc.).
I have created a small outlook plugin which can do all these stuffs. Here I will explain the software in further detail.
I have used some open source components for this tool along with some original work of mine. Some of the components used are YARTE texteditor, RicherTextBox texteditor.
Feature Details
This add-in will run on any outlook version starting 2010 and above. I have tested it on Outlook 2010 and 2013.
Once this plugin is installed, if you select any meeting item on our calendar and right-click on it you will see a menu option like the one shown in this screenshot here (Write MOM)-
If you click on this menu, the plugin will open a form like this -
The form is divided into three sections.
The top section is mostly auto populated which pulls information about the meeting and displays here. Many items like date,time,chair, minute writer are fetched by this tool.
All these will automatically go into the MOM when you finally submit this form.
The middle section is for scheduling a follow up meeting. The tool tries to find out whether there is already any follow up meeting scheduled on the same subject line, if not it shows an alert "next meeting on this topic is not yet scheduled. You can create the next meeting details from this screen". Here you can enter the details of the next meeting including a body and select the invitee list. Note that the invitee list has a outlook like type ahead feature to easily select invitee from your most recently used contact's list. This most recently used contact list is also calculated by this tool where it scans all your recent meetings and finds out all the invitee details from those meeting. At the end, when you submit the form, the next meeting is created on our calendar using the details you have submitted here.
The last section has two sub-sections. The left side text editor lets you write the minute's body of the current meeting. You can also attach files into the meeting minutes body using the attachment link -
On the right side there is a task assignment module. This one lets you write short description of tasks and assign to any of the people who were invited to this meeting. You can also mention the reminder setting for each task which is being assigned. To add to this, you can also attach file to each task being assigned. Once you submit the form, all these tasks are sent separately to each of these assigness. Also, the assigned tasks are mentioned in the final minutes body for everyone to see.
Once you're done with all the above you can click the submit button located at the center of the form. This will create the followup meeting, send tasks to assigness, create the final minutes of meeting and send it to the invitees of the meeting. This tool uses an inbuilt template for the MOM which can be changed as per your wish. The final MOM look like this -
Background
The source code of this tool is manily divided by these modules -
1. ThisAddin: This is a must have class for any Microsoft office addin application. This one is basically the entry point to your office application and this allows the any custom application to run with office applications.
2. The Ribon: Starting from outlook 2013, this is the only way to create a customized context menu in Outlook. We need to customize the context menu here to add the Write MOM menu.
3. Binding The Ribon with ThisAddin: We have to tell ThisAddin to use the custom context menu.
3. Main Form (MOM.cs): This is the main form which opens after the user clicks on Write MOM. The form has many important modules -
- Type ahead text box: this text box lets the user choose invitee list for the next meeting. This type ahead text box is a custom one.
- Text editors: I am using two open source text editors in this project - YARTE and Richt Text Editor.
- Create the next meeting: This module creates the next meeting with all the details which user passes in this form.
- Assign Tasks: This module creates outlook task items with attachments and sends the tasks to assigness.
- Create final MOM email using in-built template: This module consolidates all the details submitted by the user and create the MOM email and sends to all invitee of the meeting
Read on to fully understand the code -
Using the Code
Using the code is really simple. Just download the solution attached here, unzip it and open with visual studio. All required libraries are already added in the solution. Here are some important details of the code as highlighted in the background -
ThisAddin:
To understand this class its best to refer to what Mircrosoft has provided in msdn -
"When you extend a Microsoft Office application by creating a VSTO Add-in, you write code directly against the ThisAddIn class in your project. You can use this class to perform tasks such as accessing the object model of the Microsoft Office host application, customizing the user interface (UI) of the application, and exposing objects in your VSTO Add-in to other Office solutions.You can start writing your VSTO Add-in code in the ThisAddIn class. Visual Studio automatically generates this class in the ThisAddIn.vb (in Visual Basic) or ThisAddIn.cs (in C#) code file in your VSTO Add-in project. The Visual Studio Tools for Office runtime automatically instantiates this class for you when the Microsoft Office application loads your VSTO Add-in."
VSTO refers to Visual Studio Tools for Office - a propritory Microsoft technology.
Basically when we you want to create a project with talks to Microsoft office applications, you can select the project type in Visual studio -
When you select a Add-in type project, Visual studio will create the skeleton of ThisAddin.cs file. You have to write any custom contents you want inside this file.
The Ribon:
Once you download the source code attachment and unzip it, you will see a Visual studio solution. You need to open this solution using visual studio.
Once opened, you can see the following files -
These are the Ribon components for the project.
ok what is this ribon??
To add custom menu items to Outlook you have to add a component in your project which is called Ribon by Microsoft. Starting from Outlook 2013, ribon is the only way (there was another option available for custom menu till Outlook 2010) to create a custom context menu. To add a ribon you can just right click on your solution, click on add new item and select ribon -
All configuration of this Ribon component is controlled through the Ribon.xml file (in my project its Ribon1.xml).
Here is the content of our xml file -
<?xml version="1.0" encoding="UTF-8"?>
I have highlighted the most important sections here. See the contents inside the contextMenu tag. We can define as many contextmenu inside this tag. For this project, I am only concerned about the context menu which is displayed when the user right clicks on a calendar item. How does my project tells outlook that we want to add a new item to this context menu? The trick is the idMso value. Each context menu in outlook has its different idMso value set by Microsoft. The calendar context menu's idMso value is ContextMenuCalendarItem.
After mentioning the correct idMso value we have to mention the button's id, mention the eventhandler to be called when user clicks on it (ShowMOMFormOnClick), mention the eventhandler to show an image for this new button (GetIcon), and mention the evenhandler to show the text on the button (GetCustomLabel).
Now, let's take a look at the event handler which will be invoked when the user clicks the Write Mom button.
First thing is to understand the Explorer object. This object points to the top most active application currently on the desktop. As the user is currently clicking the outlook menu, the topmost application is the outlook calendar.
You can read more about the explorer here - https://msdn.microsoft.com/en-us/library/office/ff870017.aspx
Explorer object has a property called Selection, using which we can verify whether the user has selected anything or not. If the user has selected anything, then the code checks whether the selection is really an Outlook AppointmentItem or not. If its an appointment item, then the following code initializes the MOM_form - which is basically the main form where the user can write minutes and other details.
public void ShowMOMFormOnClick(Office.IRibbonControl control)
{
Explorer explorer = ThisAddIn.app.ActiveExplorer();
if (explorer != null && explorer.Selection != null && explorer.Selection.Count > 0)
{
object item = explorer.Selection[1];
if (item is AppointmentItem)
{
AppointmentItem mailItem = item as AppointmentItem;
string entryid = mailItem.EntryID;
String recpList = "";
if (mailItem.Recipients != null)
foreach (Outlook.Recipient rcp in mailItem.Recipients)
{
String modifiedRcpName = rcp.Name.IndexOf("@") < 0 ? rcp.Name + "(" + rcp.Address + ")" : rcp.Name;
recpList = recpList.Equals("") ? modifiedRcpName : recpList + "; " + modifiedRcpName;
}
new MOM_Form(entryid, ThisAddIn.calendar, ThisAddIn.ns, recpList, ThisAddIn.emailNameMappingDict, ThisAddIn.app).ShowDialog();
}
}
}
Binding the Ribon with ThisAddin:
We have to tell ThisAddin - which is the entry point of the addin to use the custom ribon which we created. This is a simple step. The following line inside the ThisAddin.cs does just that -
protected override Microsoft.Office.Core.IRibbonExtensibility CreateRibbonExtensibilityObject()
{
return new Ribbon1();
}
Main Form (MOM.cs):
The form has many important modules -
- Type ahead text box: this text box lets the user choose invitee list for the next meeting. This type ahead text box is a custom one.
- Text editors: I am using two open source text editors in this project - YARTE and Richt Text Editor.
- Create the next meeting: This module creates the next meeting with all the details which user passes in this form.
- Assign Tasks: This module creates outlook task items with attachments and sends the tasks to assigness.
- Create final MOM email using in-built template: This module consolidates all the details submitted by the user and create the MOM email and sends to all invitee of the meeting
Type Ahead Text Box:
This text box is required when the user selects invitee list for the follow up meeting. The idea is to provide the user an Outlook like type ahead feature when adding email ids to the invitee list.
Windows form does not provide any off the shelf type ahead text box. So I had to create one specifically for this purpose. The details of how I created this type ahead textbox is described in one of my previous article here - http://www.codeproject.com/Tips/881637/Type-Ahead-Suggestion-Box-using-Listbox. The end result is a textbox like this one -
As the user keys in any initial the textbox provides a suggestion list from where the user can select any email id.
How the email ids in the suggestion list populated?
At the load of the project when ThisAddin is invoked by the runtime, the method populateAddressListFromOutlook() scans through all the Outlook meetings in the calendar which occured in last 30 days. From each of these meetings, it extracts the invitee list and creates a Dictionary.
Here is an excerpt of the method populateAddressListFromOutlook() -
app = this.Application;
ns = app.GetNamespace("MAPI");
calendar = ns.GetDefaultFolder(Microsoft.Office.Interop.Outlook.OlDefaultFolders.olFolderCalendar);
String StringToCheck = "";
StringToCheck = "[Start] >= " + "\"" + startDate.ToString().Substring(0, startDate.ToString().IndexOf(" ")) + "\""
+ " AND [End] <= \"" + endDate.ToString().Substring(0, endDate.ToString().IndexOf(" ")) + "\"";
Microsoft.Office.Interop.Outlook.Items oItems = (Microsoft.Office.Interop.Outlook.Items)calendar.Items;
Microsoft.Office.Interop.Outlook.Items restricted;
oItems.Sort("[Start]", false);
oItems.IncludeRecurrences = true;
restricted = oItems.Restrict(StringToCheck);
restricted.Sort("[Start]", false);
restricted.IncludeRecurrences = true;
Microsoft.Office.Interop.Outlook.AppointmentItem oAppt = (Microsoft.Office.Interop.Outlook.AppointmentItem)restricted.GetFirst();
Dictionary<string, string=""> comboDict = new Dictionary<string, string="">();
comboDict.Add("ALL", "ALL");
x
while (oAppt != null)
{
oAppt = (Microsoft.Office.Interop.Outlook.AppointmentItem)restricted.GetNext();
if (oAppt != null)
foreach (Microsoft.Office.Interop.Outlook.Recipient rcp in oAppt.Recipients)
{
s
d
String recpDisplayString = rcp.Name.IndexOf("@") < 0 ? rcp.Name + "(" + rcp.Address + ")" : rcp.Name;
if (!comboDict.ContainsKey(recpDisplayString))
{
comboDict.Add(recpDisplayString, recpDisplayString);
if (recpDisplayString != null && !emailNameMappingDict.ContainsKey(recpDisplayString))
emailNameMappingDict.Add(recpDisplayString, rcp.Address != null ? rcp.Address : rcp.Name);
if (rcp.Name != null && !MOM_Form.autoCompleteList.Contains(recpDisplayString))
MOM_Form.autoCompleteList.Add(recpDisplayString);
}
}
}
</string,></string,>
Text editors:
I am using two open source text editors in this project - YARTE and RicherTextBox. Each of these editors offer different features which were required for this project.
When the user is creating next followup meeting and need to write the next meeting's body I am using RicherTextBox. The reason is, for some strange reason, Outlook appointment/meeting items body need to be read as rtf and not as normal string or html content. RicherTextBox allows you to easily convert the contents written inside the textbox as rtf.
On the other hand, it is most convenient to create Outlook email contents as html. So the minutes which are being created by this tool creates the minutes contents as html before finally sending the email. YARTE is an editor which easily allows to convert the contents to html. This is why I am using YARTE as the text editor when writing the minutes body.
Create the next Meeting:
Once the main form opens (MOM.cs) it tries to find out whether there is any meeting on the same subject already scheduled after this meeting date. If it does not find any such occurance a message is shown on the form -
* Next Meeting on this topic is not yet scheduled. You can create the next meeting details from this screen
If it finds any next occurance on the same subject line, it pulls the information and populates the next meeting section on the form. The method findNextOccuranceOfThisMeeting in MOM.cs takes care of all these activities.
public Microsoft.Office.Interop.Outlook.AppointmentItem findNextOccuranceOfThisMeeting(String subject, Microsoft.Office.Interop.Outlook.MAPIFolder calendar, Outlook.AppointmentItem calItem, String inviteeList)
{
Microsoft.Office.Interop.Outlook.Items oItems = (Microsoft.Office.Interop.Outlook.Items)calendar.Items;
DateTime startDate = calItem.Start;
oItems.Sort("[Start]", false);
oItems.IncludeRecurrences = true;
String StringToCheck = "";
StringToCheck = "[Start] > " + "\'" + startDate.ToString().Substring(0, startDate.ToString().IndexOf(" ") + 1).Trim() + "\'"
+ " AND [Subject] = '" + calItem.Subject.Trim() + "'";
"
// + " AND [Subject] = '" + calItem.Subject.Trim() + "'";
Microsoft.Office.Interop.Outlook.Items restricted;
restricted = oItems.Restrict(StringToCheck);
restricted.Sort("[Start]", false);
restricted.IncludeRecurrences = true;
Microsoft.Office.Interop.Outlook.AppointmentItem oAppt = (Microsoft.Office.Interop.Outlook.AppointmentItem)restricted.GetFirst();
if (oAppt != null) //Next occurance found
{
//Outlook.MailItem em=
Outlook.MailItem em = app.CreateItem(Outlook.OlItemType.olMailItem) as Outlook.MailItem;
em.HTMLBody = oAppt.Body;
label_next_meeting.Visible = false;
nextmeetingSubject.Text = oAppt.Subject.Trim();
nextMeetingLocation.Text = oAppt.Location != null ? oAppt.Location.Trim() : "";
//MemoryStream stream = new MemoryStream(oAppt.RTFBody);
nextMeetingBody.Rtf = System.Text.Encoding.ASCII.GetString(oAppt.RTFBody);
//ASCIIEncoding.Default.GetBytes(
//byte[] b =
// nextMeetingBody.Rtf=stream.
// nextMeetingBody.Html = em.HTMLBody;
String timeTemp = oAppt.Start.ToString().Substring(oAppt.Start.ToString().IndexOf(" ") + 1);
nextmeetingStartTime.Text = timeTemp.Substring(0, timeTemp.LastIndexOf(":")) + " " + (timeTemp.EndsWith("AM") ? "AM" : "PM");
timeTemp = oAppt.End.ToString().Substring(oAppt.End.ToString().IndexOf(" ") + 1);
nextmeetingEndTime.Text = timeTemp.Substring(0, timeTemp.LastIndexOf(":")) + " " + (timeTemp.EndsWith("AM") ? "AM" : "PM");
dateTimePicker_nextMeetingDate.Checked = true;
dateTimePicker_nextMeetingDate.Text = oAppt.Start.ToString();
styleInviteeList(inviteeList);
//Load the attachment list if there are already uploaded attachment details for the next meeting
if (oAppt.Attachments != null && oAppt.Attachments.Count > 0)
{
foreach (Outlook.Attachment atchItem in oAppt.Attachments)
{
dataGridView_next_meeting_atch.Rows.Add(atchItem.FileName.Trim(), "");
}
}
//textBox_next_invitees.Text = inviteeList;
//=Convert.ToDateTime(oAppt.Start.ToString().Substring(oAppt.Start.ToString().IndexOf(" ") + 1));
return oAppt;
}
else
{
label_next_meeting.Visible = true;
nextmeetingSubject.Text = subject;
styleInviteeList(inviteeList);
//textBox_next_invitees.Text = inviteeList;
//textBox_next_invitees.textfr
return null;
}
}
Assign Tasks:
On the lower right side of the form there is a section for the user to create and assign tasks to invitees of the meeting. While creating the task user can provide a short description, select a due date of the task, setup a reminder timing, attach file and assign to a invitee from the list.
All these tasks created here will be sent to the respective individual as outlook task item. Also, each of these tasks details will be written automatically inside the MOM body by this program when the final MOM is sent -
When anyonce clicks on the hyperlink on the action item list it will open the respective task details inside Outlook -
lets look at now how all these are done.
Once the user clicks the submit button inside the form one of the things that are done is creating the tasks and assigning it to the intended people. The following method assignTasks does the main work in creating the task items, creating the tasks body and sending it to individuals.
This method loops through each item inserted inside the datagridview of task list and creates one TaskItem. Each of these taskitem is sent to the intended recepient using the send() method.
This method also creates the html section of the minutes body which contains the task list.
private String assignTasks()
{
Dictionary<int, string=""> taskList = new Dictionary<int, string="">();
String returnHTML = "";
foreach (DataGridViewRow dr in dataGridView_action_items.Rows)
{
try
{
m
Outlook.TaskItem taskObj = app.CreateItem(Outlook.OlItemType.olTaskItem) as Outlook.TaskItem;
taskObj.Subject = dr.Cells[0].Value.ToString();
taskObj.Body = dr.Cells[0].Value.ToString();
taskObj.DueDate = DateTime.Parse(dr.Cells[1].Value.ToString());
;
taskObj.Assign();
taskObj.Recipients.Add(dr.Cells[4].Value.ToString());
taskObj.ReminderSet = true;
taskObj.ReminderTime = Convert.ToDateTime(taskObj.DueDate.ToShortDateString() + " " + comboBox_reminder.Text.Trim());
if (!dr.Cells[3].Value.ToString().Equals(""))
taskObj.Attachments.Add(dr.Cells[3].Value.ToString(), Outlook.OlAttachmentType.olByValue, 1, dr.Cells[3].Value.ToString());
taskObj.Send();
M
returnHTML += "<tr style='mso-yfti-irow:0;mso-yfti-firstrow:yes;mso-yfti-lastrow:yes'>" + "<td style='padding:.75pt .75pt .75pt .75pt'>" + "<p class=MsoNormal style='margin-top:3.0pt;margin-right:0cm;margin-bottom:3.0pt;margin-left:0cm'><b><span style='font-size:8.0pt;font-family:\"Book Antiqua\",\"serif\"'>" + "<a href=Outlook:" + taskObj.EntryID + ">" + taskObj.Subject + "</a>" + "</span></b></p> </td>" + "<td style='padding:.75pt .75pt .75pt .75pt'>" + "<p class=MsoNormal style='margin-top:3.0pt;margin-right:0cm;margin-bottom: 3.0pt;margin-left:0cm'><span style='font-size:8.0pt;font-family:\"Book Antiqua\",\"serif\"'>" + taskObj.Owner + "</span></b></p> </td>" + "<td style='padding:.75pt .75pt .75pt .75pt'>" + "<p class=MsoNormal style='margin-top:3.0pt;margin-right:0cm;margin-bottom: 3.0pt;margin-left:0cm'><span style='font-size:8.0pt;font-family:\"Book Antiqua\",\"serif\"'>" + taskObj.DueDate.ToShortDateString() + "</span></b></p> </td>"; +
}
catch (Exception ex)
{
MessageBox.Show("Error Sending Task" + ex.ToString(), "Error", MessageBoxButtons.OK);
}
}
return returnHTML;
}</int,></int,>
Create final MOM email using in-built template:
Once the user clicks the submit button the program tries to create the next followup meeting, assign tasks and then creates the minutes body. The MOM body is an HTML content and is created using an in-built template.
The template is stored in template.html file inside Resources.resx file.
this template can be changed to create different format of minutes email. In its present form, the template.html file has placeholrders as shown in this below screenshot -
all the placeholders (written as [...]) are strings which are replaced by proper html contents in the runtime when the user submits the form. Logic to replace the placesholders inside the template with real values are written inside the class HtmlTemplate.
This class has a method Render which does the work of replacing the placeholders with strings.
public string Render(object values)
{
string output = _html;
foreach (var p in values.GetType().GetProperties())
output = output.Replace("[" + p.Name + "]", (p.GetValue(values, null) as string).Trim() ?? string.Empty);
return output;
}
This render method is called from createMOMHtml method which basically passes the value of each placeholder along with the placeholder names. e.g. the placeholder TITLE is passed along with the meeting's name, DATEOFMEETING is passed along with the actual date of the meeting etc.
private String createMOMHtml(string taskHTML)
{
var template = new HtmlTemplate();
String attendeeList = "";
for (int i = 0; i < checkedListBox_attendee.CheckedIndices.Count; i++)
attendeeList = attendeeList.Equals("") ? checkedListBox_attendee.Items[i].ToString() : attendeeList + "; " + checkedListBox_attendee.Items[i].ToString();
var output = template.Render(new
{
TITLE = meetingname.Text.Trim(),
DATEOFMEETING = meetingDate.Text.Trim(),
STARTTIME = starttime.Text.Trim(),
LOCATION = location.Text.Trim(),
CHAIR = chair.Text.Trim(),
MINUTETAKEN = minutestaken.Text.Trim(),
ENDTIME = endtime.Text.Trim(),
SUBJECT = meetingname.Text.Trim(),
TOPIC = meetingNotes.Html,
NEXTMEETINGDATE = dateTimePicker_nextMeetingDate.Text,
NEXTMEETINGTIME = nextmeetingStartTime.Text,
NEXTMEETINGLOCATION = nextMeetingLocation.Text,
NEXTMEETINGSUBJECT = nextmeetingSubject.Text,
ACTIONITEMS = taskHTML,
ALLATTENDEES = attendeeList
});
return output;
}
Points Of Interest:
I would love to keep this project open for contribution and take it forward. An immediate improvement on this project can be to move to Metro style for the form, incorporation of EWS to get the availability of meeting rooms (while booking the next meeting) etc. Please feel free to post your feedback/ideas.
History