SharePoint customers typically apply the product also as technology foundation to run their employee business applications on. SharePoint as platform contains much of the required infrastructure and functional capabilities to make this possible. And if your business requirements go beyond what SharePoint provides out-of-the-box, SharePoint as technology framework enables the business application developers to extend the product featuring with own custom developed features.
In this posting, I’ll outline the architecture and technical design for such a business process application delivered through SharePoint as the Digital Workplace context.
High-over functional specification
- Users must be able to self initiate an instance of the business process(es);
- Users must be able to select from a set of business processes;
- The selection of business processes that a user is allowed to start, is role based;
- A process consists of steps to execute, and data to input; both specific per process type;
- The user experience for all business process handling must be user-friendly, intuitive and responsive;
- The business process form collects user input to process and administrate in a business administration;
- The business process form retrieves master data from the business administration to initialize input-controls (e.g. dropdown menu, checkboxes) with predefined and fixed input values;
- The business process forms must validate user input, to minimize load and disruption on the business administration due functional incorrect input. The validations contain simple pattern validators (e.g. valid postal code, email address), composite validators (if field X selected, then field Y and Z are required), and business validations (e.g., employee of function group Y may not be transitioned to function group X);
- The user must be enabled to temporarily park an initiated process instance, to continue with it at a later moment (e.g. after lunch break, or a meeting).
Required functional management flexibility
- Functional management must themselves be able to manage the authorization on process types
- New process type(s) including forms must Go-Live without disruption of the landscape / no release moment
Architecture and Software Design
SharePoint custom solutions can be JavaScript-based executing in the browser, .Net code executed on the SharePoint server (farm-solutions or sandboxed), or a combination of the two. As of SharePoint 2013, the third and now the preferred deployment model is the SharePoint App-model. In all the 3 deployment options, SharePoint can be interoperated for data administration and capabilities. It must be noted that for the client-model the exposed capabilities in SharePoint 2010 are limited to mostly data servicing, but SharePoint 2013 (on-premisse + Online / Office 365) opens up almost all of the server-side API for client-side invocation.
At the time this application was designed and developed, the involved customer-organization was (and still is) using SharePoint 2010. The App-model was therefore not possible. Yet, I aimed to setup the design as much prepared for the “loosely-coupled SharePoint architecture” that is now advocated for custom SharePoint solutions.
Applied design decisions:
- Administrate the process specifications in a SharePoint list
- The out-of-the-box SharePoint forms enable functional management to add, edit and delete process type specifications
- For role-based access, apply granulair SharePoint permissions on individual list item == process type specification
- Standard SharePoint REST service listdata.svc for clientside quering the process specifications administration
- Provide the process forms as HTML5/Javascript/CSS mini-applications, and use knockout.js for bi-directional data binding (Angular.js is another valid option).
- Administrate the process forms as HTML files in SharePoint document library
- The out-of-the-box SharePoint forms enable functional management to add, edit and delete process forms snippets
- Standard SharePoint as document/file administration for clientside on-the-fly retrieve (http-get) the process forms snippets
- Templated html that contains placeholders to dynamically fill with relevant html
- At application start, bind a selection menu to the retrieved processes data collection
- On user-selection of a process, get the process specific html and insert within the page rendering
- Administrate the process Models and ViewModels as javascript files in document library
- Standard SharePoint forms available enables functional management to add, edit and delete process forms snippets
- Standard SharePoint as document/file administration for clientside on-the-fly retrieve (http-get) the javascript resources
- Utilize javascript validator library; and validate the user input per formwizard step, on the moment the user wants to navigate to next step in the wizard.
- Utilize SharePoint BCS to interoperate from SharePoint context to the external business administrations; for data processing and business validations.
- Utilize webapi to expose a Javascript callable gateway into SharePoint BCS invocation.
Software and system components
- SharePoint
- List
- Document Library
- Listdata.svc
- Business Connectivity Services (BCS)
- webapi
- Knockout.js
- jQuery + jQuery.UI
- HTML snippets
- Javascript Model contracts
- JSON.net
- SAP, Oracle, …
Code snippets
<div id="HPW-Proces" data-bind="validationOptions: { insertMessages: false }" style="display:none;"> <div id="dialog-form-container"> <div id="processes-selection" data-bind="css: { hideElement: $root.selectedProcess() != null }"> <ul data-bind="foreach: processes"> <li data-bind="click: $root.selectedProcess"> <h3 data-bind="text: name"></h3> <a href="#" data-bind="text: description"></a> </li> </ul> </div> <div id="process-form" data-bind="with: $root.selectedProcess, css: { hideElement: $root.selectedProcess() == null }"> <ul class="nav-tabs" data-bind="foreach: tabs"> … </ul> <div class="tab-content-panel"> <div class="tab-content" data-bind="foreach: tabs"> <p class="page-title" data-bind="text: name, css: { hideElement: isSelected() !== true }"></p> <div data-bind="template: { name: 'Step' + ($index() + 1), data: $parent, afterRender: $root.initializeControls }, css: { hideElement: isSelected() !== true }"></div> </div> </div> <div class="btnRow"> …. </div> </div> </div>
EnsureScriptFunc("sp.js", "SP.ClientContext", function () { var ctx = new SP.ClientContext(); var site = ctx.get_site(); ctx.load(site, 'Url'); ctx.executeQueryAsync(function (s, a) { var siteUrl = site.get_url(); var hpwProcessUI = document.getElementById('HPW-Proces'); ko.applyBindings(new HPW.AppViewModel.ProcessesVM(siteUrl), viewProcessUI); bindingContext = ko.contextFor(hpwProcessUI); $("#HPW-Proces").show(); }); });
var HPW = window.HPW || {}; HPW.Models = function () { /* This Model Provider will dynamically determine for which process the model data must be retrieved. */ var ModelProvider = { GetModel: function (id) { return (function (id) { var model = null; switch (id) { case "PR01": model = HPW.Models.PR01.Model(); break; case "PR02": model = HPW.Models.PR02.Model(); break; … } return model; })(id); } } // Public interface return { ModelProvider: ModelProvider } }();
HPW.AppViewModel = function () { function ProcessesVM(siteUrl) { var self = this; self.siteUrl = siteUrl; //Provide selectable process models. self.processes = ko.observableArray(); var processenConfigUrl = siteUrl + "/_vti_bin/ListData.svc/HPWProcessesConfiguration?@select=Title,Name,Description,DialogUrl&@orderby=Title"; $.getJSON(processenConfigUrl, function (data) { $.each(data.d.results, function (i, result) { self.processes.push(new HPW.ProcessViewModel(result.ProcessId, result.Name, result.Description, result.DialogUrl)); }); }).fail(HPW.Utilities.ErrorHandler); //Watch selected process. self.selectedProcess = ko.observable(null); //Dynamically load correct data model. self.selectedProcess.subscribe(function (selectedProcessViewModel) { var dialogHtml = selectedProcessViewModel.dialogUrl; $.get(dialogHtml, function (data) { var x = document.getElementById('ProcessTabs'); $(x).html(data); self.selectedProcess().model(new HPW.Models.ModelProvider.GetModel(selectedProcessViewModel.id)); var instanceId = HPW.Utilities.getParameterByName('instanceId'); if (instanceId != null && instanceId != "") { var viewModel = self.selectedProcess().model(); viewModel.loadState(instanceId); } }); });
"use strict"; var HPW = window.HPW || {}; HPW.Models.PR01 = function () { var ViewModel_ProcessX = function () { var self = this; /* VIEWMODEL */ self.steps = [ …]; return { steps: steps; …. } } return { Model: ViewModel_ProcessX } }();