Unifying Your Web Dev Skills for Office Add-ins
Guest Post by Eric Legault
Using RoamingSettings with your add-in to store application data for your web-based Outlook add-in is all well and good. However, it may not take much to exceed the 32k storage limit, plus you are also left to define the data format as you see fit – which of course means extra work. Learn how you can overcome this by creating a hidden folder in the user’s mailbox to store and retrieve your application’s business objects as serialized settings in an email message, by simply passing JSON objects that then get serialized into XML for storage and vice-versa.
Technologies: Office 365; Outlook; Office Add-ins; Exchange Web Services; JavaScript; jQuery; JSON; Office UI Fabric; XML
Introduction
You’re a modern web developer. You know JavaScript, HTML 5, CSS+, etc. You may know about the new Office Add-ins (formerly Apps for Office) and how they are built on a new web-based architecture. What you may not know yet is how to tie it all together with your current skillset or how to quickly get started learning unfamiliar APIs so you can build innovative solutions which extend Outlook, Word, Excel etc. both in the desktop clients and the browser versions of these applications across all devices.
As is usual when you’re learning any kind of new technology, finding useful starter articles and/or projects can prove difficult. Hopefully when you’re done reading this you’ll come away with some big and bright lightbulbs going off in your head! I’ll be covering the foundations of using application data storage, managing business objects, performing common messaging operations and building a simple but sharp and responsive UI. The sample project accompanying this article should not only prove to be a very useful learning tool, but can serve as the basis for implementing a custom storage provider which you can easily re-use for your own projects.
The Cool Tech We’ll Use
I’ve got quite the mix in this solution – and I guarantee you’ll come away from this article with some useful APIs, techniques and code that you can steal:
· A custom reading pane Outlook Add-in
· TheOutlook Add-in API subset of the JavaScript API for Office
· JavaScript, HTML 5 and CSS (of course)
· Some jQuery (mainly for selectors, a dialog widget and some Deferreds)
· Exchange Web Services (aka EWS, but using SOAP and not that fancy managed API - so the hard way)
· JSON to XML and XML to JSON (thanks tox2js)
· The Office UI Fabric for the polish (especially the NavBar component)
Here’s a peek at what we’re doing:
Figure 1: The activated "Storage Provider Demo" Outlook add-in
Creating a Better Method for Solution Storage
The Mailbox API already provides a native way to read/write custom application for Outlook add-ins via the RoamingSettings object (see “Get and set add-in metadata for an Outlook add-in” for a good overview). However, there are limits of 32KB for each setting and 2MB maximum that a single add-in can store. That may sound like enough, but not in the real world! For one add-in I was writing I was storing mailbox folder paths and PR_ENTRY_ID values for each folder in RoamingSettings, and during testing with my own production mailbox (with 400+ folders) I hit the 32KB limit quickly. I immediately realized this was no good at all and that I needed to build something custom. So I built it!
Using the same approach as RoamingSettings, my Custom Solution Storage Provider simply uses a hidden folder in the user’s mailbox and a Post message (you can use an e-mail message if you want – just alter the CreateItem request) to store required content in the message body. Not only that, it stores it as XML so that you can easily use JSON objects to manage your application settings/data and serialize/de-serialize that data via this hidden storage. All you need to do is use EWS to read and write to the message whenever you want. However, keep in mind that EWS calls in Outlook add-ins limits both request and response calls to 1MB. It may be rare to exceed this limit, but please keep this in mind.
What We’ll Be Doing
· Using various EWS operations:
· Creating a folder in the root of a mailbox with CreateFolder
· Hiding a folder by setting extended properties with UpdateFolder
· Creating a message with CreateItem
· Retrieving the body content of an existing message with GetItem
· Updating the body of an existing message with UpdateItem
· Persisting JSON objects to XML, and create JSON objects from XML
· Keeping the code organized using the jQuery Module Pattern
· Executing multi-step EWS operations using jQuery Deferreds
· Theme the UI using Office Fabric
· Effectively displaying application notifications and dialogs in the context of an Outlook add-in
· Dynamically manipulating HTML elements on the page using jQuery
What You’ll Need
· Visual Studio 2015 or 2013 (or Napa, if you’re brave; or Yeoman if you’re hard-core)
· Quick Start: “Create and debug Office Add-ins in Visual Studio”
· An Office 365 account or Exchange 2013+ Mailbox (need a test account?)
· Source for the Office UI Fabric (optional; included in sample project)
· Source for x2js for the JSON<->XML (optional; included in sample project)
· The source for this sample project if you want to “click and learn”
· Napa version: https://aka.ms/Dtxwlh
· GitHub repo: https://github.com/elegault/OutlookAddinStorageProviderDemo
How This Will Work
We’ll create a simple but effective UI that’ll showcase how to:
· Create a named e-mail folder that’ll contain the hidden solution storage message
· Use input elements to create some sample JSON business objects in memory and output their XML representation to the page
· Save and retrieve add-in settings and business logic/XML to both RoamingSettings (to store the IDs for the folder and solution storage message) and our solution storage message
· Reset add-in settings if needed to start from scratch
Playing Along
If you want to step through the code and see how everything works, download the source code (see links above in “What You’ll Need”) and run the project. If you need help with that, see “Create and debug Office Add-ins in Visual Studio”.
You can also install the add-in and run it without needing the source code at all. I’ve published the add-in to a web site in Azure - simply download the manifest file and install it from the Manage add-ins page under Mailbox options in Office 365. This is one way of releasing an add-in without having to publish it to the Office store (as long as you have an https cert).
Core Project Components
All Office Add-in projects in Visual Studio are comprised of two separate projects within the solution: one for the add-in itself (MailAddinStorageProvider) and one for the web component (MailAddinStorageProviderWeb). You can take a look at “Create and debug Office Add-ins in Visual Studio” for a quick walkthrough if you’re a first-timer. The add-in’s project is simple and just contains an XML manifest that’s used to basically declare the add-in’s description and intent. For this project, the main requirements are to:
· Activate for read messages (as opposed to compose messages)
· Ask for ReadWriteMailbox permissions (necessary for EWS calls)
· Request a display size of 450 px (the maximum; we need a lot of space)
<?xml version="1.0" encoding="UTF-8"?>
<!--Created:cb85b80c-f585-40ff-8bfc-12ff4d0e34a9-->
<OfficeApp xmlns="https://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:type="MailApp">
<Id>981dbe1a-78eb-44de-8b36-24ef4518d0e2</Id>
<Version>1.0.0.0</Version>
<ProviderName>Eric Legault Consulting Inc.</ProviderName>
<DefaultLocale>en-US</DefaultLocale>
<DisplayName DefaultValue="Storage Provider Demo" />
<Description DefaultValue="Mail Addin Storage Provider Demo"/>
<Hosts>
<Host Name="Mailbox" />
</Hosts>
<Requirements>
<Sets>
<Set Name="MailBox" MinVersion="1.1" />
</Sets>
</Requirements>
<FormSettings>
<Form xsi:type="ItemRead">
<DesktopSettings>
<SourceLocation DefaultValue="~remoteAppUrl/AppRead/Home/Home.html"/>
<RequestedHeight>450</RequestedHeight>
</DesktopSettings>
</Form>
</FormSettings>
<Permissions>ReadWriteMailbox</Permissions>
<Rule xsi:type="RuleCollection" Mode="Or">
<Rule xsi:type="ItemIs" ItemType="Message" FormType="Read" />
</Rule>
<DisableEntityHighlighting>false</DisableEntityHighlighting>
</OfficeApp>
The web project really only contains five files that have all our custom guts:
· /Addins/Outlook/StorageProvider/AppRead/Home.css
· /Addins/Outlook/StorageProvider/AppRead/Home.html
· /Addins/Outlook/StorageProvider/AppRead/Home.js
· /Addins/Outlook/StorageProvider/App.css
· /Addins/Outlook/StorageProvider/App.js
Note: these folder paths differ from the Visual Studio project template defaults
Files for referenced components are stored in other folders:
· Content: Office and Fabric CSS and scripts
· Scripts: core JavaScript for Office, jQuery and xml2json libraries
Figure 2: Office Add-in project components in Visual Studio Solution Explorer
Architecting the Solution Storage Provider
The heavy lifting for implementing this custom storage provider is performed by using EWS operations. Note that we’re limited to a sub-set of EWS operations because not every method that is typically available in the EWS Managed API (which can be used for client applications, but NOT web apps) are supported in Outlook add-ins. The first operation that we need to call is CreateFolder, which will create a folder with the name provided in the Folder Name textbox after you click the “Create Folder” button in the NavBar. We then need to store the Id value for that folder in RoamingSettings so that we know where to create our solution storage message. For that, we need to call CreateItem and once again save the Id for that message in RoamingSettings so that we can get and update the message body when required to persist our application data via GetItem and UpdateItem calls.
Callout: EWS Operations in a Nutshell
To call an EWS operation from an Outlook add-in requires calling the Mailbox.makeEwsRequestAsync method. I’ll use the CreateFolder call as an example of how to construct an EWS request. The initial call is made in the createSolutionStorage feature:
var createSolutionStorage = function (folderName, isHidden) {
app.showNotification('Please wait...', 'Creating solution storage folder...');
solutionStorage.usingHiddenFolder = isHidden;
solutionStorage.solutionFolderName = folderName;
$.when(
mailbox.makeEwsRequestAsync(ewsRequests.getCreateSolutionStorageFolderRequest(folderName, isHidden), ewsCallbacks.createSolutionStorageFolderCallback)
).then(function () {
if (solutionStorage.solutionFolderID) {
return 'success';
};
}).fail(function() {
console.log("oops");
});
};
The first parameter for makeEwsRequestAsync requires a string in the form of an XML SOAP request with the details of the EWS operation we are asking Exchange to perform; I’m calling the ewsRequests.getCreateSolutionStorageFolderRequest function to build and return that string, helped with the folderName and isHidden parameters for that function:
var getCreateSolutionStorageFolderRequest = function(folderName, isHidden) {
var request;
var distinguishedFolderId;
//DistinguishedFolderId values: https://msdn.microsoft.com/en-us/library/office/aa580808(v=exchg.150).aspx
//NOTE: Use root to create in visible folder at Mailbox root instead, msgfolderrot to create at Top of Information Store folder (visible folders with default folders at root)
if (isHidden) {
distinguishedFolderId = "root";
} else {
distinguishedFolderId = "msgfolderroot";
}
request = '<?xml version="1.0" encoding="utf-8"?> ' +
'<soap:Envelope xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" ' +
' xmlns:xsd="https://www.w3.org/2001/XMLSchema" ' +
' xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/" ' +
' xmlns:t="https://schemas.microsoft.com/exchange/services/2006/types"> ' +
' <soap:Header>' +
' <RequestServerVersion Version="Exchange2013" xmlns="https://schemas.microsoft.com/exchange/services/2006/types" soap:mustUnderstand="0" />' +
' </soap:Header>' +
' <soap:Body>' +
' <CreateFolder xmlns="https://schemas.microsoft.com/exchange/services/2006/messages">' +
' <ParentFolderId>' +
' <t:DistinguishedFolderId Id="' + distinguishedFolderId + '"/>' +
' </ParentFolderId>' +
' <Folders>' +
' <t:Folder>' +
' <t:DisplayName>' + folderName + '</t:DisplayName>' +
' </t:Folder>' +
' </Folders>' +
' </CreateFolder>' +
' </soap:Body>' +
'</soap:Envelope>';
return request;
};
We have to set the value of the DistinguishedFolderId property accordingly based on whether we want to make the folder hidden or not. If it does not need to be hidden, the folder will be created at the root of the Mailbox and visible within the folder hierarchy in Outlook. However, I recommend for purposes of testing that you do NOT create a hidden folder, as there is no code provided that can delete that folder if you want to reset your solution storage and start from scratch. To reset your storage with a visible folder, you can delete the folder in Outlook (and hence delete the hidden message) and then make sure to click the “Clear Settings” button to remove the Ids for the folder and message from RoamingSettings (the named settings themselves are deleted, not just the values). If you do want to delete the hidden folder, you can use OutlookSpy, MFCMAPI or the EWSEditor to find that folder in the MsgFolderRoot and delete it using those utilities.
The second parameter is the function callback that will read the XML response returned from Exchange; in this case, the createSolutionStorageFolderCallback function:
var ewsCallbacks = (function() {
var createSolutionStorageFolderCallback = function (asyncResult){
//Use arguments[0].asyncContext to get at userContext parameter value (if used in caller)
var result = null;
if (asyncResult === null) {
app.showNotification('Error!', '[in createSolutionStorageFolderCallback]: null result');
return 'error';
}
if (asyncResult.error !== null) {
app.showNotification('Error!', '[in createSolutionStorageFolderCallback]: ' + asyncResult.error.message);
return 'error';
} else {
try {
var response = $.parseXML(asyncResult.value);
var responseDOM = $(response);
var prop;
if (responseDOM) {
if (responseDOM) {
prop = responseDOM.filterNode("m:ResponseCode")[0];
}
if (!prop) {
app.showNotification('Error!', '[in createSolutionStorageFolderCallback]: Failed to parse response');
return 'error';
} else {
var errorType = prop.textContent;
if (prop.textContent === "ErrorFolderExists") {
//The folder already exists!
return 'error';
}
if (prop.textContent === "NoError") {
var foldersNode = null;
foldersNode = responseDOM.filterNode("m:Folders")[0];
if (!foldersNode) {
app.showNotification('Error!', '[in createSolutionStorageFolderCallback]: Failed to retrieve folder data');
return 'error';
}
var folderchildNodes;
try {
//NOTE Get ID for new solution storage folder
folderchildNodes = foldersNode.childNodes[0];
solutionStorage.solutionFolderID = folderchildNodes.childNodes.item("Folder").getAttribute("Id");
//NOTE Do not update folder (call updateFolderCallback) if we are creating it at the Mailbox root (i.e. it is not hidden and does not require setting extended properties)
if (solutionStorage.usingHiddenFolder) {
//NOTE Make new solution storage folder hidden
mailbox.makeEwsRequestAsync(ewsRequests.getUpdateFolderRequest(solutionStorage.solutionFolderID), ewsCallbacks.updateFolderCallback);
} else {
//Saves the folder Id to RoamingSettings
solutionStorage.saveSettings();
}
result = solutionStorage.solutionFolderID;
} catch (e) {
return 'error';
}
} else {
prop = responseDOM.filterNode("m:MessageText")[0];
app.showNotification('Error!', '[in createSolutionStorageFolderCallback]: ' + errorType + ": " + prop.textContent);
return 'error';
}
}
}
} catch (errorMsg) {
app.showNotification('Error!', '[in createSolutionStorageFolderCallback]: Failed to parse response (' + errorMsg + ')');
return 'error';
}
}
return result;
};
Note the judicious use of evaluations for various response scenarios. Ideally errors will never be hit, but you’ll find these checks invaluable when you’re first trying out any kind of EWS operation as they almost never work out at first run! However, if everything works as it should then the results of our call will contain the FolderId value for our new folder in the XML response body which we can then persist in RoamingSettings to use later when we need to create the storage message in that folder. A typical response body would look something like this:
<?xml version="1.0" encoding="utf-8" ?>
<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="https://www.w3.org/2001/XMLSchema">
<soap:Header>
<t:ServerVersionInfo MajorVersion="8" MinorVersion="0" MajorBuildNumber="595" MinorBuildNumber="0"
xmlns:t="https://schemas.microsoft.com/exchange/services/2006/types" />
</soap:Header>
<soap:Body>
<CreateFolderResponse xmlns:m="https://schemas.microsoft.com/exchange/services/2006/messages"
xmlns:t="https://schemas.microsoft.com/exchange/services/2006/types"
xmlns="https://schemas.microsoft.com/exchange/services/2006/messages">
<m:ResponseMessages>
<m:CreateFolderResponseMessage ResponseClass="Success">
<m:ResponseCode>NoError</m:ResponseCode>
<m:Folders>
<t:Folder>
<t:FolderId Id="AS4AUn==" />
</t:Folder>
</m:Folders>
</m:CreateFolderResponseMessage>
<m:CreateFolderResponseMessage ResponseClass="Success">
<m:ResponseCode>NoError</m:ResponseCode>
<m:Folders>
<t:Folder>
<t:FolderId Id="AS4AUn==" />
</t:Folder>
</m:Folders>
</m:CreateFolderResponseMessage>
</m:ResponseMessages>
</CreateFolderResponse>
</soap:Body>
</soap:Envelope>
Ready to Store Your Data!
Now that we have our solution storage folder created, go ahead and create some business objects! Enter an Artist Name, select a genre and click the “Add Artist” button:
Figure 3: Using the add-in's UI to generate business objects and XML
Behind the scenes, the addArtist() function create a new instance of a Band object, adds it to our solutionStorage.appicationData.FavoriteBands object and then passes solutionStorage.appicationData to the X2JS. json2xml_str function, which returns the data as serialized XML:
function addArtist() {
var artistName = $("#artistName").prop('value');
var genre = $("#genres").prop('value');
if (artistName === undefined) {
app.showNotification("Wait!", "You must enter an artist name.");
return;
}
if (genre === undefined) {
app.showNotification("Wait!", "You must select a genre.");
return;
}
var artist = new Band(artistName, genre);
var xmlData;
var x2js = new X2JS();
if (solutionStorage.applicationData.FavoriteBands.Band === "") {
//X2JS.xml_str2json will set the root object as an empty string if there are no XML data nodes, so we need to initialize it as an array
solutionStorage.applicationData.FavoriteBands.Band = [];
solutionStorage.applicationData.FavoriteBands.Band.push(artist);
return;
} else {
if (Array.isArray(solutionStorage.applicationData.FavoriteBands.Band)) {
//Add band to bands
solutionStorage.applicationData.FavoriteBands.Band.push(artist);
} else {
//There is one existing Band, but it is an Object and not an array; we need to change the Object to an Array, re-add the existing band and then add the second one. There's probably something I don't understand here...
var firstFave = solutionStorage.applicationData.FavoriteBands.Band;
solutionStorage.applicationData.FavoriteBands.Band = [];
solutionStorage.applicationData.FavoriteBands.Band.push(firstFave);
solutionStorage.applicationData.FavoriteBands.Band.push(artist);
}
}
$("#numberOfBusinessObjects").prop('innerText', "#Business Objects in memory: " + solutionStorage.applicationData.FavoriteBands.Band.length);
xmlData = x2js.json2xml_str(solutionStorage.applicationData);
$("#xmlText").prop('value', xmlData); //NOTE Use .prop for dynamic attributes like checked, selected and value (https://api.jquery.com/prop/)
}
Here’s what our FavoriteBands collection of Band objects looks like in XML:
<FavoriteBands>
<Band>
<Name>Iron Maiden</Name>
<Genre>Heavy Metal</Genre>
</Band>
<Band>
<Name>Dio</Name>
<Genre>Heavy Metal</Genre>
</Band>
<Band>
<Name>Alice in Chains</Name>
<Genre>Heavy Metal</Genre>
</Band>
</FavoriteBands>
Now that we have the XML, we can push it up to storage – go ahead and click the “Update Storage” button. This will run the updateStorage() function, which is setup to detect whether this is the first time we’re using the solution storage, because if it is we’ll create it first – otherwise we’ll just update it. Since this is our first time using storage, this function will call solutionStorage.createStorageItem() function which in turn issues an EWS request for CreateItem. When we read the response from solutionStorage.createStorageItemCallback we can grab the Id of the new message and persist it to RoamingSettings. Later calls to update storage will use the EWS UpdateItem operation instead.
Both CreateItem and UpdateItem calls are similar in that they are taking our business objects (stored in solutionStorage.applicationData) and passing it to the X2JS.json2xml_str function which conveniently returns to us the XML string that we can store in the message body:
var x2js = new X2JS();
xmlData = x2js.json2xml_str(applicationData);
body = xmlData;
body = htmlEncode(body);
As you can see in the folder that we created, the XML markup is happily living in the message body of the Post message that’s being used for solution storage:
Figure 4: Data for the add-in stored as XML in an Outlook Post item
The UI
When I first built this it looked rather plain using the standard HTML controls. I wanted it to look more modern and quickly realized this is exactly what the Office UI Fabric is for. With minimal effort you can not only use the Office Design Language so that it blends right in to the Office application you’re extending, but it also ensures that your UI is responsive and “mobile first”. All that’s needed is to apply some Fabric CSS classes (and implement some JavaScript where required for some components).
To get started, simply add some references to the Fabric CSS via CDN (or host them yourself) in the head of the HTML:
<link rel="stylesheet" href="https://appsforoffice.microsoft.com/fabric/1.0/fabric.min.css">
<link rel="stylesheet" href="https://appsforoffice.microsoft.com/fabric/1.0/fabric.components.min.css">
Then apply a Fabric class to make otherwise bland HTML controls like buttons and dropdowns jump out, such as in the controls we’re using to add and remove business objects:
<div class="ms-Dropdown" tabindex="0">
<label class="ms-Label">Genre</label>
<i class="ms-Dropdown-caretDown ms-Icon ms-Icon--caretDown"></i>
<select class="ms-Dropdown-select" id="genres">
<option value="Heavy Metal">Heavy Metal</option>
<option value="Blues">Blues</option>
<option value="Classical">Classical</option>
<option value="Jazz">Jazz</option>
<option value="Rock">Rock</option>
<option value="Other">Other</option>
</select>
</div>
<button class="ms-Button ms-Button--primary" id="addArtist"><span class="ms-Button-icon"><i class="ms-Icon ms-Icon--plus"></i></span> <span class="ms-Button-label">Add Band</span> <span class="ms-Button-description"></span></button>
<button class="ms-Button ms-Button--primary" id="clearArtists"><span class="ms-Button-icon"><i class="ms-Icon ms-Icon--plus"></i></span> <span class="ms-Button-label">Clear Artists</span> <span class="ms-Button-description">Empties memory</span></button>
Code Review Time
Those of you who are traditional desktop or Office developers (hello COM Add-ins! You’re not dead yet!) may have a bit of a learning curve when it comes to the bread and butter of modern web application development, especially JavaScript and jQuery. So it may help to go through a brief review of how those APIs were used effectively in the context of this solution.
Object-Oriented Programming adherents will notice a similarity in how the solutionStorage feature was designed. I followed the jQuery Module Pattern and broke down the core methods and properties into loosely coupled units of functionality the best I could. The key constructs are:
· ApplicationData, FavoriteBands and Band object functions
· Features for EWS operations (ewsCallbacks and ewsRequests) and the core [module] function used by all Office add-ins
· The solutionStorage feature with core properties and functions for:
· clearSettings()
· createSolutionStorageFolder()
· createStorageItem()
· get StorageIds()
· getStorageItem()
· saveMyAddinSettingsCallback()
· saveSettings()
· updateStorageItem()
Figure 5: The Module Pattern with core features
One thing that’s very handy when dealing with EWS calls is being able to cleanly execute multi-step EWS operations using jQuery Deferreds. I’m not a fan of calling a second EWS call from within an EWS callback function as your code can get very messy very quickly. By using Deferred objects you can have more legible code through chainable constructors to better organize your asynchronous calls. A good example of how this is used in our sample project is in the createFolder function:
function createFolder() {
var folderName = $("#folderName").prop('value');
var isHidden = $("#hiddenFolder").prop('checked');
if (folderName == "") {
app.showNotification("Wait!", "You must enter a folder name.");
return;
}
solutionStorage.clearSettings();
var result;
//NOTE Using deferreds: https://learn.jquery.com/code-organization/deferreds/jquery-deferreds/
$.when(
result = solutionStorage.createSolutionStorage(folderName, isHidden)
).then(function () {
solutionStorage.saveSettings();
if (isHidden) {
app.showNotification("Folder created!", "Now one more call to make it hidden...");
result = solutionStorage.updateFolder();
}
}).fail(function() {
app.showNotification("Zoot alors!", "!!!");
return;
});
//result = solutionStorage.createSolutionStorage(folderName, isHidden);
if (result = 'success') {
app.showNotification("Woo-hoo!", "Folder '" + solutionStorage.solutionFolderName + "' created. Now go ahead and start creating some business objects and then click the 'Update Storage' button to save that data to xml in a message within that folder.");
}
}
After we make the call to create the folder in the “when()” branch, we can then wait until the “then()” branch executes to decide if we need to make a second EWS call to make the folder hidden. This is a better approach than making this evaluation inside the first callback within the “when()” branch. Deferreds can quickly become essential if you need to make three or more EWS calls (it happens!) in a row, and the compactness of the code is far better than navigating amongst multiple methods when coding or debugging.
What’s Next?
Feel free to rip out the solutionStorage feature for your own projects and extend or modify it as you see fit. You may require multiple storage items for advanced scenarios or need to use JSON strings instead of XML for application data. Take the UI examples even further with other cool Fabric components like the Panel, DatePicker, PersonaCard or ListItem. Or borrow the code organization patterns and EWS XML requests for your own unique solution. You have your wicked web dev skills already, and now you hopefully have a good foundation for taking what you know to the next level with Office add-ins. Cheers!
Originally posted on https://www.ericlegaultconsulting.com/blog/