Duet workflow extensibility and SharePoint-hosted apps Part 1

In this blog post, learn how to retrieve updated information for a Duet workflow task from SAP and customize the workflow item page to show the information in an app for SharePoint.

The following are the high-level tasks required to accomplish our scenario:

    1. Create a SharePoint-hosted app.
    2. Add a Business Data Connectivity (BDC) model and external lists to the app.
    3. Use Duet Enterprise Single Sign-on for the app.
    4. Use the Business Connectivity Services (BCS) JavaScript object model (JSOM) to fetch workflow tasks and associated Items from the SAP system.
    5. Add a Web Part to the page to show the updated information.

To create a SharePoint-hosted app

    1. Start Visual Studio 2012 by using the Run as administrator option.
    2. In Visual Studio 2012, on the File menu, choose New, Project.
    3. In the New Project dialog box, expand the Visual C# node, expand the Office/SharePoint node, and then choose the Apps node. Choose App for SharePoint 2013.
    4. Name the project, and then choose the OK button.
    5. In the first Specify the App for SharePoint Settings dialog box, name your app and choose SharePoint-hosted under How do you want to host your app for SharePoint. Also specify the URL of the Duet WorkFlow site you want to debug against under What SharePoint site do you want to use for debugging your app, and choose Finish.
    6. In Visual Studio, on the Build menu, choose Deploy your app name.

After the app is deployed, Visual Studio launches the default page for the app for SharePoint.

To add BDC model and external lists to the app

    1. In Solution Explorer, open the shortcut menu for the project, and choose Add, Content types for External Data source.
    2. On the Specify OData Source page, enter the URL of the Duet Enterprise Workflow Service .
    3. Choose a name for your OData source (for example, SAPWorkflows), and then choose Next.
    4. Specify the SAP username and password to connect to the service metadata.
    5. A list of data entities that are being exposed by the OData Service appears. Select the following entities:
      • AttachmentCollection
      • CommentCollection
      • DecisionOptionCollection
      • ExtensibleElementCollection
      • ParticipantCollection
      • Service Operations
      • TaskDescriptionCollection
      • WorkflowTaskCollection
    1. Select the Create list instances for the selected data entities (except Service Operations) check box.
    2. Choose Finish.

clip_image001Note

Make sure that the SAP workflow service allows basic authentication because the BDC Autogeneration tool currently supports only anonymous and basic authentication.

If you deploy the app and navigate to /Lists/WorkflowTaskCollection inside your app, you will get “Message from External System : 'LobSystem (External System) returned authentication error.'” Now it is time to enhance the app to use the Duet Enterprise Single Sign-on feature.

To enhance the app to use Duet Enterprise Single Sign-on

    1. Create an OData connection by executing the following command in the SharePoint Management Shell.
 New-SPODataConnectionSetting -Name SAPWorkflow 
-ServiceContext "https://localhost" -ExtensionProvider 
"OBA.Server.Canary.ObaOdataServerExtensionProvider, OBA.Server.SSOProvider, 
Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" 
-AuthenticationMode passthrough -ServiceAddressURL 
"<URL_OF_DUET_ENTERPRISE_WORKFLOW_SERVICE>” 
    1. Modify the BDC model to use the newly created connection.

      1. Double-click WorkflowTaskCollection.ect.
      2. On the Properties tab, update the value of ODataConnectionSettingId to SAPWorkflow.
    2. Allow the app to use the connection.

      1. Open AppManifest.xml.
      2. In Permission Requests, select Scope = BCS and Permission = Read.
    3. Deploy the app and validate single sign-on.

      1. Press F5 to deploy the app.
      2. On the Permissions page for the app, select the OData connection this app will use (for example, SAPWorkflow).
      3. Choose the Trust It button.
      4. Navigate to the Lists/WorkflowTaskCollection URL inside the app.

You would see SAP tasks assigned to you coming directly from the SAP system.

To use the BCS JSOM to fetch workflow tasks and associated Items from the SAP system

    1. In Solution Explorer, right-click the Pages folder, add an HTML page, and name it MoreInfo.html.
    2. Copy the following markup and paste it in the HTML page. The markup performs the following tasks:
      • Retrieves the ClientContext for the parent web to which SharePoint has been installed.
      • Retrieves the TaskInstanceParentId for the selected workflow task using the Duet workflow list GUID and the selected item id.
      • Parses the TaskInstanceParentId to get SAP Origin and the id for the workflow in SAP.
      • Loads the entities for the external content types.
      • Reads the specific workflow task from SAP using a specific finder by passing SAP Origin and the workflow task id for SAP as parameters.
      • Reads the Extensible Elements for the workflow task from SAP.

The following is a complete HTML page with JavaScript.

 <html lang="en" xmlns="https://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>More Details from SAP</title>

    <!--Loads the libraries from the Microsoft CDN.-->
    <script 
        src="//ajax.aspnetcdn.com/ajax/4.0/1/MicrosoftAjax.js" 
        type="text/javascript">
    </script>
    <script 
        src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.2.min.js" 
        type="text/javascript">
    </script>      

    <script type="text/javascript">

        // Set the style of the Client Web Part page to 
        // be consistent with the host web.
        function setStyleSheet() {
            var hostUrl = decodeURIComponent(getParameter("SPHostUrl"));
            if (hostUrl == "") {
                document.write("<link rel=\"stylesheet\"" +
                 "href=\"/_layouts/15/1033/styles/themable/corev15.css\"/>");
            }
            else {
                document.write("<link rel=\"stylesheet\"" +
                 " href=\"" + hostUrl + "/_layouts/15/defaultcss.ashx\" />");
            }
        }
        setStyleSheet();

        // Retrieves the query string parameters.
            function getParameter(paramToRetrieve) {
                var params = document.URL.split("?")[1].split("&");
                var strParams = "";
                for (var i = 0; i < params.length; i = i + 1) {
                var singleParam = params[i].split("=");
                if (singleParam[0] == paramToRetrieve)
                    return singleParam[1];
            }
        }
    </script>
</head>

<body>
    <script type="text/javascript">

        // Loads the required SharePoint libraries 
        // and executes retrieveTaskFromList once the libraries are loaded.
        $(document).ready(function () {
            var hostweburl = decodeURIComponent(
                    getQueryStringParameter("SPHostUrl"));
            var scriptbase = hostweburl + "/_layouts/15/";
            $.getScript(scriptbase + "SP.Runtime.js",
              function () { $.getScript(scriptbase + "SP.js", execOperation); }
            );
        });

        function execOperation() {
            retrieveTaskFromList();
        }

        function retrieveTaskFromList() {

            // Retrieves the ClientContext of the parent web.
            var clientContext = 
            new SP.ClientContext.get_current();
            var appContextSite = 
            new SP.AppContextSite(clientContext, 
                   decodeURIComponent(getQueryStringParameter("SPHostUrl")));
            var web = appContextSite.get_web();

            // Loads the selected workflow task from the Duet workflow list.
            var listGuid = decodeURIComponent(
                    getQueryStringParameter("ListId"));
            var itemId = decodeURIComponent(
                    getQueryStringParameter("ItemId"));
            this.workflowItem = web.get_lists().getById(
                    listGuid.substring(1, listGuid.length - 1)).
                    getItemById(itemId);
            clientContext.load(workflowItem);

            clientContext.executeQueryAsync(
                Function.createDelegate(this, this.onLoadingTaskSucceeded),
                Function.createDelegate(this, this.onError)
            );
        }

        function onLoadingTaskSucceeded() {
            var sapParameters = parseParentTaskId(workflowItem.
                    get_item("TaskInstanceParentId"));
            getDataFromSAP(sapParameters);
        }

        // Parses the TaskInstanceParentId 
        // to get SAP Origin and the id for the workflow in SAP.
        function parseParentTaskId(parentTaskId) {
            var idlength = parseInt(parentTaskId.substring(0, 2));
            var sapParameters = new Object();
            if (parentTaskId.length > (17 + idlength)) {
                sapParameters.workitemId = parentTaskId.
                        substring(5, 5 + idlength);
                sapParameters.sapOrigin = parentTaskId.
                        substring(17 + idlength);
            }
            return sapParameters;
        }
        
        // Retrieves the workflow task and associated elements from SAP.
        function getDataFromSAP(sapParameters) {
            context = new SP.ClientContext.get_current();

            //Loads the entities for the external content types.
            wfEntity = context.get_web().getAppBdcCatalog().
                   getEntity("DUET_WORKFLOW_CORE", "WorkflowTaskCollection");
            context.load(wfEntity);
            wfExtensibleElementEntity = context.get_web().getAppBdcCatalog().
                    getEntity("DUET_WORKFLOW_CORE", 
                    "ExtensibleElementCollection");
            context.load(wfExtensibleElementEntity);

            // Loads a LOB system instances.
            lobSystem = wfEntity.getLobSystem();
            lobSystemInstanceCollection = lobSystem.getLobSystemInstances();
            context.load(lobSystemInstanceCollection);

            context.executeQueryAsync(onLoadingLOBInstanceSucceeded,onError);

            function onLoadingLOBInstanceSucceeded() {
                lobSystemInstance = lobSystemInstanceCollection.get_item(0);

                // Reads the workflow task from SAP using specific finder.
                var identifierValues = [sapParameters.sapOrigin, 
                          sapParameters.workitemId];
                var entityIdentity = SP.BusinessData.Runtime.EntityIdentity.
                          newObject(context, identifierValues);
                wfEntityInstance = wfEntity.findSpecific(entityIdentity, 
                           "ReadSpecificWorkflowTask", lobSystemInstance);
                context.load(wfEntityInstance);

               context.executeQueryAsync(onLoadingWorkflowSucceeded,onError);
            }

            function onLoadingWorkflowSucceeded() {

                // Reads the Extensible Elements for the workflow task.
                var exFilterCollection=wfExtensibleElementEntity.getFilters(
                        "GetExtensibleElementsFromWorkflowTaskCollection");
                context.load(exFilterCollection);
                exElementsCollection = wfExtensibleElementEntity.
                        findAssociated(wfEntityInstance, 
                        "GetExtensibleElementsFromWorkflowTaskCollection", 
                        exFilterCollection, lobSystemInstance);
                context.load(exElementsCollection);

                context.executeQueryAsync(getUpdatedDataSucceeded, onError);
            }

            function getUpdatedDataSucceeded() {
                alert("Number of extensible elements: " + 
                        exElementsCollection.get_count());
            }

            function onError(sender, e) {
                alert("Request failed. " + e.get_message() +
                           "\n" + e.get_stackTrace());
            }
        }

        // Retrieves a query string value.
        function getQueryStringParameter(paramToRetrieve) {
            var params = document.URL.split("?")[1].split("&");
            var strParams = "";
            for (var i = 0; i < params.length; i = i + 1) {
                var singleParam = params[i].split("=");
                if (singleParam[0] == paramToRetrieve)
                    return singleParam[1];
            }
        }
    </script>
</body>
</html>

      3. Open AppManifest.xml.

      4. In Permission Requests, select Scope = Web and Permission = Read.

To show a page from the app web in the host web

    1. Open SharePoint Designer 2013.

    2. Open the Duet workflow site in SharePoint Designer.

    3. Choose New, Add Items, More Pages. Choose HTML, click Create, and name the file cewp (This file will serve as the source for the content editor Web Part).

    4. Go to Site Pages and open WrkTaskIP.aspx. Choose Edit File and open the page in the advanced mode.

    5. Copy the following markup in WrkTask.aspx. The markup performs the following tasks:

      1. Adds a Content Editor Web Part to the SharePoint page.
      2. Sets its content source to cewp.htm.
 <WebPartPages:ContentEditorWebPart 
              runat="server" 
              ID="GetDataFromSAP" 
              IsVisible="True" 
              Title="More Details From SAP" 
              Height="" 
              Width="" 
              ContentLink="<Duet workflow site>/SitePages/cewp.htm" 
              __WebPartId="{5CD66503-2117-4E9B-8AB4-6E79F4921CE3}">
</WebPartPages:ContentEditorWebPart>

clip_image001[8]To Do:

Replace the <Duet workflow site> with the full URL of the Duet workflow site.

    1. Copy the following markup in cewp.htm. The markup performs the following tasks:

        · Adds an iframe to the CEWP and sets its source to the information page MoreInfo.html of the installed app for SharePoint.

 <!DOCTYPE html>
<html lang="en" xmlns="https://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>More Details from SAP</title>

    <!--Loads the libraries from the Microsoft CDN.-->
    <script 
        src="//ajax.aspnetcdn.com/ajax/4.0/1/MicrosoftAjax.js" 
        type="text/javascript">
    </script>
    <script 
        src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.2.min.js" 
        type="text/javascript">
    </script>      
</head>
<body>
    <div>
        <!--Adds an iFrame to show the contents of the information page.-->
       <iframe id="showDataFrame" height="70px" width="100%" scrolling="no">
            <p>Your browser does not support iframes.</p>
        </iframe>
    </div>
        
    <script type="text/javascript">
        var hostWebURL = "";
        var appWebURL = "";

        // Retrieves the list ID and the item ID from the URL of the page.
        var listID = decodeURIComponent(getQueryStringParameter("List"));
        var itemID = decodeURIComponent(getQueryStringParameter("ID"));
        var sourceURL = "";

        // Loads the required SharePoint libraries and 
        // executes getAppWebUrl once the libraries are loaded.
        $(document).ready(function () {
            hostWebURL = _spPageContextInfo.webAbsoluteUrl;
            var scriptbase = hostWebURL + "/_layouts/15/";
            $.getScript(scriptbase + "SP.Runtime.js",
              function () { $.getScript(scriptbase + "SP.js", getAppWebUrl);}
            );
        });

        // Retrieves the AppWebURL for the installed app for SharePoint.
        function getAppWebUrl(){
            var clientContext = new SP.ClientContext.get_current();
            var web = clientContext.get_web();

            // Loads the app instances for the installed app for SharePoint 
            // given the Product ID.
            // See appManifest for the product ID.
            var appInstances=web.getAppInstancesByProductId("<Product ID>"); 
            clientContext.load(appInstances);
            clientContext.executeQueryAsync(
                    getAppInstancesSucceeded, 
                    getAppInstancesFailed
                );    
            
            function getAppInstancesSucceeded(){
                if(appInstances.get_count()>0){
                    var enumerator = appInstances.getEnumerator();
                    enumerator.moveNext();
                    var appInstance = enumerator.get_current();

                  // Retrieves the full AppWebURL for the app for SharePoint.
                    appWebURL = appInstance.get_appWebFullUrl();

                    // Creates the source URL for the iFrame to show 
                    // the contents of the information page.
                    var sourceURL = appWebURL + "/Pages/MoreInfo.html?" + 
                      "SPHostUrl=" + hostWebURL + "&" + 
                      "ListId={" + encodeURIComponent(listID) + "}&" + 
                      "ItemId=" + encodeURIComponent(itemID);
                    showData(sourceURL);
                }
            }

            // Sets the source for the iFrame.
            function showData(sourceURL) {
                var divElement = document.getElementById("showDataFrame");
                divElement.setAttribute("src", sourceURL );
            }

            function getAppInstancesFailed(sender, e){
                alert(e.message);
            }
        }

        // Retrieves a query string value.
        function getQueryStringParameter(paramToRetrieve) {
            var params = document.URL.split("?")[1].split("&");
            var strParams = "";

            for (var i = 0; i < params.length; i = i + 1) {
                var singleParam = params[i].split("=");
                if (singleParam[0] == paramToRetrieve)
                    return singleParam[1];
            }
        }
    </script>
</body>
</html>

clip_image001[9]To Do:

Replace the <Product ID> with the Product ID of the app for SharePoint.

Deploy the app for SharePoint, and then go to the Duet workflow site and open a workflow task assigned to you. On the page, you will see the added Web Part as shown in the following figure. The page will alert the number of extensible elements associated to the selected workflow after retrieving the workflow task and associated elements from SAP.

Figure 1. Message showing the number of extensible elements associated with the selected workflow

Using HTML and JavaScript to render custom UI

    1. Copy the following markup inside the body in MoreInfo.html.
 <div id = "Data"></div>
    1. In Solution Explorer, right-click and add a JavaScript file to the Scripts folder and name it BuildUI.js.
    2. Include the following snippet inside the head in MoreInfo.html.
 <script 
    src="../Scripts/BuildUI.js" 
    type="text/javascript">
</script>
    1. Copy the following code to BuildUI.js to display the recent information from SAP.
 function addDetail(tbody, label, value) {
    var row = document.createElement("tr");
    var name_cell = document.createElement("td");
    name_cell.appendChild(document.createTextNode(label));
    row.appendChild(name_cell);
    var value_cell = document.createElement("td");
    value_cell.appendChild(document.createTextNode(value));
    row.appendChild(value_cell);
    tbody.appendChild(row);
}

function AddUpdatedDataForWorkflow(divElement, status_label, status_value,
                                        priority_label, priority_value,
                                        XProp_label, exElementsCollection) {
    var table = document.createElement("table");
    var tbody = document.createElement("tbody");

    addDetail(tbody, status_label, status_value);
    addDetail(tbody, priority_label, priority_value);

    var extensibleElementsEnum = exElementsCollection.getEnumerator();
    while (extensibleElementsEnum.moveNext()) {
        var extensibleElement = extensibleElementsEnum.get_current();

        if (extensibleElement.get_item("context") === "XPROP") {
         if (extensibleElement.get_item("name")==="LeaveDaysUsedTillToday") {
           addDetail(tbody,XProp_label, extensibleElement.get_item("value"));
          }
        }
    }

    table.appendChild(tbody);
    divElement.append(table);
}
    1. In MoreInfo.html, replace
 alert("Number of extensible elements: " + exElementsCollection.get_count());

with

 AddUpdatedDataForWorkflow($("#Data"), 
    "Status", wfEntityInstance.get_item("status_txt"),
    "Priority", wfEntityInstance.get_item("priority"),
    "Leaves Taken Till Today", exElementsCollection);

Now the recent information for the Duet workflow task from SAP is visible on the page.

Figure 2. Content Editor Web Part showing the recent information for the Duet workflow task from SAP