Thursday, September 10, 2009

CRM 4.0 Running an On Demand Workflow from JavaScript




Running a workflow from JavaScript is a great alternative to developing server side (web services) components. This is since the workflow designer is able to accomplish many programmable tasks without actually writing complex dot.net code. Implementing a simple JS framework that activates workflows is a great enhancement to any CRM project and can simplify many of your tasks.

The code integrates the notification (yellow ribbon) as a textual progress bar so the user can observe the progress of his actions.
The code also exposes a few properties that control the interval (in milliseconds) that checks the WF status, whether to refresh the page automatically or not and so forth.

The OnCrmPageLoad function below demonstrates the usage of the OnDemandWorkflow class. Paste the entire code in your onload event
and set the OnDemandWorkflow instance with the correct workflow id and properties.

Don’t forget to check your workflow as On Demand if you need to run them from code. You should also consider the logic that implements the client side call since user interaction can cause your workflow to run more then once.

Enjoy…


function OnDemandWorkflow()
{
var odw = this;
var request = null;

odw.DefaultProgress = new ProgressInfo();
odw.WorkflowId = "";
odw.OperationId = "";
odw.EntityId = "";
odw.Name = "";
odw.WorkflowStatus = -1;
odw.OnFinishCallback = null;

function UpdateNotification(msg)
{
if (!odw.DefaultProgress.Visible)
{
return;
}

var notification = document.all.Notifications;
notification.style.display = "inline";
notification.style.width = "100%";
notification.style.height = "20px";
notification.innerHTML = odw.Name + ": " + msg + "";
}

odw.Activate = function(sWorkflowId)
{
UpdateNotification("Activated");

if (sWorkflowId)
{
odw.WorkflowId = sWorkflowId
}

if (!odw.WorkflowId)
{
return UpdateNotification("Missing Workflow ID");
}

if (!odw.EntityId && crmForm.FormType == 2)
{
odw.EntityId = crmForm.ObjectId;
}
else if (!odw.EntityId)
{
return UpdateNotification("Workflow is missing an Entity name");
}

var xmlActReq = new StringBuilder();
xmlActReq.Append(GenerateSoapHeader());
xmlActReq.Append("");
xmlActReq.Append("");
xmlActReq.Append("").Append(odw.EntityId).Append("");
xmlActReq.Append("").Append(odw.WorkflowId).Append("");
xmlActReq.Append("
");
xmlActReq.Append("
");
xmlActReq.Append(GenerateSoapFooter());

odw.Execute("Execute",xmlActReq.ToString(),odw.ActivateEnd);
}

odw.Execute = function(sMethodName,sXmlRequest, fCallback)
{
if (request)
{
request.abort();
}
request = new ActiveXObject("Microsoft.XMLHTTP");
request.Open("POST", "/mscrmservices/2007/CrmService.asmx", true);
request.onreadystatechange = fCallback;
request.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2007/WebServices/" + sMethodName);
request.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
request.setRequestHeader("Content-Length", sXmlRequest.length);
request.send(sXmlRequest);
}

odw.ActivateEnd = function()
{
if (request.readyState == 4)
{
if (!validateResponse())
{
return;
}

odw.OperationId = request.responseXML.selectSingleNode("//Response/Id").nodeTypedValue;
odw.CheckStatus();
}
}

odw.CheckStatus = function()
{
var xmlChkReq = new StringBuilder();
xmlChkReq.Append(GenerateSoapHeader());
xmlChkReq.Append("");
xmlChkReq.Append("asyncoperation");
xmlChkReq.Append("{").Append(odw.OperationId).Append("}");
xmlChkReq.Append("");
xmlChkReq.Append("");
xmlChkReq.Append("statuscode");
xmlChkReq.Append("
");
xmlChkReq.Append("
");
xmlChkReq.Append("
");
xmlChkReq.Append(GenerateSoapFooter());

odw.Execute("Retrieve",xmlChkReq.ToString(),odw.CheckStatusEnd);
}

odw.CheckStatusEnd = function()
{
if (request.readyState == 4)
{
if (!validateResponse())
{
return setTimeout(odw.CheckStatus, odw.DefaultProgress.CheckInterval);
}

odw.WorkflowStatus = request.responseXML.selectSingleNode("//q1:statuscode").nodeTypedValue;

switch(parseInt(odw.WorkflowStatus))
{
case 30:
if (odw.DefaultProgress.RefreshWhenDone)
{
if (odw.DefaultProgress.RequestRefresh)
{
window.onbeforeunload = function()
{
return "Operation has succeeded, Do you wish to refresh the page?";
}
}
else
{
crmForm.detachCloseAlert();
}

setTimeout(function(){
window.location.reload();
},1000);
}

UpdateNotification("Operation has succeeded");
if (odw.OnFinishCallback)
{
odw.OnFinishCallback(odw);
}

break;
default:

switch(parseInt(odw.WorkflowStatus))
{
case 32: //canceled
UpdateNotification("Operation was canceled");
break;
case 22: //canceling
UpdateNotification("Operation is being canceled");
break;
case 31: //failed
UpdateNotification("Operation has failed");
break;
case 20: //In progress
UpdateNotification("Operation is in progress");
break;
case 21: //Pausing
UpdateNotification("Operation is pausing");
break;
case 10: //Waiting
UpdateNotification("Operation is waiting");
break;
case 0: //Waiting for resources
UpdateNotification("Operation is waiting for resources");
break;
}

return setTimeout(odw.CheckStatus, odw.DefaultProgress.CheckInterval);
}
}
}

function validateResponse()
{

var error = request.responseXML.selectSingleNode("//error");
var faultstring = request.responseXML.selectSingleNode("//faultstring");

if (error == null && faultstring == null)
{
return true;
}
else
{
odw.DefaultProgress.Visible = true;
if (error)
{
UpdateNotification(error.text);
}
else
{
UpdateNotification(faultstring.text);
}

return false;
}
}

function GenerateSoapHeader()
{
var soapHeader = new StringBuilder();
soapHeader.Append("");
soapHeader.Append(" soapHeader.Append(" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'");
soapHeader.Append(" xmlns:xsd='http://www.w3.org/2001/XMLSchema'>");
soapHeader.Append(GenerateAuthenticationHeader());
soapHeader.Append("");
return soapHeader.ToString();
}

function GenerateSoapFooter()
{
var soapFooter = new StringBuilder();
soapFooter.Append("
");
soapFooter.Append("
");
return soapFooter.ToString();
}

function ProgressInfo()
{
this.RequestRefresh = true;
this.RefreshWhenDone = false;
this.Visible = false;
this.CheckInterval = 2000;
}

function StringBuilder()
{
var parts = [];
this.Append = function( text ){parts[ parts.length ] = text;return this;}
this.Reset = function(){parts = [];}
this.ToString = function(){return parts.join( "" );}
}
}

function OnCrmPageLoad()
{
if (confirm("Run Workflow"))
{
var odWorkflow = new OnDemandWorkflow();
/* The workflow name - for presentation */
odWorkflow.Name = "Create Task";
var odwProgress = odWorkflow.DefaultProgress;
/* true is default - true = user has to approve the page reload. */
odwProgress.RequestRefresh = false;
/* false is default - true = try to refresh when the workflow is done successfully. */
odwProgress.RefreshWhenDone = true;
/* false is default - true = see notification progress */
odwProgress.Visible = true;
/* 2000 is default - ping the server each x milliseconds */
odwProgress.CheckInterval = 1000;
odWorkflow.WorkflowId = "76f5cecb-987c-4635-8d78-66d2caa2f9ae";
/* default Entity Id - the entity instance id (guid) */
odWorkflow.EntityId = crmForm.ObjectId;
odWorkflow.OnFinishCallback = CallmeWhenTheWFIsDone
odWorkflow.Activate();
}
}

function CallmeWhenTheWFIsDone(odw)
{
if (odw.WorkflowStatus == 30)
{
alert('more code here');
}
}

OnCrmPageLoad();

3 comments:

Dania said...

Hello Adi,
I've been trying to use your code while replacing the workflowID with my workflow ID as well as the name attribute. However, i'm getting an error on page load.
Is there anything i am missing?
Thanx for your help in advance.
Dania

Adi Katz said...

There might be a formatting issue the code in the post. Are you getting a syntax error?

Other than that, if the workflow is registered to run on demand and is available to all users the workflow id is all you need to change.

firedave said...

Hi Adi,

I have been trying to think of a good and helpful time that this would be useful. Do you have any examples of when you would use this over just allowing an on demand workflow be initiated the usual way from the menu?

Many thanks,
David