Closing An Incident (Case) That Has Open Activities
In The Lap Of The Gods…
Every so often I come across a feature in CRM that makes me wonder “why was it designed like that?”. The one that catches me out almost every time I demo is the inability to close or cancel a incident when there are associated open activities. This wouldn’t be so bad except that many of these activities are generated automatically by workflow, so when you cancel these activities manually, workflow processes continue to run and create still more activities.
Solving this problem requires a a plug-in that will first cancel any running workflows (to prevent new activities from being created) and then cancel any open or scheduled activities before closing or cancelling an incident,
On the incident entity, Microsoft Dynamics CRM 4.0 fires a CloseIncident message when a case is resolved and a SetStateDynamicEntity message when a case is cancelled or reactivated. So to start with we need to implement the IPlugin.Execute method and check for the “SetStateDynamicEntity” or “Close” messages.
It is good practice to check straight away that the plug-in is running in the correct context to avoid unnecessary code from executing and minimise performance bottlenecks. Here we are checking that we are running synchronously against the incident entity in the pre-processing stage of a parent pipeline.
Public Sub Execute(ByVal context As IPluginExecutionContext) Implements IPlugin.Execute
' Exit if any of the following conditions are true:
' 1. plug-in is not running on the 'incident' entity
' 2. plug-in is not running synchronously (context.Mode = Synchronous)
' 3. plug-in is not running in the 'pre-processing' stage of the pipeline (context.Stage = BeforeMainOperationOutsideTransaction)
' 4. plug-in is not running in a 'parent' pipeline (context.InvocationSource = Parent)
' 5. plug-in is not running on the 'Close', or 'SetStateDynamicEntity' messages
If (context.PrimaryEntityName = "incident") Then
If (context.Mode = MessageProcessingMode.Synchronous) Then
If (context.Stage = MessageProcessingStage.BeforeMainOperationOutsideTransaction) Then
If (context.InvocationSource = MessageInvocationSource.Parent) Then
If context.MessageName = "SetStateDynamicEntity" Then
HandleSetStateDynamicEntity(context)
ElseIf context.MessageName = "Close" Then
HandleClose(context)
End If
End If
End If
End If
End If
End Sub
The “SetStateDynamicEntity” event generates a context with an InputParameters property collection that contains a State property. Before continuing, we should check that this property equals “Cancelled”. We can then check for the EntityMoniker property for the id field which contains the ID of the case.
Private Sub HandleSetStateDynamicEntity(ByVal context As IPluginExecutionContext)
If context.InputParameters.Properties.Contains("State") And context.InputParameters.Properties.Contains("EntityMoniker") Then
If TypeOf context.InputParameters.Properties("State") Is String And TypeOf context.InputParameters.Properties("EntityMoniker") Is Moniker Then
If CStr(context.InputParameters.Properties("State")) = "Canceled" Then
Dim moniker = CType(context.InputParameters.Properties("EntityMoniker"), Moniker)
If Not moniker Is Nothing Then
Dim incidentid = CType(context.InputParameters.Properties("EntityMoniker"), Moniker).Id
CancelChildWorkflows(incidentid, context)
CancelChildActivities(incidentid, context)
End If
End If
End If
End If
End Sub
Similarly, the “Close” event generates a context with an InputParameters property collection that contains an IncidentResolution property. Before continuing, we should check that this property is a DynamicEntity and then check for the incidentid field which contains the ID of the case.
Private Sub HandleClose(ByVal context As IPluginExecutionContext)
If context.InputParameters.Properties.Contains("IncidentResolution") Then
If TypeOf context.InputParameters.Properties("IncidentResolution") Is DynamicEntity Then
Dim incidentresolution = CType(context.InputParameters.Properties("IncidentResolution"), DynamicEntity)
If incidentresolution.Properties.Contains("incidentid") Then
If TypeOf incidentresolution.Properties.Item("incidentid") Is Lookup Then
Dim incidentid = CType(incidentresolution.Properties.Item("incidentid"), Lookup).Value
CancelChildWorkflows(incidentid, context)
CancelChildActivities(incidentid, context)
End If
End If
End If
End If
End Sub
Notice I have included quite a bit of error checking to make sure that we don’t hit any errors such as ArgumentNullException.
Now that we have the ID of the case record, we need to loop through all the active child workflows and open child activities, and cancel them. Since the process is almost identical for activities as it is for workflows, I’ll just cover off the process for cancelling workflows.
First up, we need to define a QueryExpression that queries the asyncoperation entity and requests all records where the operationtype = 10 (i.e. workflow), the regardingobjectid equals the case id, and the statecode is either “Suspended” or “Ready”. Unfortunately, if a workflow is being processed by CRM, it will be in a “Locked” state, which means that no other process can access it. It might not therefore be possible to cancel all active workflows if the plug-in executes at the same time a workflow is locked.
Private Function RetrieveChildWorkflows(ByVal parententityid As Guid, ByVal context As IPluginExecutionContext) As List(Of BusinessEntity)
Dim filterStateCode As New FilterExpression
filterStateCode.FilterOperator = LogicalOperator.Or
filterStateCode.AddCondition("statecode", ConditionOperator.Equal, "Suspended")
filterStateCode.AddCondition("statecode", ConditionOperator.Equal, "Ready")
Dim filter As New FilterExpression
filter.FilterOperator = LogicalOperator.And
filter.AddCondition("regardingobjectid", ConditionOperator.Equal, parententityid)
filter.AddCondition("operationtype", ConditionOperator.Equal, 10)
filter.AddFilter(filterStateCode)
Dim qe As New QueryExpression
qe.ColumnSet = New ColumnSet(New String() {"asyncoperationid", "statecode", "statuscode"})
qe.EntityName = "asyncoperation"
qe.Criteria = filter
Dim request As New RetrieveMultipleRequest
request.ReturnDynamicEntities = True
request.Query = qe
service = context.CreateCrmService(False)
Dim response = CType(service.Execute(request), RetrieveMultipleResponse)
Return response.BusinessEntityCollection.BusinessEntities
End Function
Finally we need to loop through each asyncoperation in turn, and cancel by updating statecode = “Completed” and statuscode = 32 (i.e. Cancelled).
Private Sub CancelChildWorkflows(ByVal parententityid As Guid, ByVal context As IPluginExecutionContext)
For Each asyncoperation In RetrieveChildWorkflows(parententityid, context)
If TypeOf asyncoperation Is DynamicEntity Then
If Not CType(asyncoperation, DynamicEntity) Is Nothing Then
CancelWorkflow(CType(asyncoperation, DynamicEntity), context)
End If
End If
Next
End Sub
Private Sub CancelWorkflow(ByVal entity As DynamicEntity, ByVal context As IPluginExecutionContext)
If entity.Name = "asyncoperation" Then
If entity.Properties.Contains("statecode") And entity.Properties.Contains("statuscode") Then
If TypeOf entity.Properties("statecode") Is String And TypeOf entity.Properties("statuscode") Is Status Then
entity.Properties("statecode") = "Completed"
entity.Properties("statuscode") = New Status(32)
Dim target As New TargetUpdateDynamic
target.Entity = entity
Dim request As New UpdateRequest
request.Target = target
service = context.CreateCrmService(False)
Dim response = CType(service.Execute(request), UpdateResponse)
End If
End If
End If
End Sub
Great, so now we’re done cancelling active child workflows we can do something very similar with open child activities. I’ve uploaded the Visual Studio 2008 project here for you to get the full source code.
Also, to make life easier for those of you who just want to install the plug-in “AS-IS”, I’ve used the SDK plug-in installer sample code, and created two batch files, install.cmd and uninstall.cmd, which you can use. All you need to do is edit these files and modify the orgname, url, domain, username and password parameters to match your own crm environment.
So is that it? Well, not quite! This plug-in works as expected when you select Cancel Case from the Actions menu, but when you select Resolve Case things don’t go according to plan.
The main problem is the that the Resolve Case form checks for any open activities during the OnLoad event, and if it finds any, a dialog box is opened with a warning message. When you click OK to acknowledge the warning, the Resolve Case form is helpfully closed as well – The upshot is that no “Close” event is ever fired.
!!!…WARNING: UNSUPPORTED CUSTOMISATION ALERT…!!!
So you probably guessed from the warning that you can fix this issue, but only by modifying one of the files on each CRM server in your environment. I would strongly urge you only try this in NON-PRODUCTION environments. For more details about the bad things that occur when you step outside the supportability rules, please read the following SDK article: Unsupported Customizations.
OK, so now you understand the ramifications of going unsupported, here’s how to fix the problem.
- In the CRMWeb\CS\cases\ folder on your CRM server, locate the file dlg_closecase.aspx
- Using Visual Studio, Notepad, or any text editor of your choice, edit this file.
- Find the statement if (typeof(LOCID_CONFIRM_ACTIVITIES)!="undefined") statement
- Directly below this statement, comment out the lines alert(LOCID_CONFIRM_ACTIVITIES); and window.close();
- Save the file.
Your modifications should look something like this.
if (typeof(LOCID_CONFIRM_ACTIVITIES)!="undefined")
{
//alert(LOCID_CONFIRM_ACTIVITIES);
//window.close();
}
Now, when when you select Resolve Case from the Actions menu, you get to fill out the Case Resolution form without the warning. When you click OK to save your information, the “Close” event is fired, and the plug-in works as expected.
This posting is provided "AS IS" with no warranties, and confers no rights.
Laughing Boy Chestnuts Pre-School
Comments
Anonymous
August 05, 2010
GREATEAnonymous
February 24, 2011
Great posting - do you think it will migrate to CRM 2011?Anonymous
February 24, 2011
Given the huge UI changes we made in CRM 2011, I am almost certain that the answer will be no :-(