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.

image

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.

image

!!!…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.

  1. In the CRMWeb\CS\cases\ folder on your CRM server, locate the file dlg_closecase.aspx
  2. Using Visual Studio, Notepad, or any text editor of your choice, edit this file.
  3. Find the statement if (typeof(LOCID_CONFIRM_ACTIVITIES)!="undefined") statement
  4. Directly below this statement, comment out the lines alert(LOCID_CONFIRM_ACTIVITIES); and window.close();
  5. 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

srh.crm.plugin.incident.zip