Useful Refresh Command

I recently watched a video by my colleague Michael Zlatkovsky in which he demonstrates the changes to the OfficeJS Libraries. And in this video he proposed a nifty little trick I have been using ever since to refresh the task pane app without having to stop and reload the solution (which saves minutes each time you need to stop debugging). What you do is place a refresh button at the bottom of your task pane HTML, like this:

[code language=”html”]
<id="refresh-button">Refresh</button>
[/code]

Then you wire it up like this:

[code language=”javascript”]
$(‘#refresh-button’).click(function () {
location.reload();
});
[/code]

Simple eh?! Thanks Michael for the nifty tip! wlEmoticon-hotsmile.png

Simple Web Service Controller for Office Add-in

I have found Web Service Controllers in Office Add-ins to be quite useful. There are a number of reasons you might want to keep functions of your add-in on a server, including obfuscation, complex calculation, data intense, service mashups, and much more. However, every time you want to use your Web Ser4vice from the JavaScript interface, there is a lot of code associated with the AJAX call you have to make. This begs for simplicity and that is what I have done. In my App.js project, I added a simple makeAjaxCall function that takes the command you want to invoke, the parameters you want to pass it (as an array) a callback when the call is complete and a callback if there was an error. Here is the core code for the makeAjaxCall() method:

[code lang=”javascript” collapse=”true” title=”click to expand if the docs.com embedding below is not visible.”]
// Helper function to call the web service controller
app.makeAjaxCall = function (command, params, callback, error) {
var dataToPassToService = {
Command: command,
Params: params
};
$.ajax({
url: ‘../../api/WebService’,
type: ‘POST’,
data: JSON.stringify(dataToPassToService),
contentType: ‘application/json;charset=utf-8’
}).done(function (data) {
callback(data);
}).fail(function (status) {
error(status);
})
};
[/code]


// Helper function to call the web service controller
app.makeAjaxCall = function (command, params, callback, error) {
var dataToPassToService = {
Command: command,
Params: params
};
$.ajax({
url: '../../api/WebService',
type: 'POST',
data: JSON.stringify(dataToPassToService),
contentType: 'application/json;charset=utf-8'
}).done(function (data) {
callback(data);
}).fail(function (status) {
error(status);
})
};

view raw

makeAjaxCall.js

hosted with ❤ by GitHub

Here is a sample of my web service controller:

[code lang=”javascript” collapse=”true” title=”click to expand if the docs.com embedding below is not visible.”]
/// <summary>
/// CORE SERVICE FUNCTION
/// This function will take a web request which will contain the command the caller wants
/// to initate and then the paramaters needed for that call. We enter a SELECT statement
/// below to determine the command given and we then call the proper helper function
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost()]
public WebServiceResponse WebService(WebServiceRequest request)
{
WebServiceResponse response = null;
switch(request.Command)
{
case "DoFunctionA":
return functionA(request.Params);
case "DoFunctionB":
return functionB(request.Params);
case "DoFunctionC":
return functionC(request.Params);
case "DoFunctionD":
return functionD(request.Params);
}

response = new WebServiceResponse();
response.Message = "Unknown command";
return response;
}
[/code]


/// <summary>
/// CORE SERVICE FUNCTION
/// This function will take a web request which will contain the command the caller wants
/// to initate and then the paramaters needed for that call. We enter a SELECT statement
/// below to determine the command given and we then call the proper helper function
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost()]
public WebServiceResponse WebService(WebServiceRequest request)
{
WebServiceResponse response = null;
switch(request.Command)
{
case "DoFunctionA":
return functionA(request.Params);
case "DoFunctionB":
return functionB(request.Params);
case "DoFunctionC":
return functionC(request.Params);
case "DoFunctionD":
return functionD(request.Params);
}
response = new WebServiceResponse();
response.Message = "Unknown command";
return response;
}

And here is how you call it:

[code language=”javascript”]
app.makeAjaxCall("DoFunctionA", ["Value1", "Value2"], function (data) {
var result = $.parseJSON(data.Message);
// do something with the retuirned result here…
});
[/code]

For more information on how to create a Web Service controller, there is a great blog post from a colleague of mine, Michael Zlatkovsky on how to do this:

Create a web service for an app for Office using the ASP.NET Web API

[UPDATE] Docs.com (is retired)

IMPORTANT: DOCS.COM is going to be retired in December 2017.

https://support.office.com/en-us/article/Important-information-about-Docs-com-end-of-service-3b0d4877-1643-457c-9756-8caf28b94da4?ui=en-US&rs=en-US&ad=US

I am not using GIST from GitHub and updating my blogs postes previously posted with Docs.com. See this post.

Recently a new feature was added to a relatively new product Microsoft Docs.doc.  Source Code upload with context highlighting. And I can embed it here in WordPress too. So, I am including my last post on easyEWS.js:

[code lang=”javascript” collapse=”true” title=”click to expand if the docs.com embedding below is not visible.”]
/*!
* easyEWS JavaScript Library v1.0.1
* http://davecra.com
*
* Copyright David E. Craig and other contributors
* Released under the MIT license
* https://tldrlegal.com/license/mit-license
*
* Date: 2016-04-18T19:14EST
*/

var easyEws = (function () {
"use strict";

var easyEws = {};

easyEws.initialize = function () {

// PRIVATE: creates a SOAP EWS wrapper
function getSoapHeader(request) {
var result =
‘<?xml version="1.0" encoding="utf-8"?>’ +
‘<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"&#8217; +
‘ xmlns:xsd="http://www.w3.org/2001/XMLSchema"&#8217; +
‘ xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"&#8217; +
‘ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"&#8217; +
‘ xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">&#8217; +
‘ <soap:Header>’ +
‘ <RequestServerVersion Version="Exchange2013" xmlns="http://schemas.microsoft.com/exchange/services/2006/types&quot; soap:mustUnderstand="0" />’ +
‘ </soap:Header>’ +
‘ <soap:Body>’ + request + ‘</soap:Body>’ +
‘</soap:Envelope>’;
return result;
};

// PRIVATE: makes an EWS callback with promise
function asyncEws(soap, successCallback, errorCallback) {
Office.context.mailbox.makeEwsRequestAsync(soap, function (ewsResult) {
if (ewsResult.status == "succeeded") {
var xmlDoc = $.parseXML(ewsResult.value);
successCallback(xmlDoc);
} else {
if (errorCallback != null)
errorCallback(ewsResult);
}
});
};

// PUBLIC: updates the x-headers in the mail item
// RETUNS: ‘succeeded’ if call completed successfully
// SEE: https://msdn.microsoft.com/en-us/library/office/dn596091(v=exchg.150).aspx
easyEws.updateEwsHeader = function (mailItemId, headerName, headerValue, successCallback, errorCallback) {
var soap =
‘<m:UpdateItem MessageDisposition="SaveOnly" ConflictResolution="AlwaysOverwrite">’ +
‘ <m:ItemChanges>’ +
‘ <t:ItemChange>’ +
‘ <t:ItemId Id="’ + mailItemId + ‘"/>’ +
‘ <t:Updates>’ +
‘ <t:SetItemField>’ +
‘ <t:ExtendedFieldURI DistinguishedPropertySetId="InternetHeaders"’ +
‘ PropertyName="’ + headerName + ‘"’ +
‘ PropertyType="String" />’ +
‘ <t:Message>’ +
‘ <t:ExtendedProperty>’ +
‘ <t:ExtendedFieldURI DistinguishedPropertySetId="InternetHeaders"’ +
‘ PropertyName="’ + headerName + ‘"’ +
‘ PropertyType="String" />’ +
‘ <t:Value>’ + headerValue + ‘</t:Value>’ +
‘ </t:ExtendedProperty>’ +
‘ </t:Message>’ +
‘ </t:SetItemField>’ +
‘ </t:Updates>’ +
‘ </t:ItemChange>’ +
‘ </m:ItemChanges>’ +
‘</m:UpdateItem>’;

soap = getSoapHeader(soap);
// make the EWS call
asyncEws(soap, function (xmlDoc) {
successCallback("succeeded");
}, function (errorDetails) {
if (errorCallback != null)
errorCallback(errorDetails);
});
};

// PUBLIC: returns a list of items in the folder
// RETURNS: an array of ItemIds
easyEws.getFolderItemIds = function (folderId, successCallback, errorCallback) {
var soap =
‘<m:FindItem Traversal="Shallow">’ +
‘ <m:ItemShape> ‘ +
‘ <t:BaseShape>IdOnly</t:BaseShape>’ +
‘ </m:ItemShape>’ +
‘ <m:ParentFolderIds>’ +
‘ <t:FolderId Id="’ + folderId + ‘"/>’ +
‘ </m:ParentFolderIds>’ +
‘</m:FindItem>’;

var returnArray = [];
soap = getSoapHeader(soap);

// call ews
asyncEws(soap, function (xmlDoc) {
$.each(xmlDoc.getElementsByTagName("t:ItemId"), function (index, value) {
returnArray.push(value.getAttribute("Id"));
});
successCallback(returnArray);
}, function (errorDetails) {
if (errorCallback != null) {
errorCallback(errorDetails);
}
});
}

// PUBLIC: gets the details for a specific item by ID
// RETURNS: a Dictionary of key/value pairs for the mail item
easyEws.getMailItem = function(ItemId, successCallback, errorCallback) {
var soap =
‘<m:GetItem>’ +
‘ <m:ItemShape>’ +
‘ <t:BaseShape>Default</t:BaseShape>’ +
‘ <t:IncludeMimeContent>true</t:IncludeMimeContent>’ +
‘ </m:ItemShape>’ +
‘ <m:ItemIds>’ +
‘ <t:ItemId Id="’ + ItemId + ‘" />’ +
‘ </m:ItemIds>’ +
‘</m:GetItem>’;
soap = getSoapHeader(soap);
// make call to EWS
asyncEws(soap, function (xmlDoc) {
var item = new MailItem(xmlDoc);
successCallback(item);
}, function (errorDetails) {
if(errorCallback != null) {
errorCallback(errorDetails);
}
});
}

// PUBLIC: expand a group and returns all the members
// NOTE: does not enumerate groups in groups
// RETURNS: An array of Email Addresses
easyEws.expandGroup = function (group, successCallback, errorCallback) {
var soap =
‘<m:ExpandDL>’ +
‘ <m:Mailbox>’ +
‘ <t:EmailAddress>" + group + "</t:EmailAddress>’ +
‘ </m:Mailbox>’ +
‘</m:ExpandDL>’;
soap = getSoapHeader(soap);
// make the EWS call
var returnArray = [];
asyncEws(soap, function (xmlDoc) {
var extendedProps = xmlDoc.getElementsByTagName("EmailAddress");
$.each(extendedProps, function (index, value) {
returnArray.push(value);
});
successCallback(returnArray);
}, function (errorDetails) {
if (errorCallback != null)
errorCallback(errorDetails);
});
};

// PUBLIC: Find a given conversation by the ID
// RETURNS: An array of ItemID
easyEws.findConversationItems = function (conversationId, successCallback, errorCallback) {
// NOTE: search for parent:
// http://stackoverflow.com/questions/19008696/exchange-find-items-in-ews-conversation-using-xml-request
// http://www.outlookcode.com/codedetail.aspx?id=1714
// https://msdn.microsoft.com/en-us/library/office/dn610351(v=exchg.150).aspx
var soap =
‘ <m:GetConversationItems>’ +
‘ <m:ItemShape>’ +
‘ <t:BaseShape>IdOnly</t:BaseShape>’ +
‘ <t:AdditionalProperties>’ +
‘ <t:FieldURI FieldURI="item:Subject" />’ +
‘ <t:FieldURI FieldURI="item:DateTimeReceived" />’ +
‘ </t:AdditionalProperties>’ +
‘ </m:ItemShape>’ +
‘ <m:FoldersToIgnore>’ +
‘ <t:DistinguishedFolderId Id="deleteditems" />’ +
‘ <t:DistinguishedFolderId Id="drafts" />’ +
‘ </m:FoldersToIgnore>’ +
‘ <m:SortOrder>TreeOrderDescending</m:SortOrder>’ +
‘ <m:Conversations>’ +
‘ <t:Conversation>’ +
‘ <t:ConversationId Id="’ + conversationId + ‘" />’ +
‘ </t:Conversation>’ +
‘ </m:Conversations>’ +
‘ </m:GetConversationItems>’;
soap = getSoapHeader(soap);
// Make EWS call
asyncEws(soap, function (xmlDoc) {
var returnArray = [];
$.each(xmlDoc.getElementsByTagName("t:ItemId"), function (index, value) {
returnArray.push(value.getAttribute("Id"));
});
successCallback(returnArray);
}, function (errorDetails) {
if (errorCallback != null)
errorCallback(errorDetails);
});
};

// PUBLIC Gets Internet headers for a spific item
// RETURNS: a Dictionary of key value pairs
// SEE: https://msdn.microsoft.com/en-us/library/office/aa566013(v=exchg.150).aspx
easyEws.getEwsHeaders = function (itemId, successCallback, errorCallback) {
var soap =
‘ <m:GetItem>’ +
‘ <m:ItemShape>’ +
‘ <t:BaseShape>AllProperties</t:BaseShape>’ +
‘ <t:IncludeMimeContent>true</t:IncludeMimeContent>’ +
‘ </m:ItemShape>’ +
‘ <m:ItemIds>’ +
‘ <t:ItemId Id="’ + itemId + ‘" />’ +
‘ </m:ItemIds>’ +
‘ </m:GetItem>’;

soap = getSoapHeader(soap);
// Make the EWS call
var returnArray = new Dictionary();
asyncEws(soap, function (xmlDoc) {
for (var item in xmlDoc.getElementsByTagName("t:InternetMessageHeader")) {
returnArray.add(item.getAttribute("HeaderName"), item.textContent);
}
successCallback(returnArray);
}, function (errorDetails) {
if (errorCallback != null)
errorCallback(errorDetails);
});
};

// PUBLIC: updates a folder property
// RETURNS: ‘succeeded’ is process completed successfully
easyEws.updateFolderProperty = function (folderId, propName, propValue, successCallback, errorCallback) {
var soap =
‘ <m:UpdateFolder>’ +
‘ <m:FolderChanges>’ +
‘ <t:FolderChange>’ +
‘ <t:FolderId Id="’ + folderId + ‘" />’ +
‘ <t:Updates>’ +
‘ <t:SetFolderField>’ +
‘ <t:ExtendedFieldURI ‘ +
‘ DistinguishedPropertySetId="PublicStrings" ‘ +
‘ PropertyName="’ + propName + ‘" ‘ +
‘ PropertyType="String" />’ +
‘ <t:Folder>’ +
‘ <t:ExtendedProperty>’ +
‘ <t:ExtendedFieldURI ‘ +
‘ DistinguishedPropertySetId="PublicStrings" ‘ +
‘ PropertyName="’ + propName + ‘" ‘ +
‘ PropertyType="String" />’ +
‘ <t:Value>’ + propValue + ‘</t:Value>’ +
‘ </t:ExtendedProperty>’ +
‘ </t:Folder>’ +
‘ </t:SetFolderField>’ +
‘ </t:Updates>’ +
‘ </t:FolderChange>’ +
‘ </m:FolderChanges>’ +
‘ </m:UpdateFolder>’;

soap = getSoapHeader(soap);
// make the EWS call
asyncEws(soap, successCallback, errorCallback);
}

// PUBLIC: gets a folder property
// RETURNS: property value if process completed successfully
easyEws.getFolderProperty = function (folderId, propName, successCallback, errorCallback) {

var soap =
‘<m:GetFolder>’ +
‘<m:FolderShape>’ +
‘<t:BaseShape>IdOnly</t:BaseShape>’ +
‘<t:AdditionalProperties>’ +
‘<t:ExtendedFieldURI ‘ +
‘ DistinguishedPropertySetId="PublicStrings" ‘ +
‘ PropertyName="’ + propName + ‘" ‘ +
‘ PropertyType="String" />’ +
‘</t:AdditionalProperties>’ +
‘</m:FolderShape>’ +
‘<m:FolderIds>’ +
‘<t:FolderId Id="’ + folderId + ‘"/>’ +
‘</m:FolderIds>’ +
‘</m:GetFolder>’;
soap = getSoapHeader(soap);
// make the EWS call
asyncEws(soap, function(xmlDoc) {
successCallback(xmlDoc.getElementsByTagName("t:Value")[0].textContent);
}, errorCallback);
}

// PUBLIC: Gets the folder id by the given name from the store
// RETURNS: a string with ID of the folder
easyEws.getFolderId = function (folderName, successCallback, errorCallback) {
var soap =
‘ <m:GetFolder>’ +
‘ <m:FolderShape>’ +
‘ <t:BaseShape>IdOnly</t:BaseShape>’ +
‘ </m:FolderShape>’ +
‘ <m:FolderIds>’ +
‘ <t:DistinguishedFolderId Id="’ + folderName + ‘" />’ +
‘ </m:FolderIds>’ +
‘ </m:GetFolder>’;
soap = getSoapHeader(soap);
// make EWS callback
asyncEws(soap, function (xmlDoc) {
var id = xmlDoc.getElementsByTagName("t:FolderId")[0].getAttribute("Id");
successCallback(id);
}, function (errorDetails) {
if (errorCallback != null)
errorCallback(errorDetails);
});
}
}

return easyEws;

})();

/* HELPER FUNCTIONS AND CLASSES */
function MailItem(value) {

this.value = value || {};

MailItem.prototype.MimeContent = function () {
return this.value.getElementsByTagName("t:MimeContent")[0].textContent;
};

MailItem.prototype.MimeContent.CharacterSet = function () {
return this.value.getElementsByTagName("t:MimeContent")[0].getAttribute("CharacterSet");
};

MailItem.prototype.Subject = function () {
return this.value.getElementsByTagName("t:Subject")[0].textContent;
};
}

function Dictionary(values) {
this.values = values || {};

var forEachIn = function (object, action) {
for (var property in object) {
if (Object.prototype.hasOwnProperty.call(object, property))
action(property, object[property]);
}
};

Dictionary.prototype.containsKey = function (key) {
return Object.prototype.hasOwnProperty.call(this.values, key) &&
Object.prototype.propertyIsEnumerable.call(this.values, key);
};

Dictionary.prototype.forEach = function (action) {
forEachIn(this.values, action);
};

Dictionary.prototype.lookup = function (key) {
return this.values[key];
};

Dictionary.prototype.add = function (key, value) {
this.values[key] = value;
};

Dictionary.prototype.length = function () {
var len = 0;
forEachIn(this.values, function () { len++ });
return len;
};
};
[/code]

https://docs.com/david-craig/5089/easyews

Please let me know what you think.

UPDATE: By the way, if you want to embed DOCS.COM links in your blog posts on WordPress, DO NOT use the Embed option on your listing. Instead just copy the raw link and past it into your page. WordPress with automagically convert it for you. wlEmoticon-hotsmile.png

easyEWS.js for Outlook Add-ins

If you have done any work with Outlook Add-ins using the Office JavaScript API’s, you might have found a nifty function that allows you to poll the Exchange Server using EWS calls. The function: makeEwsRequestAsync(). However, this function is not the easiest thing to use. You have to formulate an EWS SOAP message that you send to the service. Getting those correct, writing the code for them, and processing the results are a real beast. But, even worse is finding exactly how to formulate the SOAP message from the existing documentation. It is something I personally did NOT look forward to as I was working on my customers solutions.

My frustration is your benefit (I hope). I created a JavaScript class called easyEws, that makes certain calls very easy. I posted the project on GitHub (previously, I had posted this on CodePlex):

https://github.com/davecra/easyEWS

I have attempted to make the functions a lot easier to use. Here are a few examples:

This code will get you the folder ID for the Drafts folder:

[code language=”javascript”]
easyEws.getFolderId(&quot;drafts&quot;, function (value) {
app.showNotification(&quot;Drafts folder ID: &quot; + value);
});

[/code]

Or, this example which will connect to the Inbox and tell you how many items are there:

[code language=”javascript”]
easyEws.getFolderId(&quot;inbox&quot;, function (folderId) {
easyEws.getFolderItemIds(folderId, function (arrayOfIDs) {
app.showNotification(&quot;There are &quot; + arrayOfIDs.length + &quot; items.&quot;);
});
});
[/code]

easyEWS has the following commands that encapsulates the makeEwsRequestAsync() calls and the SOAP messages:

  • expandGroup: one dimensional expansion of a group (does not do groups within group expansions).
  • findConversationItems: returns a list of mail items that all share the same conversationId.
  • getEwsHeaders: gets a list of X-Headers in the mail message.
  • getFolderId: returns the folder ID for a named folder, like “Drafts”, “Inbox”, etc.
  • getFolderItemIds: returns a list of mail item IDs in a given folder.
  • getFolderProperty: gets a named property from a folder.
  • getMailItem: returns a mail item from the given Id.
  • updateEwsHeader: Updated the named x-header in the message.
  • updateFolderProperty: Updates the property of a folder by the given ID.

 

Determine Compose Message Type

In working with a customer on a new Mail App for OWA, they had a requirement to determine if the mail item being composed is a Reply or Forward or new mail message. Determining is New or Reply/Forward is easy. But getting the Reply/Forward determination is unfortunately… too easy. wlEmoticon-disappointedsmile.png Meaning the ONLY way I have been able to determine this is to look at the subject and see if there is a RE: or FW: in there. It’s a little ugly, but this is how it is. And worse – it is language dependent. If you need to support multiple languages you will have to determine the language and then make this function – much larger. So here is the English only version:

[code lang=”javascript” collapse=”true” title=”click to expand if the github.com embedding below is not visible.”]
/// getMailType()
/// This function determines the type of email item currently being composed
/// – If it is a new item, it returns "New"
/// – If it is a reply, it return "Reply"
/// – If it is a Forward it returns "Forward"
/// – And if it cannot determine, it returns "UnknownReplyOrForward"
/// This accepts a function that is called with the resulting value.
function getMailType(returnFunction) {
// get the conversation ID – if it exists
var id = Office.cast.item.toItemCompose(Office.context.mailbox.item).conversationId;
if (id == null) {
// We have a new item
returnFunction("New");
return;
}
else {
// at this point we know we have a reply or forward. Now we determine which on it is.
// we do this by getting the SUBJECT and then – yes – we look and see if it is a
// RE: or FW: or unknown.
Office.cast.item.toItemCompose(Office.context.mailbox.item)
.subject.getAsync(function (result) {
var subject = result.value;
// now this sucks, but the only way to do this is look at the
// beginning of the subject and see it if it RE or FWD and
// even worse, this is language specific…
// and worse yet – if the user changed it, we have no idea
if(subject.toString().toUpperCase.startsWith("RE:")){
returnFunction("Reply");
}
else if(subject.toString().toUpperCase.startsWith("FW:")){
returnFunction("Forward");
}
else {
returnFunction("UnknownReplyOrForward");
}
});
}
}
[/code]


/// getMailType()
/// This function determines the type of email item currently being composed
/// – If it is a new item, it returns "New"
/// – If it is a reply, it return "Reply"
/// – If it is a Forward it returns "Forward"
/// – And if it cannot determine, it returns "UnknownReplyOrForward"
/// This accepts a function that is called with the resulting value.
function getMailType(returnFunction) {
// get the conversation ID – if it exists
var id = Office.cast.item.toItemCompose(Office.context.mailbox.item).conversationId;
if (id == null) {
// We have a new item
returnFunction("New");
return;
}
else {
// at this point we know we have a reply or forward. Now we determine which on it is.
// we do this by getting the SUBJECT and then – yes – we look and see if it is a
// RE: or FW: or unknown.
Office.cast.item.toItemCompose(Office.context.mailbox.item)
.subject.getAsync(function (result) {
var subject = result.value;
// now this sucks, but the only way to do this is look at the
// beginning of the subject and see it if it RE or FWD and
// even worse, this is language specific…
// and worse yet – if the user changed it, we have no idea
if(subject.toString().toUpperCase.startsWith("RE:")){
returnFunction("Reply");
}
else if(subject.toString().toUpperCase.startsWith("FW:")){
returnFunction("Forward");
}
else {
returnFunction("UnknownReplyOrForward");
}
});
}
}

view raw

getMailType.js

hosted with ❤ by GitHub

And to test this, I just created a button on my Compose App task pane, that runs the following code:

[code language=”javascript”]
$(‘#getMailType’).click(function () {
getMailType(function (result) {
app.showNotification("This is a: " + result);
});
});
[/code]

[UPDATE] In which Application is my Add-in running?

UPDATE: The code outlined in this post will no longer work with Office Online. See this post for more information: http://stackoverflow.com/a/32851938/678505. Specifically, this block:

Update Dec 5, 2016: We will soon be releasing an API to detect the platform info (partially in response to the fact that the _host_info URL paramater, which folks had unofficially relied on, needed to be recently removed for Office Online). We also have a temporary workaround in anticipation of the forthcoming official API. See “In Excel Online, OfficeJS API is not passing the host_Info_ parameter anymore to Excel Add-In” for information on the API and workaround.

Original post:

In case you missed the news, there was an update to the Office API’s. Version 1.2 was announced at //build and released 2 weeks ago. This API enhancement gets us closer to parity with the native object models used in VBA and VSTO. One interesting aspect of the new Office add-ins (formally known as Apps for Office) is how you can use the same codebase across all the applications. However, as the API becomes richer they are also becoming more product specific. Meaning, there is now the Excel.run() and Word.run() commands (PowerPoint is a little lagging in this area, but it is supposedly on the horizon).  These allow you to do Excel Workbook (or Word document) context specific commands. And there is a lot of other goodies (like promises). But, I digress… wlEmoticon-hotsmile.png

I was working on a proof of concept for my customer when I found that I needed to use the same codebase for a lot of the same work, but in some cases I needed to do something specific in Word and in another, something specific in Excel. I came up with the following function that I placed in my App.js file:

[code lang=”javascript” collapse=”true” title=”click to expand if the docs.com embedding below is not visible.”]
var current;
app.hostTypes = { Word :"Word", PowerPoint :"PowerPoint", Excel :"Excel"};
app.getHost = function () {
if (current == null) {
if (Office.context.requirements.isSetSupported(‘WordApi’)) {
current = app.hostTypes.Word;
} else if (Office.context.requirements.isSetSupported(‘ExcelApi’)) {
current = app.hostTypes.Excel;
} else {
var host = $.urlParam("_host_Info");
if (host.toLowerCase().indexOf("word",0) &gt;= 0) {
current = app.hostTypes.Word;
} else if (host.toLowerCase().indexOf("excel",0) &gt;= 0) {
current = app.hostTypes.Excel;
} else {
current = app.hostTypes.PowerPoint;
}
}
return current;
}
else {
return current;
}
};
[/code]


var current;
app.hostTypes = { Word :"Word", PowerPoint :"PowerPoint", Excel :"Excel"};
app.getHost = function () {
if (current == null) {
if (Office.context.requirements.isSetSupported('WordApi')) {
current = app.hostTypes.Word;
} else if (Office.context.requirements.isSetSupported('ExcelApi')) {
current = app.hostTypes.Excel;
} else {
var host = $.urlParam("_host_Info");
if (host.toLowerCase().indexOf("word",0) >= 0) {
current = app.hostTypes.Word;
} else if (host.toLowerCase().indexOf("excel",0) >= 0) {
current = app.hostTypes.Excel;
} else {
current = app.hostTypes.PowerPoint;
}
}
return current;
}
else {
return current;
}
};

view raw

getHost.js

hosted with ❤ by GitHub

To use this you would do this for Word:

[code language=”javascript”]
/// WORD
if (app.getHost() == app.hostTypes.Word) {
// Word specific code where
}
[/code]

Or, this for Excel:

[code language=”javascript”]
/// EXCEL
if (app.getHost() == app.hostTypes.Excel) {
// Excel specific code here
}
[/code]

Debugging with Office Online

Recently I began working on an Office Add-in (formally known as Apps for Office) that interfaces with Excel, PowerPoint and Word online. By default, when you create a new Office Add-in in Visual Studio 2015, it will default to using the installed Office Desktop Client. I have done a lot of work with Mail Apps, but this was the first time I really delved deeply into Office Add-ins with the express need to design against the online versions. After pulling my hair out looking for the settings in the debug tab of the Manifest Project settings, I found this in the Properties page of the Manifest Project file:

When you select the project at the top, you see these properties in the Properties window (F4).

Simply change these as such:

You then specify the URL to your Office 365 subscription. To get this, I logged into my Office 365 developer account from http://office365.com selected my OneDrive folder from the menu and the copied the full path from the address bar and placed it in the field. I got a prompt to log in and then all was good.

The next problem I had was with side loading the application. When I tried to Debug, I got this error in Visual Studio:

Error occurred in deployment step ‘Install app for SharePoint’

According to the documentation and everything I can find, you should not get this error if you are using an Office 365 Developer account. I am, and I am still getting this error. So, I had to go into my account and enable Side loading. I searched and searcha nd then found this blog by Tobias Lekman: https://blog.lekman.com/2012/11/sharepoint-2013-sideloading-of-apps-is.html. He gives two simple enough sounding steps:

  1. Download and install the SharePoint Online Management Shell for PowerShell
  2. Download the script Sideload.ps1 and execute it within the SharePoint Online Management Shell.

Sounds easy. So, I downloaded and installed this: https://www.microsoft.com/en-us/download/details.aspx?id=30359.

Then, I downloaded script and placed it right in my PowerShell folder: http://lekman.codeplex.com/releases/view/98505.

Now, I am the first to admit, I am a developer and not a PowerShell scripter. So I have next to no experience with PowerShell. When I ran the script I got an error:

Essentially there is an execution policy preventing my PowerShell script from running. So, I went to the link provided: https://technet.microsoft.com/library/hh847748.aspx. From there I found I needed to run this command:

After, I did that I was able to run the Sideload.ps file, it asked for my url, username and password and then setup my side to allow the side load for testing.

When I returned to Visual Studio and clicked Run, it ran and installed my app.

Updating X-Headers using EWS in Mail Apps

UPDATE (3/1/2021): This solution probably does not work with the updated Office templates that do not have an “app” object defined. You would need to add code like this:

[code language="javascript"]
var app = {};
[/code]

A better option is to use the “updateEwsHeader()” function from my easyEws library.

Transport headers are a very useful tool to help pass application settings from one user mailbox to another. It is also useful in some rules on transport and edge servers to determine how to treat a specific message. While manipulating these headers is easier in Outlook (full client) doing this from a Mail App is a little less clear.

The following code is how I managed to get this to work. First, you have to SAVE the mail item so that you can get a EWS ID. Once you get the ID, we then build the soap message (I declared this in the ‘app’ namespace). Once we have the SOAP message we then make the EWS call.

NOTE: I am using the window.alert() which you can find how to use here.

[code language="javascript"]
    function setXHeader() {

        // first save the item
        Office.cast.item.toItemCompose(Office.context.mailbox.item).saveAsync(function (composeId)
        {
            var id = composeId.value;

            // update the x-headers
            var xml = app.updateXHeaderSoap(id, "X-Test", "ValueHere");
            Office.context.mailbox.makeEwsRequestAsync(xml, function (ewsResult) {
                if (ewsResult.status != "succeeded") {
                    window.alert("Unable to attach x-headers.\n" + ewsResult.error.message);
                }
            });
        });
    }

        // updates the x-headers in the mail item
        // SEE: https://msdn.microsoft.com/en-us/library/office/dn596091(v=exchg.150).aspx
        app.updateXHeaderSoap = function (id, property, value) {
            var soap =
                '&amp;amp;amp;lt;UpdateItem MessageDisposition=&amp;amp;amp;quot;SaveOnly&amp;amp;amp;quot; ConflictResolution=&amp;amp;amp;quot;AlwaysOverwrite&amp;amp;amp;quot;' +
                '            xmlns=&amp;amp;amp;quot;http://schemas.microsoft.com/exchange/services/2006/messages&amp;amp;amp;quot;&amp;amp;amp;gt;' +
                '   &amp;amp;amp;lt;ItemChanges&amp;amp;amp;gt;' +
                '       &amp;amp;amp;lt;t:ItemChange&amp;amp;amp;gt;' +
                '           &amp;amp;amp;lt;t:ItemId Id=&amp;amp;amp;quot;' + id + '&amp;amp;amp;quot;/&amp;amp;amp;gt;' +
                '           &amp;amp;amp;lt;t:Updates&amp;amp;amp;gt;' +
                '               &amp;amp;amp;lt;t:SetItemField&amp;amp;amp;gt;' +
                '                   &amp;amp;amp;lt;t:ExtendedFieldURI DistinguishedPropertySetId=&amp;amp;amp;quot;InternetHeaders&amp;amp;amp;quot;' +
                '                                       PropertyName=&amp;amp;amp;quot;' + property + '&amp;amp;amp;quot;' +
                '                                       PropertyType=&amp;amp;amp;quot;String&amp;amp;amp;quot; /&amp;amp;amp;gt;' +
                '                   &amp;amp;amp;lt;t:Message&amp;amp;amp;gt;' +
                '                       &amp;amp;amp;lt;t:ExtendedProperty&amp;amp;amp;gt;' +
                '                           &amp;amp;amp;lt;t:ExtendedFieldURI DistinguishedPropertySetId=&amp;amp;amp;quot;InternetHeaders&amp;amp;amp;quot;' +
                '                                               PropertyName=&amp;amp;amp;quot;' + property + '&amp;amp;amp;quot;' +
                '                                               PropertyType=&amp;amp;amp;quot;String&amp;amp;amp;quot; /&amp;amp;amp;gt;' +
                '                           &amp;amp;amp;lt;t:Value&amp;amp;amp;gt;' + value + '&amp;amp;amp;lt;/t:Value&amp;amp;amp;gt;' +
                '                       &amp;amp;amp;lt;/t:ExtendedProperty&amp;amp;amp;gt;' +
                '                   &amp;amp;amp;lt;/t:Message&amp;amp;amp;gt;' +
                '               &amp;amp;amp;lt;/t:SetItemField&amp;amp;amp;gt;' +
                '           &amp;amp;amp;lt;/t:Updates&amp;amp;amp;gt;' +
                '       &amp;amp;amp;lt;/t:ItemChange&amp;amp;amp;gt;' +
                '   &amp;amp;amp;lt;/ItemChanges&amp;amp;amp;gt;' +
                '&amp;amp;amp;lt;/UpdateItem&amp;amp;amp;gt;';

            return app.getSoapHeader(soap);
        };

 app.getSoapHeader = function(request) {
         var result =
             '<?xml version="1.0" encoding="utf-8"?>' +
             '<soap:Envelope xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance' +
             '               xmlns:xsd=http://www.w3.org/2001/XMLSchema' +
             '               xmlns:m=http://schemas.microsoft.com/exchange/services/2006/messages' +
             '               xmlns:soap=http://schemas.xmlsoap.org/soap/envelope/' +
             '               xmlns:t=http://schemas.microsoft.com/exchange/services/2006/types>' +
             '   <soap:Header>' +
             '       <RequestServerVersion Version="Exchange2013" xmlns=http://schemas.microsoft.com/exchange/services/2006/types soap:mustUnderstand="0" />' +
             '   </soap:Header>' +
             '   <soap:Body>' + request + '</soap:Body>' +
             '</soap:Envelope>';
         return result;
     };
[/code]

Working with SharePoint Lists

I have been getting more and more requests to integrate Visual Studio Tools for Office solutions with SharePoint. As such I find myself connecting to SharePoint Lists and grabbing files and uploading files, etc. The following set of functions are a small subset (but most the most generic), that I can provide for now. The primary function here is CopyToSharePoint. This function does exactly what it sounds like. You provide the URL to the site, the List name you want to copy to, the local path and filename of the file you want to upload.

This does NOT work with Office 365 SharePoint lists. Those require a special project you can download from MSDN to get you the proper SharePoint context for the Microsoft Cloud. The following example only works with on-premises SharePoint installs.

NOTE: I will be working on cleaning up all my SharePoint integration code so it is generic (detached from its specific solutions), and attach it here as a full solution.




[sourcecode language=”csharp”]
/// <summary>
/// Copies the given file to the SharePoint site
/// </summary>
/// <param name="PstrSite">The URL to the site: http://sharepoint/site </param>
/// <param name="PstrList">The name of the list: Documents </param>
/// <param name="PstrLocalPath">The local path of the file to be copied: c:\test\test.docx</param>
/// <param name="PstrLocalName">The local name of the file: text.docx</param>
public static void CopyToSharePoint(string PstrSite, string PstrList, string PstrLocalPath, string PstrLocalName)
{
try
{
SPContext LobjContext = GetSharePointContext();
SPList LobjList = GetSharePointList(PstrSite, PstrList);
SPFolder LobjFolder = LobjList.RootFolder;
LobjContext.Load(LobjFolder);
LobjContext.ExecuteQuery();
string LstrRelativeUrl = LobjFolder.ServerRelativeUrl + "/" + PstrLocalName;
// copy the temp file
using (var LobjFs = new FileInfo(PstrLocalPath).OpenRead())
{
SP.File.SaveBinaryDirect(Common.GobjClientContext, LstrRelativeUrl, LobjFs, true);
}
}
catch (Exception PobjEx)
{
MessageBox.Show(PobjEx.ToString());
}
}

/// <summary>
/// Get the SharePoint context
/// </summary>
/// <returns></returns>
public static SP.Context GetSharePointContext(string PstrSite)
{
// get the current context
SPClientContext LobjContext = new SPClientContext(PstrSite);
// do we have a cached credential?
if (MobjCredential == null)
{
// no – ask the user to sign in or use current
CredentialsForm LobjCreds = new CredentialsForm();
if (LobjCreds.ShowDialog() == DialogResult.OK)
{
if (LobjCreds.UseCurrent)
{
MobjCredential = CredentialCache.DefaultNetworkCredentials;
}
else
{
MobjCredential = new NetworkCredential(LobjCreds.UserName,
LobjCreds.Password,
LobjCreds.Domain);
}
}
else
{
PbolUserCancel = true;
return null; // cancel
}
}
LobjContext.Credentials = MobjCredential;
return LobjContext;
}

/// <summary>
/// Access the list
/// </summary>
/// <returns></returns>
public static SP.List GetSharePointList(string PstrSite, string PstrTitle)
{
try
{
SPList LobjList = null;
SPContext LobjContext = GetSharePointContext(PstrSite);
if (LobjContext != null)
{
if (!string.IsNullOrEmpty(PstrTitle))
{
SPListCollection LobjLists = LobjContext.Web.Lists;
LobjContext.Load(LobjLists);
LobjContext.ExecuteQuery();

foreach (SPList LobjItem in LobjLists)
{
if (LobjItem.Title == PstrTitle)
{
LobjList = LobjItem;
break;
}
}

// done
return LobjList;
}
else
{
return null;
}
}
else
{
return null;
}
}
catch (Exception PobjEx)
{
MessageBox.Show(PobjEx.ToString());
return null;
}
}
[/sourcecode]

Adding a Field to a Word Table

I ran into an interesting problem trying to add a field to a table cell range from C#. I would get an exception:

The command is not available.

This is because the range you get from a cell includes it’s cell marker and you cannot delete that. So, you have to reset the range by one character (see bolded line below). Like this:

private void insertTableWithField(Word.Range PobjRange)
{
Word.
Table LobjTable = PobjRange.Tables.Add(PobjRange, 2, 1);
LobjTable.Cell(1, 1).Range.Text = “Table with Field”;
Word.Range LobjRange = LobjTable.Cell(2, 1).Range;
LobjRange.SetRange(LobjRange.Start, LobjRange.End – 1);
Word.Field LobjField = LobjRange.Fields.Add(LobjRange);
LobjField.Code.Text = “DOCPROPERTY Author \\* MERGEFORMAT”;
}