Now that the tools for developing
SharePoint and Office 2013 apps have RTM’d (see Soma's blog here) I wanted to look at a particular scenario
that I think will be very common. The SharePoint app model provides the ability
to add a workflow to an app. The trouble is when you do this, workflows in apps
can only be associated with other resources in the same app. SharePoint
workflows are generally started in three ways, through a list association,
manually started or programmatically. I think association with a list/library
is by far the most common. So this means that a workflow you add to an app can
only be associated with a list that is also defined in the same app. This isn’t
really what you want though. Why? Because deploying lists as part of an app is
problematic. This is because lists contain data and if you undeploy your app
the data will be lost. It’s very likely that a workflow will change at a
different rate to a list instance and so there is a problem.
The solution to this is to *not*
associate the workflow to a list in the app. Instead, think about keeping the
list definitions and workflow definitions separate. Of course, the workflow
still needs to be associated to a list somehow, and to do this, you can use the
Handle App Installed event receiver. In this event receiver you can wire up a
workflow to any list in any app web or host web you choose. However, it is not
(currently) possible to have a workflow definition in one web and a list in
another web and associate the two. Instead, the workflow definition must be
deployed to the same location as the list.
There are therefore several steps to
deploying workflows in apps:
1. Implement your app, add your workflow, etc
2. Implement the Handle App Installed event receiver. This will do the following:
a. Obtain a reference to the app’s web – the source
b. Obtain a reference to the target web (e.g. host web
or another app web)
c. Copy the workflow definition from the source to the
target
d. Associate the workflow now in the target to the
list in the target
e. Specify the events the workflow should trigger on,
e.g. item added or updated
3. Deploy your app
All of this is possible because of
the rich API that workflow manager provides. Let’s have a look at the code
required to achieve all this. The first step is to obtain a reference to
the contexts for the source and target locations as shown below.
string targetUrl = "http://targetlocationUrl"; // this is the location of the list/library to associate workflow with
string targetListTitle = "DemoList"; // this is the title of the list to associate with the workflow
ClientContext sourceContext = TokenHelper.CreateAppEventClientContext(properties, true);
sourceContext.Load(sourceContext.Web);
sourceContext.ExecuteQuery();
Uri sharepointUrl = new Uri(targetUrl);
string contextTokenString = properties.ContextToken;
HttpRequestMessageProperty requestProperty = (HttpRequestMessageProperty)OperationContext.Current.IncomingMessageProperties[HttpRequestMessageProperty.Name];
SharePointContextToken contextToken = TokenHelper.ReadAndValidateContextToken(contextTokenString, requestProperty.Headers[HttpRequestHeader.Host]);
string accessToken = TokenHelper.GetAccessToken(contextToken, sharepointUrl.Authority).AccessToken;
ClientContext targetContext = TokenHelper.GetClientContextWithAccessToken(sharepointUrl.ToString(), accessToken);The next thing to do
is get a reference to the workflow manager objects. These will allow access and manipulation of workflow artifacts via the API. There are three main services, deployment, instance and subscription. The first step is to obtain references to the target app web:
var tgtwsm = new Microsoft.SharePoint.Client.WorkflowServices.WorkflowServicesManager(targetContext, targetContext.Web);
targetContext.Load(tgtwsm);
targetContext.ExecuteQuery();
var tgtwds = tgtwsm.GetWorkflowDeploymentService();
var tgtwis = tgtwsm.GetWorkflowInstanceService();
var tgtwss = tgtwsm.GetWorkflowSubscriptionService();
targetContext.Load(tgtwds);
targetContext.Load(tgtwis);
targetContext.Load(tgtwss);
var lists = targetContext.Web.Lists;
targetContext.Load(lists);
targetContext.ExecuteQuery();
Then the code below is used to do the same for the source app web:
var srcwsm = new Microsoft.SharePoint.Client.WorkflowServices.WorkflowServicesManager(sourceContext, sourceContext.Web);
sourceContext.Load(srcwsm);
sourceContext.ExecuteQuery();
var srcwds = srcwsm.GetWorkflowDeploymentService();
var srcwis = srcwsm.GetWorkflowInstanceService();
var srcwss = srcwsm.GetWorkflowSubscriptionService();
sourceContext.Load(srcwds);
sourceContext.Load(srcwis);
sourceContext.Load(srcwss);
sourceContext.ExecuteQuery();
Then the workflow in the app can be retrieved - it will have been deployed at this point as the event is fired after installation. Here I'm simply grabbing the first workflow I find as I know there will be only one.
var wDefCollection = srcwds.EnumerateDefinitions(false);
sourceContext.Load(wDefCollection);
sourceContext.ExecuteQuery();
Guid definitionId = Guid.NewGuid();
string wfName = string.Empty;
foreach (var wDefEnumerator in wDefCollection)
{
var defDetails = wDefEnumerator.Id + " : " + wDefEnumerator.DisplayName + "\n";
definitionId = wDefEnumerator.Id;
wfName = wDefEnumerator.DisplayName;
}With the workflow definition in hand, I can retrieve the workflow xaml and copy it to the target app web and then save and publish it.
srcwds.GetDefinition(definitionId);
var _readDef = srcwds.GetDefinition(definitionId);
sourceContext.Load(_readDef);
sourceContext.ExecuteQuery();
string wfXaml = _readDef.Xaml;
var workflow = new Microsoft.SharePoint.Client.WorkflowServices.WorkflowDefinition(targetContext);
workflow.Xaml = wfXaml;
workflow.DisplayName = wfName;
targetContext.Load(workflow);
tgtwds.SaveDefinition(workflow);
targetContext.ExecuteQuery();
tgtwds.PublishDefinition(workflow.Id);
targetContext.ExecuteQuery();
Finally, I need to wire up the workflow definition with the list on which I want it to fire. For this I use the subscription service. It's possible to specify the event types such as ItemUpdated to define on which list operations a workflow instance will be created. The main piece of information required is ID of the list itself of course. I'm also specifying the history and task lists that workflow instances are able to use. I'll come back to that point in a moment.
var wSub = new Microsoft.SharePoint.Client.WorkflowServices.WorkflowSubscription(targetContext);
historyList = targetContext.Web.Lists.GetByTitle("WorkflowHistoryList");taskList = targetContext.Web.Lists.GetByTitle("Workflow Tasks");List targetList = targetContext.Web.Lists.GetByTitle(targetListTitle);
targetContext.Load(targetList);
targetContext.Load(historyList);
targetContext.Load(taskList);
targetContext.Load(wSub);
targetContext.ExecuteQuery();
wSub.Name = targetListTitle + " - ItemAdded";
wSub.DefinitionId = workflow.Id;
wSub.EventTypes = new List<string>() { "ItemAdded" };
wSub.EventSourceId = targetList.Id;
wSub.SetProperty("HistoryListId", historyList.Id.ToString());
wSub.SetProperty("TaskListId", taskList.Id.ToString());
wSub.SetProperty("ListId", targetList.Id.ToString());
wSub.SetProperty("Microsoft.SharePoint.ActivationProperties.ListId", targetList.Id.ToString());
var wSubId = tgtwss.PublishSubscription(wSub);
targetContext.ExecuteQuery();If you want to use workflow history list and workflow task
list you probably need to create them in the target app web. This is because they just won't exist. In the code below I'm checking if they already exist and if not, I create them (this code should go just above the previous block).
List historyList = null;
List taskList = null;
foreach (var list in lists)
{
if (list.Title == "WorkflowHistoryList")
{
historyList = targetContext.Web.Lists.GetByTitle("WorkflowHistoryList");
}
if (list.Title == "Workflow Tasks")
{
taskList = targetContext.Web.Lists.GetByTitle("Workflow Tasks");
}
}// check if the lists exist in host
if (null == historyList)
{
// create the lists in the host
ListCreationInformation tgtHistoryList = new ListCreationInformation();
tgtHistoryList.Title = "WorkflowHistoryList";
tgtHistoryList.TemplateType = 140;
List tgtHistory = targetContext.Web.Lists.Add(tgtHistoryList);
tgtHistory.Description = "This list instance is used for workflow History items.";
tgtHistory.Update();
targetContext.ExecuteQuery();
}
if (null == taskList)
{
ListCreationInformation tgtTaskList = new ListCreationInformation();
tgtTaskList.Title = "Workflow Tasks";
tgtTaskList.TemplateType = 171;
List tgtTask = targetContext.Web.Lists.Add(tgtTaskList);
tgtTask.Description = "This list instance is used for workflow Task items.";
tgtTask.ContentTypesEnabled = true;
ContentTypeCollection cts = targetContext.Web.AvailableContentTypes;
targetContext.Load(cts);
targetContext.ExecuteQuery();
ContentType ct = cts.GetById("0x0108003365C4474CAE8C42BCE396314E88E51F");
targetContext.Load(ct);
targetContext.ExecuteQuery();
tgtTask.ContentTypes.AddExistingContentType(ct);
tgtTask.Update();
targetContext.ExecuteQuery();
}
For this to compile, you must add a reference to Microsoft.SharePoint.Client.WorkflowServices.dll.
Next, add a workflow to the app, doesn't matter what you call it or what it does. During the creation process, make it a list workflow and un-check the box so that you don't associate it with anything. Then add a list, accept the defaults and call it DemoList. With the code above, what will happen is that the workflow will get deployed and wired up to the list programatically. In this example I'm just using the same source and target but you can change the target URL to whatever you like. In this way the workflow can be deployed in an app for convenience (one main reason being that you can develop workflows in Visual Studio without a local instance of SharePoint) and deploy to wherever you like.
With the code above in the Handle App Installed event receiver I can now deploy and test out the app. When the app installs my event receiver fires and copies it to the target URL I specify, wiring up to the list I provide in the code.
Navigating to the list (using a url in the form https://myO365namespace.sharepoint.com/workflowdeploydemo/lists/demolist) I see this:

Adding a new item we can see the workflow association and the instance executing:

This will bring up the workflows as shown below:

So, the workflow has executed and should've written out a line to the history list as my workflow just had a single activity WriteToHistory in it. Pointing the browser at this list (in the form https://myO365namespace.sharepoint.com/WorkflowDeployDemo/Lists/WorkflowHistoryList) and voila:

And that's it. How to deploy workflows in an app and associate them with the thing you really want to associate them with
