Blog

Custom Provisioning of settings, Apps and App Parts during Site Creation


by Tobias Lekman, 18 December, 2014

Background

The App framework has a few things that need to be implemented before we can abandon the Web Part model. In my latest project, we ran into the issue of web template provisioning and Apps. We can do this using Web Parts and standard farm features but we are not coding to that pattern – we are using a “cloud ready” App model. You can upload and add an app to a site using CSOM with some trickery:

// <summary>
//
/ Deploy SP App
//
/ </summary>
//
/ <param name="context">Client context</param>
//
/ <param name="appFullPath">Full path to app file (.app)</param>
//
/ <returns></returns>
private static AppInstance DeployApp(ClientContext context, string appFullPath)
{
using (
var packageStream = System.IO.File.OpenRead(appFullPath))
{
var appInstance = context.Web.LoadAndInstallApp(packageStream);
context.Load(appInstance);
context.ExecuteQuery();
return appInstance;
}
}


// Usage example
using (var ctx = new ClientContext(url))
{
ctx.Credentials
= new SharePointOnlineCredentials(userName, securePassword);
var appInstance = DeployApp(ctx, @"C:\Packages\SPApp.app");
if (appInstance != null && appInstance.Status == AppInstanceStatus.Initialized)
{
Console.WriteLine(
"App was installed.");
}
}

Source: http://stackoverflow.com/questions/25260197/installing-sharepoint-app-with-csom

However, this only applies to side where sideloading is enabled, and that is a big no-no on a production site, where you then could potentially upload any App to the site.

We want to create a framework for adding Apps, App Parts and change other settings to site during site provisioning. And we didn’t give up!

Provisioning Framework

What we do is to run a thread when the user creates the site. That user will have owner privileges and has the right to perform the actions. The user will, however, not know how to perform all the needed actions. Therefore we set a web property bag field using a timestamp, allowing us to “lock the provisioning thread” and to

"use strict"; // Our script files should always use strict formatting

// Register our namespaces
var common = window.common || {};
common.Provision
= function (timeStampKey, timeoutInterval, featureId) {
//#region Properties
this.timeStampKey = 'PROVISION_TIMESTAMP_' + timeStampKey.toUpperCase();
this.timeoutInterval = timeoutInterval * 1000;
this.featureId = featureId;
this.executeHandler = null;
//#endregion

//#region Private properties
var ctx;
var web;
var props;
//#endregion

//#region Public functions

this.execute = function (handler) {
// Load SP context objects
writeLog('Executing provisioning handler.');
ctx
= new SP.ClientContext.get_current();
web
= ctx.get_web();
props
= web.get_allProperties();
ctx.load(web);
ctx.load(props);
if (handler != null) {
this.executeHandler = handler;
}
var module = this;
// Determine if the process is currently running
ctx.executeQueryAsync(
// success
function (sender, args) {
if (shouldRun(module)) {
// We should run
writeTimeStamp(module);
module.executeHandler();
}
},
// fail
function (sender, args) {
writeLog(
'Failed to retrieve site context');
}
);
}
// Call when all actions are completed. This will write the status to the timestamp key and also deactivate the feature.
this.complete = function () {
props.set_item(
this.timeStampKey, 'Done');
web.update();
var module = this;
ctx.executeQueryAsync(
function (sender, args) {
// deactivate my feature
var features = web.get_features();
ctx.load(features);
ctx.executeQueryAsync(
function (sender, args) {
features.remove(module.featureId,
true);
web.update();
ctx.executeQueryAsync(
function (sender, args) {
writeLog(
'Feature deactivated');
writeLog(
'All operations completed.');
},
function (sender, args) {
writeLog(
'Failed to deactivate feature in inner step');
});
},
function (sender, args) {
writeLog(
'Failed to deactivate feature');
});
},
function (sender, args) {
writeLog(
'Unable to set timestamp!');
});
}
//#endregion

//#region Private functions

// Retrieves the timestamp to see if it is empty, completed or stalled
function shouldRun(module) {
try {
module.timeStamp
= props.get_item(module.timeStampKey);
writeLog(
'Timestamp key ' + module.timeStampKey + ' retrieved, value ' + module.timeStamp);
}
catch (ex) {
// the property is not present. we should run.
return true;
}
if (module.timeStamp == 'Done') {
writeLog(
'Timestamp done, forcing completion and removal');
module.complete();
return false;
}
var now = parseInt(new Date().getTime().toString());
var stamp = parseInt(module.timeStamp);
var diff = now - stamp;
if (diff > module.timeoutInterval) {
writeLog(
'Timestamp states that operation has timed out. Running all operations again.');
// The operation has timed out. Run again.
return true;
}
return false;
}
// Writes the timestamp to show that we have started processing the item
function writeTimeStamp(module) {
props.set_item(module.timeStampKey,
new Date().getTime().toString());
web.update();
ctx.executeQueryAsync(
function (sender, args) { }, function (sender, args) { });
}
// Writes to the log file
function writeLog(message) {
try {
console.log(message);
}
catch (ex) {
// no devtools
}
}
// #endregion
}

We then use the provision object as

// Register our name spaces
var provision = window.provision || {};
jQuery(document).ready(
function () {

provision.prototype
= new common.Provision( // Create a new instance of a provisioning context. This is used to manage threading and locks
'WebTemplatePrototype', // The ID key of the provisioning. MUST me different for each action type and cannot be reused!
30, // The timeout, in seconds, for the action to complete. If another page load triggers the request after 2 minutes and the operation failed, then it will try again.
'e7d39b51-f053-471c-9b50-863dcf4240ab'); // The ID of the feature containing this registration. The framework removes the feature after its done.

provision.prototype.execute(
function () { // we pass the executing main function to the framework. this method only executes if the thread is correctly locked.
provision.something.Task().done(
// It's a good idea to use jQuery promises here so that we can control threading
).done(function (result) {
// Once promise is done, check the result. If all is fine, then complete the call.
provision.prototype.complete(); // IMPORTANT! Here we mark the provisioning as complete and it will not happen again. The feature is also removed so this script is not called again.
});
});
});
});

As you can see from the comments, we create a new common.Provision object with a specific ID (used in the property bag), a timeout interval to that the operation can resume if it fails and we specify the feature ID so that the script can “remove itself” by deactivating the feature and therefore removing its script. This was done in the XML module as

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomAction
Id="ProvisionPrototype"
ScriptSrc
="~site/script/provisionPrototype.js"
Location
="ScriptLink"
Sequence
="100" />
<Module Name="ScriptRegistration">
<File Path="Prototype\provisionPrototype.js" Url="script/provisionPrototype.js" />
</Module>
</Elements>

Provisioning Apps and App Parts

My colleague and friend Fredrik Ekstrom has started a new blog (just to share this? :-D ) and he has posted the beginnings of the solution here: https://fredrikekstromsp.wordpress.com/2014/12/15/2/.

To achieve this, the solution was to mimic the posts that is done by the browser and in some of the cases open the admin pages in an IFRAME and simulate the clicks in the browser.

The basic flow of the customization is

addapp

All of these steps are therefore taken care of by the ‘macro’ style jQuery script that runs while the feature is active. This will be removed once the provision.prototype feature is deactivated.

 Read more on Fredrik’s blog for details.

The disclaimer!

This type of solution is highly vulnerable to any service update or change in implementation from Microsoft’s side. Therefore, this could be a terribly unstable solution on an Office 365 solution. On the other hand, we simply have no choice as this is a fixed client requirement. The client has thousands of publishers and they create hundreds of sites each year. Not automating this would just not be an alternative for us, so we are really hoping that this will become a standard feature of “App Model v2”.