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

Comments (3)

  1. purwar says:

    GREATE

  2. Gavin says:

    Great posting – do you think it will migrate to CRM 2011?

  3. Simon Hutson says:

    Given the huge UI changes we made in CRM 2011, I am almost certain that the answer will be no 🙁

Skip to main content