Dynamics 365 Plugin for Capturing Marketing List Member Events with C#

As you may know accessing the hidden entity listmember (Marketing List Members) is not possible via the SDK when writing a Dynamics 365 plugin.  After digging around from site to site, searching Google till my eyes popped out, I uncovered a couple unique message names that one can attach to the parent entity list (Marketing List) that are triggered when adding or removing contacts to a static marketing list.  The reason that I needed to capture when a contact was either added or removed from a marketing list was that I needed to modify some contact field information depending upon the type of event that occurred.

What I’ll try to show in this article is how one would interact with a static marketing list when adding or removing contacts and describe those events that follow.  After that I’ll dive into an example plugin written in C# that will show how a plugin can interact with a static marketing list when those users are added or removed.  I won’t write a full plugin since there are many ways to write one and plenty of examples on the web.  What I’ll do is show the important pieces when dealing with the C# code.

Managing Marketing List Members

When adding contacts to a marketing list it’s advised that you use either the “Add using Lookup” option or “Add using Advanced Find” option to properly trigger the required events.  There is one caveat to this process, if selecting the “Add using Advanced Find” method, only select the top option “Add only selected members to the marketing list” even if you plan on adding everyone that was returned from the advanced find query.  The second option “Add all the members returned by the search to the marketing list” fires an event that the SDK does not currently have access to nor supports at this time.  I haven’t heard of any future plans from Microsoft on supporting this event via the SDK when dealing with plugins either. 

Event Messages

There are three event messages that one can listen for.  Those messages include AddMember, AddListMembers, and RemoveMember.  Each are triggered at different times in the interface depending upon the actions of the user.  I’ll also walk-through a scenario where one such action does not trigger anything because the message is not exposed to the SDK as described in the previous section, thus when that scenario is in-acted there are no workarounds at the moment.  You’ll have to make note of that issue so that users can avoid venturing down that road.

Message: AddMember

  • Dynamic Marketing Lists
    • Plugin does not trigger for dynamic lists.
  • Static Marketing Lists
    • *** Plugin was triggered via “Add Using Lookup” ***
    • Add Members via “Add Using Advanced Find”, plugin DID NOT trigger with “Add only selected members to the marketing list”
    • Add Members via “Add Using Advanced Find”, plugin DID NOT trigger with “Add all the members returned by the search to the marketing list” – Currently there is no way to capture this event.

Message: AddListMembers

  • Dynamic Marketing Lists
    • Plugin does not trigger for dynamic lists.
  • Static Marketing Lists
    • Plugin DID NOT trigger via “Add Using Lookup”
    • *** Add Members via “Add Using Advanced Find”, plugin triggered with “Add only selected members to the marketing list” ***
    • Add Members via “Add Using Advanced Find”, plugin DID NOT trigger with “Add all the members returned by the search to the marketing list” – Currently there is no way to capture this event.

Message: RemoveMember

  • Dynamic Marketing Lists
    • Plugin does not trigger for dynamic lists.
  • Static Marketing Lists
    • From Contact Association Screen, select the contacts and click “Remove from Marketing List” 

C# Example Code

In this example I am first checking to see if the plugin fired on the list (Marketing List) entity by checking to see if the context contains the input parameter ListId.  If so, grab the value that is of type GUID and then use that to return the list name that the contact was either added to or removed from.

        if (context.InputParameters.Contains("ListId")&&context.InputParameters["ListId"] is Guid) {
          localContext.Trace(String.Format(CultureInfo.InvariantCulture,"{0}: Retrieving List Variables...", this.ChildClassName));
         
          var listId=(Guid) context.InputParameters["ListId"];
          localContext.Trace(String.Format(CultureInfo.InvariantCulture,"{0}: Found ListId: {1}", this.ChildClassName, listId));

          var lookupList = service.Retrieve(CrmEarlyBound.List.EntityLogicalName, listId, new ColumnSet(CrmEarlyBound.List.Fields.ListName));
          var listName = lookupList.GetAttributeValue<string>(CrmEarlyBound.List.Fields.ListName) ?? String.Empty;

          localContext.Trace(String.Format(CultureInfo.InvariantCulture,"{0}: List Information -- ListId: {1}; ListName: {2}", this.ChildClassName, listId, listName));

Once I’ve established a positive id on the list and have verified that the list name meets my requirements, I’ll need to grab one more important input parameter provided by the context called MessageName.  Below is an example code block for the AddMember message.  The RemoveMember message usage is the same as AddMember and both process one contact at a time.

if (context.MessageName==Common.AddMember) {
  var entityId=(Guid) context.InputParameters["EntityId"];

  localContext.Trace(String.Format(CultureInfo.InvariantCulture,"{0}: Found EntityId: {1}", this.ChildClassName, entityId));

  var lookupContact=service.Retrieve(CrmEarlyBound.Contact.EntityLogicalName,entityId,new ColumnSet( CrmEarlyBound.Contact.Fields.FullName, CrmEarlyBound.Contact.Fields.new_someField2));
  var contactFullName=lookupContact.GetAttributeValue<string>(CrmEarlyBound.Contact.Fields.FullName);
  var someField1=lookupContact.GetAttributeValue<Boolean>(CrmEarlyBound.Contact.Fields.new_someField1);
  var someField2=lookupContact.GetAttributeValue<Boolean>(CrmEarlyBound.Contact.Fields.new_someField2);

  localContext.Trace(String.Format(CultureInfo.InvariantCulture,"{0}: Entity Information -- EntityId: {1}; FullName: {2}; Field 1: {3}; Field 2: {4}", this.ChildClassName, entityId, contactFullName, someField1, someField2));

  if (someField1.Equals(false)||someField2.Equals(false)) {
    lookupContact[CrmEarlyBound.Contact.Fields.new_someField1] = true;
    lookupContact[CrmEarlyBound.Contact.Fields.new_someField2] = true;

    localContext.Trace(String.Format("{0}: ***Ready To Update", this.ChildClassName));
    service.Update(lookupContact);
    localContext.Trace(String.Format("{0}: ***Updated", this.ChildClassName));
  }
}

The final message AddListMembers is similar in nature but instead of parameters for an individual contact the parameter returns an array of member ids.  Casting the input parameter to type GUID[ ] is the way to handle this input parameter.  Once you have created your array you can then cycle through each value using a foreach loop.

if (context.MessageName==Common.AddListMembers) {
  var memberIds=(Guid[]) context.InputParameters["MemberIds"];

  foreach (var memberId in memberIds) {
    localContext.Trace(String.Format(CultureInfo.InvariantCulture,"{0}: Found MemberId: {1}", this.ChildClassName, memberId));

    var lookupContact = service.Retrieve(CrmEarlyBound.Contact.EntityLogicalName, memberId, new ColumnSet( CrmEarlyBound.Contact.Fields.FullName, CrmEarlyBound.Contact.Fields.new_someField2)); 
    var contactFullName = lookupContact.GetAttributeValue<string>(CrmEarlyBound.Contact.Fields.FullName); 
    var someField1=lookupContact.GetAttributeValue<Boolean>(CrmEarlyBound.Contact.Fields.new_someField1); 
    var someField2=lookupContact.GetAttributeValue<Boolean>(CrmEarlyBound.Contact.Fields.new_someField2);

    localContext.Trace(String.Format(CultureInfo.InvariantCulture,"{0}: Entity Information -- MemberId: {1}; FullName: {2}; Field 1: {3}; Field 2: {4}", this.ChildClassName, memberId, contactFullName, someField1, someField2));
    
    if (new_someField1.Equals(false)||new_someField2.Equals(false)) {
      lookupContact[CrmEarlyBound.Contact.Fields.new_someField1] = true;
      lookupContact[CrmEarlyBound.Contact.Fields.new_someField2] = true;

      localContext.Trace(String.Format("{0}: ***Ready To Update", this.ChildClassName));
      service.Update(lookupContact);
      localContext.Trace(String.Format("{0}: ***Updated", this.ChildClassName));
    }
  }
}

Food For Thought:

ExecuteMultipleRequest Object

Keep in mind that I’m taking the easy way out and simply looping though and updating a single contact at a time.  A more efficient way is to use the ExecuteMultipleRequest object and build the statement out where you can execute in bulk.

I’ve provided an example of how one would use the ExecuteMultipleRequest object in conjunction with the AddMemberListRequest object.  This object is used when adding multiple contact members to multiple static marketing lists.  This same code can be used when you want to remove multiple contacts from multiple static marketing lists by replacing the above with the RemoveMemberListRequest object.

var multipleRequest = new ExecuteMultipleRequest() {
  Settings = new ExecuteMultipleSettings() {
    ContinueOnError = false
   ,ReturnResponses = true
  }
 ,Requests=new OrganizationRequestCollection()
};

foreach (var entity in myCollection.Entities) {
  /* Do some stuff here */

  var addRequest = new AddMemberListRequest {
    EntityId=theEntityId
   ,ListId=theListId
  }
}

if (multipleRequest.Requests.Count>0) {
  var multipleResponses = (ExecuteMultipleResponse) service.Execute(multipleRequest);
  var responses = (ExecuteMultipleResponseItemCollection) multipleResponses.Results["Responses"];

  foreach (var responseItem in responses) {
    localContext.Trace(responseItem.Fault != null ? String.Format("{0}: Multiple Request Response Item Returned a fault that has occurred", this.ChildClassName) : String.Format("{0}: Item(s) purged successfully", this.ChildClassName));
  }
}

Keep in mind that when using this method the ExecuteMultipleRequest will return a ExecuteMultipleResponse object.  You’ll use this object to verify each record was processed successfully and if not will provide you with some information on the error.

 

Advertisements

Dynamics 365: Phone Number Formatting with JavaScript and RegEx

The default number formatting in Dynamics 365 can be somewhat limiting.  If you are like me, I actually don’t employ jQuery, I use straight up JavaScript for most of my custom interactions with form data.  Yes, I know it sounds crazy but for the longest time Microsoft told us not to use jQuery and even though it’s now built into the product, I still use straight JavaScript.

There are plenty of examples of how to interact with phone number formatting on the net.  Many of them are much more extensive.  However, my goal was to create something that could handle about 95% of the numbers that we would record in our Account and Contact data.  Of course this handles a variety of input values for U.S. numbers but it also takes a general approach to international numbers as well, most of them anyways.

Environment: Dynamics 365 Online v9.0+

There are 3 main sections to the code.  The first section handles the possibility that there might be some type of extension number attached to the phone number we are trying to format.  Now, even though formatting the extension is possible, it will inadvertently cause the number to fail dialing out via Microsoft Lync.  Any number that has a “+” sign in front of it allows the number to be dialed directly via the Microsoft Lync interface in conjunction with Microsoft Dynamics 365 by clicking the number as if it were a URL link. However, extension numbers tacked on the end of that number breaks the dialing process.  So keep that in mind when rolling this out as a possible policy issue when making numbers available to Microsoft Lync that may also have extensions attached to them.

Regular Expressions

What is this thing called a regular expression?  A regular expression is a special type of search string that describes a search pattern.  This search pattern can be used by any type of programming language, text editor, or 3rd party product that can utilize RegEx expressions.  My example uses JavaScript and I have broken up each section of the code below that performs a particular function when dealing with a phone number.

Options

There are many different RegEx Options one can choose from.  Depending upon how you want to read your string expression will determine what type of options you set in your code.  I have provided a picture of the options that you are presented with when testing your RegEx strings in the online tool for the website Regular Expressions 101.  We will mostly be dealing with the option “i” for case insensitive matching.  Two others options that you may deal with more that say the rest would be the top ones, global “g” and multi line “m”.

Section: Extensions

Since dealing with extensions creates a few unknowns, handling them first by identifying and thus stripping them from the rest of the phone number is our first priority.  This will insure a smooth outcome when breaking up the rest of the phone number.  As you can see below, the RegEx string contains 3 main parts.  Then we set the RegEx Option “i” because we want to cover upper and lower case items without the need to type in every possible variant.  For example,  someone could have typed Ext or ext; so keep it simple.

The first part (ext|ex|x|ext.|ex.|x.) looks for any of the standard ways one might type the abbreviation of extension. The second part *\s* accounts for any number of spaces one might find between the abbreviated form of extension and the actual extension number. The third and final part (\d{2,5}))?$ looks for a pattern of digits numbering from 2-5 in length, starting from right to left as designated by the $ character.  This can be adjusted for your organization depending upon what pattern you would like to support.

  • Regular Expression: /((ext|ex|x|ext.|ex.|x.)*\s*(\d{2,5}))?$/i
  • Example: Ext. 1234
  • Group 1: (ext|ex|x|ext.|ex.|x.) =  Ext.
  • Group 2: (\d{2,5}))?$ = 1234
 
var extPart="";
let extMatch=fieldObject.match(/((ext|ex|x|ext.|ex.|x.)*\s*(\d{2,5}))?$/i);
if(extMatch[2]!="undefined"&&extMatch[2]!=null) {//     Array.length=4, Alpha in array member with index=2
  extPart=" x".concat(extMatch[0].replace(/[^0-9]/g,'')); /* Remove any non-numeric characters */
  //alert(extPart);
}

Section: Remove Alpha Characters

In order for a clean RegEx search string one needs to remove all of the unnecessary characters that might end up in a partially formatted phone number.  This RegEx string removes all of the characters not defined as a digit.  This is key as it removes all the dashes, periods, parentheses, spaces, and anything else that one could add into a phone number.

  • Regular Expression: /[^0-9]/g
  • Example: 1 (800) 123-4567
  • Returned: 18001234567
 
var tempObject=tempObjectMatch[0].replace(/[^0-9]/g,''); /* Remove any non-numeric characters */
//alert(tempObject);

Section: Grab Actual Number

This section of the code is needed to grab the actual number in case there actually is an extension tacked on at the end of the phone number.

  • Regular Expression: /[^a-z]+/i
 
let tempObjectMatch=fieldObject.match(/[^a-z]+/i); /* Grab everything up until first alpha character */
//alert(tempObjectMatch[0]);

Section: Standard Number

The standard number section simply has 2 groups to pull.  The two groups retrieved via the search string d{?} where the ? are the number of digits starting at 1, not zero based.  So {3} will return 3 digits. In the international number section i actually show the zero based option.

  • Regular Expression: /(\d{3})(\d{4})/g
  • Example: 1234567
  • Group 1: (\d{4}) =  4567
  • Group 2: (\d{3}) = 123
 
if(tempObject.length==7) {
  Xrm.Page.getAttribute(fieldName).setValue( tempObject.replace(/(\d{3})(\d{4})/g,"$1-$2").concat(extPart));
  Xrm.Page.getControl(fieldName).clearNotification();
}

Section: Standard Number w/ Area Code

Here we add on a third grouping with the standard number that includes the area code.

  • Regular Expression: /(\d{3})(\d{3})(\d{4})/g
  • Example: 3211234567
  • Group 1: (\d{4}) =  4567
  • Group 2: (\d{3}) = 123
  • Group 3: (\d{3}) = 321
 
} else if(tempObject.length==10) {
  Xrm.Page.getAttribute(fieldName).setValue( tempObject.replace(/(\d{3})(\d{3})(\d{4})/g,"+1 ($1) $2-$3").concat(extPart));
  Xrm.Page.getControl(fieldName).clearNotification();
}

Section: Standard Number w/ Area Code & 1

The final grouping we add the ability to grab the 1 off the number.

  • Regular Expression: /(\d{1})(\d{3})(\d{3})(\d{4})/g
  • Example: 13211234567
  • Group 1: (\d{4}) =  4567
  • Group 2: (\d{3}) = 123
  • Group 3: (\d{3}) = 321
  • Group 4: (\d{1}) = 1
 
} else if(tempObject.length==11) {
  Xrm.Page.getAttribute(fieldName).setValue( tempObject.replace(/(\d{1})(\d{3})(\d{3})(\d{4})/g,"+$1 ($2) $3-$4").concat(extPart)); 
  Xrm.Page.getControl(fieldName).clearNotification(); 
}

Section: International Number

The last part of the if statement singles out most International numbers that have a range from 12-15 digits.  The JavaScript code will need to be updated if you require numbers greater than 15 digits.  Below is a sample number with each group results as read using RegEx.  The $ instructs RegEx to begin reading from the end of the string.

The first three groups retrieved via the search string d[0-9] with the {?} where the ? are the number of digits starting at 0 or zero based.  So {3} will return 4 digits.  The last group simple pulls 2-5 digits as stated with the (\d{2,5}) search string.  The $ tells us that we should create our groups from right to left.

  • Regular Expression: /(\d{2,5})(\d[0-9]{1})(\d[0-9]{3})(\d[0-9]{3})$/g
  • Example: 449812345678
  • Group 1: (\d[0-9]{3}) =  5678
  • Group 2: (\d[0-9]{3}) = 1234
  • Group 3: (\d[0-9]{1}) = 98
  • Group 4: (\d{2,5}) = 44
} else if(tempObject.length>11&&tempObject.length<16) {
  Xrm.Page.getAttribute(fieldName).setValue( tempObject.replace(/(\d{2,5})(\d[0-9]{1})(\d[0-9]{3})(\d[0-9]{3})$/g,"+$1 $2 $3 $4").concat(extPart));
  Xrm.Page.getControl(fieldName).clearNotification();
}

JavaScript Solution

 
if(typeof (MyCode.Common)=="undefined") {
  MyCode.Common={__namespace:true};
}
Common.FormatPhoneNum ={
  OnAttributeChange:function(context) {
    var fieldES=context.getEventSource();
    if(typeof (fieldES)!="undefined"&&fieldES!=null) {
      var fieldName=context.getEventSource().getName();
      var fieldObject = Xrm.Page.getAttribute(fieldName).getValue();
      if(typeof (fieldObject)!="undefined"&&fieldObject!=null) {
        var extPart="";
        let extMatch = fieldObject.match(/((ext|ex|x|ext.|ex.|x.)*\s*(\d{2,5}))?$/i);
        if(extMatch[2]!="undefined"&&extMatch[2]!=null) {// Array.length=4, Alpha in array member with index=2
          extPart=" x".concat(extMatch[0].replace(/[^0-9]/g,'')); /* Remove any non-numeric characters */
          //alert(extPart);
        }
        let tempObjectMatch=fieldObject.match(/[^a-z]+/i); /* Grab everything up until first alpha character */
        //alert(tempObjectMatch[0]);
        var tempObject=tempObjectMatch[0].replace(/[^0-9]/g,''); /* Remove any non-numeric characters */
        //alert(tempObject);
        if(tempObject.length==7) {
          Xrm.Page.getAttribute(fieldName).setValue( tempObject.replace(/(\d{3})(\d{4})/g,"$1-$2").concat(extPart));
          Xrm.Page.getControl(fieldName).clearNotification();
        } else if(tempObject.length==10) {
          Xrm.Page.getAttribute(fieldName).setValue( tempObject.replace(/(\d{3})(\d{3})(\d{4})/g,"+1 ($1) $2-$3").concat(extPart));
          Xrm.Page.getControl(fieldName).clearNotification();
        } else if(tempObject.length==11) {
          Xrm.Page.getAttribute(fieldName).setValue( tempObject.replace(/(\d{1})(\d{3})(\d{3})(\d{4})/g,"+$1 ($2) $3-$4").concat(extPart));
          Xrm.Page.getControl(fieldName).clearNotification();
        } else if(tempObject.length>11&&tempObject.length<16) {
          Xrm.Page.getAttribute(fieldName).setValue( tempObject.replace(/(\d{2,5})(\d[0-9]{1})(\d[0-9]{3})(\d[0-9]{3})$/g,"+$1 $2 $3 $4").concat(extPart));
          Xrm.Page.getControl(fieldName).clearNotification();
        }
      }
    }
  }
};

Finally, always make sure to wrap your JavaScript code before importing into the global namespace.  This will keep your code and other 3rd party code free of conflicts.

Helpful Resources:

Transition Between Dynamics 365 Business Process Flow Stages using a Workflow Activity and C# – Part 4

In the previous article Transition Between Dynamics 365 Business Process Flow Stages using a Workflow Activity and C# – Part 3, I showed you how install your custom workflow activity using the Microsoft Plugin Registration Tool.  Next I’ll discuss a major sticking point that I experienced after deploying my workflow activity to production.  There was very little information out there concerning this error and thus had to open a case with Microsoft Support.

Workflow Activity Error: Does Not Exist – After Upgrade to Dynamics 365 v8.2

Below is the Plugin Trace output from the Business Process Flow workflow activity that we created and deployed in this series of blog posts.  After deploying to production I noticed errors in the log.  The output includes a rather obscure error message that occurs when initiating a switch to a new Business Process Flows.  I’ll describe in detail what the log shows and highlight the key points that should provide insight into why this error is occurring after a version update to 8.2 for Microsoft Dynamics 365 Online.

Plugin Trace:

[CnCrm.WfActivities: CnCrm.WfActivities.SetBizProcessFlowV2]
[CnCrm.WfActivities (1.0.0.0): CnCrm.WfActivities.SetBizProcessFlowV2]

SetBizProcessFlowV2.Execute(): ActivityInstanceId: 1; WorkflowInstanceId: d0a80194-ce48-490c-bae8-1ce75a8e7808; CorrelationId: 9bc1a247-172c-4a47-93b6-d439e20fe868; InitiatingUserId: dc4e4a9c-072b-e011-8c2a-1cc1dee8dad5 -- Entering
SetBizProcessFlowV2: Count of Process Instances concurrently associated with the Entity record: 3
SetBizProcessFlowV2: BPF Definition Name currently set for the Entity record: BizProcFlow1, Id: a7a209f3-ca82-e711-8160-e0071b66dfc1
SetBizProcessFlowV2: Switching to BPF Unique Name: new_bpf_472aceaabf7c4f1db4d13ac3c7076c65, Id: 472aceaa-bf7c-4f1d-b4d1-3ac3c7076c65
SetBizProcessFlowV2: Built BPF Query, Now Executing...
SetBizProcessFlowV2: [&amp;amp;lt;fetch distinct="false" no-lock="true" mapping="logical"&amp;amp;gt;&amp;amp;lt;entity name="workflow"&amp;amp;gt;&amp;amp;lt;attribute name="name" /&amp;amp;gt;&amp;amp;lt;filter type="and"&amp;amp;gt;&amp;amp;lt;condition attribute="uniquename" operator="eq" value="new_bpf_472aceaabf7c4f1db4d13ac3c7076c65" /&amp;amp;gt;&amp;amp;lt;/filter&amp;amp;gt;&amp;amp;lt;/entity&amp;amp;gt;&amp;amp;lt;/fetch&amp;amp;gt;], Message: Update
SetBizProcessFlowV2: Found matching Business Process Flows...
SetBizProcessFlowV2: Successfully retrieved the Business Process Flow that we'll be switching to: BizProcFlow2, Id: 472aceaa-bf7c-4f1d-b4d1-3ac3c7076c65
SetBizProcessFlowV2: ***Ready To Update - Business Process Flow
SetBizProcessFlowV2: ***Updated
SetBizProcessFlowV2: Successfully Switched to 'BizProcFlow1' BPF for the Entity Record.
SetBizProcessFlowV2: Count of process instances concurrently associated with the entity record: 3.
SetBizProcessFlowV2: All process instances associated with the entity record: BizProcFlow1, BizProcFlow2, BizProcFlow3
SetBizProcessFlowV2: Retrieved the BPF Stages in the Active Path of the Process Instance:
SetBizProcessFlowV2: Looping Through Stage #1: STAGE01 (StageId: f0be05ac-51be-498c-b1d5-ebfc036095e2, IndexId: 0)
SetBizProcessFlowV2: Looping Through Stage #2: STAGE02 (StageId: a9fce3a5-d54c-41da-ac9f-56fa7f84404e, IndexId: 1)
SetBizProcessFlowV2: Concerning the Process Instance -- Initial Active Stage Name: STAGE02 (StageId: a9fce3a5-d54c-41da-ac9f-56fa7f84404e)
SetBizProcessFlowV2: Looping Through Stage #3: STAGE03 (StageId: 0c63beac-b1dc-4db5-a4b5-0ffd9ac96cc9, IndexId: 2)
SetBizProcessFlowV2: Looping Through Stage #4: STAGE04 (StageId: 213bdcdf-9ac2-415e-a204-59b261b42162, IndexId: 3)
SetBizProcessFlowV2: Concerning the Process Instance -- Desired New Stage Name: STAGE04 (StageId: 213bdcdf-9ac2-415e-a204-59b261b42162)
SetBizProcessFlowV2: Looping Through Stage #5: STAGE05 (StageId: 350d89b3-df17-48d2-a930-91012686b62f, IndexId: 4)
SetBizProcessFlowV2: Looping Through Stage #6: STAGE06 (StageId: 4c3ed26b-4f86-450b-83c2-1c32d65cc6af, IndexId: 5)
SetBizProcessFlowV2: Looping Through Stage #7: STAGE07 (StageId: d0bbe23f-e72f-48de-9b0e-ce410469a36f, IndexId: 6)
SetBizProcessFlowV2: Looping Through Stage #8: STAGE08 (StageId: d5242580-a6c3-4835-99a1-7b4a6cd910dc, IndexId: 7)
SetBizProcessFlowV2: Number of Stages Shifting Forward: 2
SetBizProcessFlowV2: Setting To Stage #2: STAGE02 (StageId: a9fce3a5-d54c-41da-ac9f-56fa7f84404e, IndexId: 1)
SetBizProcessFlowV2: Fault Exception: An error occurred during Plug-in execution
SetBizProcessFlowV2: Fault Timestamp: 8/19/2017 8:03:33 PM
SetBizProcessFlowV2: Fault Code: -2147220969
SetBizProcessFlowV2: Fault Message: new_bpf_472aceaabf7c4f1db4d13ac3c7076c65 With Id = a7a209f3-ca82-e711-8160-e0071b66dfc1 Does Not Exist
SetBizProcessFlowV2: Fault Inner Exception: Has Inner Fault
SetBizProcessFlowV2.Execute(): ActivityInstanceId: 1; WorkflowInstanceId: d0a80194-ce48-490c-bae8-1ce75a8e7808; CorrelationId: 9bc1a247-172c-4a47-93b6-d439e20fe868 -- Exiting

Error Message:

Unhandled Exception: Microsoft.Xrm.Sdk.InvalidPluginExecutionException: SetBizProcessFlowV2: Plug-in Warning: Manually forcing exception for logging purposes.

Original BPF:

  • Workflow Display Name: BizProcFlow2
  • BPF Unique Name: new_bpf_a7a209f3ca82e7118160e0071b66dfc1
  • Id: a7a209f3-ca82-e711-8160-e0071b66dfc1

Desired BPF:

  • Workflow Display Name: BizProcFlow1
  • BPF Unique Name: new_bpf_472aceaabf7c4f1db4d13ac3c7076c65
  • Id: 472aceaa-bf7c-4f1d-b4d1-3ac3c7076c65

As you can see the Plugin Trace output displays our desired Business Process Flow that the workflow activity is attempting to switch too (Successfully retrieved the Business Process Flow that we’ll be switching to: BizProcFlow2, Id: 472aceaa-bf7c-4f1d-b4d1-3ac3c7076c65).  After the supposed switch we see another line stating the update was successful, yet when we look at the output displaying the new Business Process Flow we see the original value of BizProcFlow1.  A couple lines later I output a list of associated processes and Microsoft’s own documentation states that the first process returned is the active process further confirming that the BPF process hasn’t been modified (All process instances associated with the entity record: BizProcFlow1, BizProcFlow2, BizProcFlow3).  Lastly, when you look at the actual entity record in Dynamics 365 it also displays the original process BizProcFlow1 as still active on the entity.

Now with all that said I’m sure you are thinking well yes, the code doesn’t work so fix it.  Well, that is really where it gets a bit weird.  Let me explain, if you scroll down the the actual error message displayed in the log we see the following:

SetBizProcessFlowV2: Setting To Stage #2: STAGE02(StageId: a9fce3a5-d54c-41da-ac9f-56fa7f84404e, IndexId: 1)
SetBizProcessFlowV2: Fault Exception: An error occurred during Plug-in execution
SetBizProcessFlowV2: Fault Timestamp: 8/19/2017 8:03:33 PM
SetBizProcessFlowV2: Fault Code: -2147220969
SetBizProcessFlowV2: Fault Message: new_bpf_472aceaabf7c4f1db4d13ac3c7076c65 With Id = a7a209f3-ca82-e711-8160-e0071b66dfc1 Does Not Exist

Compare the ids with the Original BPF and Desired BPF values above and you’ll begin to understand why I said the word weird (The Business Process Flow with Unique Name: new_bpf_472aceaabf7c4f1db4d13ac3c7076c65 with the Id: a7a209f3-ca82-e711-8160-e0071b66dfc1 Does Not Exist).  Well yeah duh, it doesn’t exist because it’s literally impossible for it to exist! My only explanation was that the initial update seems to stall midway.  While not a legal update it some how made it through and is now referencing both unique name of the desired process while still referencing the Id of the original process.

Microsoft Support

Yes, after discovering this problem I soon realized no amount of code was going to dig me out of this hole.  So I called up my friendly Microsoft Support person and started down the long support road.  All I had to go on was a blog posting by another individual with what seemed to be an issue identical to mine.  There resolution too was to call up Microsoft Support where they determined that “some scripts” needed to be run on the back-end to resolve the issue.  Apparently these magical scripts of which I have no reference too seemed to do the trick.  I stated this to my Microsoft Support Engineer assigned to my case and he concurred that he too had heard of this issue.  After researching he ran the scripts that would effectively fix our solution as it had done in the past for others.

Unfortunately for our solution it did not work and our case was elevated to higher tier of support.  After a couple of weeks of debugging with support I found that this was an issue with the underlying process of switching BPFs.  It also had to do with whether this was a synchronous process or an asynchronous process.  My support engineer was able to get things working immediately if everything was set to synchronous but of course Microsoft’s own best practices states the opposite.  He also was able to get things working in a simple example.  However when I set up the same test I was getting mixed results.  It would work sometimes but other times it would not and it all had to do with how the record was being saved.  Bare with me for a moment, here are the scenarios, none of which raised any errors.

Scenario 1:

I create a new record, click save and close the record.  The process would work, the BPF would change to the correct one and the correct stage would be selected.  It seemed like as long as immediately saved the record without making any more modifications the process would switch and everything would work as intended.

Scenario 2:

I create a new record, click save and not close the record but instead wait a few and refresh, the process never completes.  I literally waited and kept clicking refresh, nothing happened.  I checked the logs and it looked like everything worked but in fact nothing was farther from the truth.  I consistently replicated this issue over and over again.  I even tried different browsers and different computers thinking something was up with my testing machine.

Microsoft Hotfix

I stated this to the Microsoft engineers and later was told that they would move forward in creating an investigation for a possible code defect.  A few days later I was told that the code defect request was successfully placed with the development team and then was approved for a hotfix release at some future date.  So there you go, hopefully  interactions with the Business Process Flow will be more stable in the near future.

Related Articles

Dynamics 365: How To Sync Field Changes In Excel Templates That Display Dynamic Data

Introduction

Have you ever created an Excel template for displaying dynamic data from a custom entity view? Then decide to do a bunch of custom work to that Excel template to satisfy your user base. Things like graphs, pivot tables, charts, KPIs, Power Query data references to 3rd party sources just to name a few. Then after all that hard work you upload that customized gem back into Dynamics 365 only to realize you forgot the most import field that connects the dots! You know that xyz field that helps you calculate gross income by client or whatever that key field is that you unfortunately forgot. Better yet you just want to add more fields for additional calculations but you simply forgot to include them in your original Dynamics 365 view. How are you going to get these new fields to display in your dynamic data source of your Excel template without having to rebuild the entire template?

Problem

The problem here is that Microsoft Dynamics 365 doesn’t seem to have a friendly option to simply refresh the dynamic dataset that the Excel template references. The Excel template dataset is pointed to a custom view you originally created the template from but finding that is like searching for a needle in a haystack. The custom view provides the default fields for your new Excel template. However, adding or removing fields from the Dynamics 365 view is not reflected in your Excel template the next time you download it. It seems to be an afterthought on Microsoft’s part. My coworker told me that she swore at one point, possibly for about a week, one could click a refresh button from within the Excel template and the dynamic dataset would magically pull in all the field changes that took place in the referenced view. However, for whatever reasons Microsoft pulled that functionality out of the product and I have not heard or seen any plans of getting that functionality back.  So what do we do now? How does one get the Excel template to reflect the field changes that took place in the referenced view?

Solution

After some trial and error I was able to find a rather simple solution to this rather ugly problem. The issue I had with the initial Excel template file was that some type of reference had to be stored in order for the template to point back to the Dynamics 365 data where it can properly pull the required new data. It may no longer need the view after you’ve uploaded your snazzy template back into Dynamics 365 but it still needs to know how to pull the data when you make that next template pull request. So here are the steps to get this all working and hopefully your nerves can calm a bit knowing future changes are doable.

First off you need to activate the Developer Ribbon in Excel if it’s not already visible in the Office Ribbon. Skip this part if you already see it.

Next, click the View Code button, which will launch the Microsoft Visual Basic for Applications tool in a separate window.

Once the project opens you’ll see a list of objects associated with this project template. Below is a screenshot of an Excel template that only has one worksheet from the initial export from Dynamics 365. Your template could have many worksheet objects depending upon the level of customization. You’ll notice 3 objects below in the project window. The top one will be that initial worksheet created from the Dynamics 365 template export. The next one is a special worksheet and the one that requires our attention. The last one that reads ThisWorkbook we can simply ignore.

Below that window are the properties for each spreadsheet. When you click on the hiddenDataSheet and view the properties you’ll notice that the visibility setting is set to 2 – xlSheetVeryHidden meaning there is no way to make the worksheet visible unless you open up the Developer window  You need to set that value to -1 – xlSheetVisible.

Once you do that the sheet in question is now visible in your Excel template.  Minimize the Developer window for now and select the hiddenSheet.

At the top of this sheet is a rather long string of names, yes those are the original view fields pulled when the Excel template was first created. You should copy this into some type of program like Notepad and save this backup just in case you accidentally insert a typo, thus having something to fallback too if needed.

From here you need to follow the simple syntax in order to add additional fields to this hidden datasource. These new fields should exist in your referenced view. That means if you haven’t already added or removed the fields you need in the reference view then do so now. Once that’s completed use a tool like Metadata Explorer from the Dynamics 365 SDK or go into the System Settings –> Customizations where you can obtain the schema names for each of the new fields. Then add them to the end of the string of fields you see in the first cell on the hiddenSheet.

Syntax Example

  • The syntax should read as follows, & then Schema Name and then finally the Friendly Name.
  • Example: &new_schemaname=Friendly%20Name
  • Any special characters like spaces will need to be URL encoded and that means a space would equal ‘%20’. For additional information on how to create URL encoded strings visit this site – URL Encode/Decode Tool.

Once you have completed that then go back to the properties for the hiddenSheet and set the visibility back to the original value of 2 – xlSheetVeryHidden, then save and close the Developer window.

The final piece is to alter the original worksheet that contains the data from Dynamics 365. This is where the Friendly Names come into play. When the data is pulled the Friendly Name matches back to the Friendly Name located in the hiddenSheet. If you skip this step or forget to do this part of the workaround then on export the data will in fact pull since the dataset had been altered but without the new columns there won’t be a place for this new data to reside. Make sure to insert all your new fields in front of the last field so each new column will inherit all the cell properties that exist. Please view the examples screenshots below.

Right-click on the last column and insert new.

Cut and Paste the last column in front of the newly created column to keep the view order synced with what is defined in the Dynamics 365 view.

Once you have added all the Friendly Name columns to Excel then save and upload your Excel template back into Dynamics 365. Now the next time you download the Excel template it will contain all your field changes including the data.

…YES!

Additional Information

 

Convert Your QueryExpression To FetchXml For Testing in Dynamics 365

Have you had issues with your QueryExpression object but placement of your execution method seems to cause an error lacking any real detail in the log output?  This can cause quite the headache creating roadblocks when trying to determine what part of the query is causing the failure.  For an easy way to take the constructed QueryExpression object and transform it to a readable FetchXML string that one could execute in a tool such as XrmToolBox, simple copy and paste the #region section below.

//*** Your Query
var qe = new QueryExpression {
  EntityName = CrmEarlyBound.MyEntity.EntityLogicalName,
  ColumnSet = new ColumnSet(new string[] { CrmEarlyBound.MyEntity.Fields.MyEntityId, CrmEarlyBound.MyEntity.Fields.MyEntityField1,CrmEarlyBound.MyEntity.Fields.MyEntityField2 }),
  Criteria = new FilterExpression {
    FilterOperator = LogicalOperator.And,
    Conditions = {
      new ConditionExpression {
        AttributeName = CrmEarlyBound.MyEntity.Fields.RegardingObjectId, Operator = ConditionOperator.Equal, Values = { someGuidId }
      },
      new ConditionExpression {
        AttributeName = CrmEarlyBound.MyEntity.Fields.MyEntityTypeCode, Operator = ConditionOperator.Equal, Values = { maybeBadValue1 } 
      }, 
      new ConditionExpression {
        AttributeName = CrmEarlyBound.MyEntity.Fields.StateCode, Operator = ConditionOperator.Equal, Values = { maybeBadValue2 }
      }
    }
  },
  NoLock = true,
  Distinct = false
};

#region Convert Query Expression to FetchXML
var conversionRequest = new QueryExpressionToFetchXmlRequest {
  Query = qe
};
var conversionResponse = (QueryExpressionToFetchXmlResponse)serviceProxy.Execute(conversionRequest);
var fetchXml = conversionResponse.FetchXml;

tracingService.Trace("{0}: [{1}], Message: {2}", CHILD_CLASS_NAME, fetchXml, context.MessageName);

#endregion Convert the query expression to FetchXML.

This is helpful when working with objects like plug-ins and workflow activities in Dynamics 365.  Just make sure to paste the below code after you define QueryExpression object but before the actual execution of that query code by the IOrganizationService object.  That way the fully constructed FetchXml query can be pulled from the trace log and pasted into one of the many useful plug-ins for XrmToolBox.  One such plugin is called FetchXml Tester and can be quite useful in data discovery and query debugging.

Transition Between Dynamics 365 Business Process Flow Stages using a Workflow Activity and C# – Part 3

In the previous article Transition Between Dynamics 365 Business Process Flow Stages using a Workflow Activity and C# – Part 2, I showed you how create a custom workflow activity for switching Business Process Flows in code along with transitioning the associated Stages of those Business Process Flows.  In Part 3 of this series I’ll quickly talk about how to deploy and use your new custom workflow activity.

How to Deploy and Use Your New Custom Workflow Activity

Now that you have finished coding and testing your new workflow activity it simply needs to be deployed to production.  Once the workflow activity has been deployed successfully to your solution then anyone with permissions to create and update workflows will be able to start using it.  I currently don’t have any fancy helper projects to ease the deployment process for my various plug-ins and workflow activities at the moment so I’ll just use the Microsoft Dynamics 365 SDK provided tool called the Microsoft Plugin Registration Tool.  For the latest version of the Plugin Registration Tool simply download the latest version of the Microsoft Dynamics 365 SDK.  Once you have downloaded and installed the SDK, navigate to and launch the Plugin Registration Tool.

Tool Path: “..\SDK\Tools\PluginRegistration\PluginRegistration.exe”

Select the first option of Create New Connection. Follow the process of registering a new connection to your production Dynamics 365 system, whether it be the online or on-premise version, it doesn’t matter.  Once you have successfully created a connection then select the Register button.  Concerning workflow activities, the Install Assembly and Update Assembly screens are virtually the same.  Below is a screenshot of how to update the assembly process but don’t worry, it’s the same either way. You’ll either select Register for new assemblies or Update when changes are made. Navigate to your custom workflow assembly debug or release folder and complete the registration process.

Incorporating the Workflow Activity

I’ll assume that you already know how to create workflows in Dynamics 365.  Once you have created the necessary steps in your workflow and have determined where best to insert your new custom workflow activity step for modifying the Business Process Flow and/or Stages then select the Add Step drop-down menu and scroll down to the bottom section where your custom workflow activity should be waiting.

Once the step has been inserted click the Set Properties button.  This will take you to the required parameters for the custom workflow activity.  As you can see the first parameter shows up as an entity reference lookup.  This is where you’ll select the correct Business Process Flow that you want to either switch too or would like to transition Stages for.  The second parameter is for the BPF Stage Name that you wish to transition too depending upon what your first parameter was set too.

Make sure to adjust your filter search to Business Process Flow.  I believe the search criteria defaults to Workflows and Actions.  You’ll need to modify the Look in drop-down section.

How to Deploy and Use Your New Custom Workflow Activity - Pic 4

Your custom workflow activity step should look something like this and make sure to place a good description in the label of the step for your workflow to keep the process transparent for everyone.

Once the workflow step has been properly configured then the step will resemble something like this below in your workflow.

How to Deploy and Use Your New Custom Workflow Activity - Pic 6

Related Articles

Transition Between Dynamics 365 Business Process Flow Stages using a Workflow Activity and C# – Part 2

In the previous article Transition Between Dynamics 365 Business Process Flow Stages using a Workflow Activity and C# – Part 1, I discussed the need for Business Process Flow and related Stage transitioning in code.  I also showed a previous version of the workflow activity that used LINQ. With Microsoft CRM’s recent rebranding (now Dynamics 365), how you interact with the code base has changed and problems can occur.  With that I described a recent problem centered around transitioning stages that were out of sequence.  These stages also may or may not have had any field requirements assigned to them.  This issue seems to have surfaced from a recent version update that has since closed the window on a long outstanding bug. You are now only allowed to transition phases in order and you must complete all the field requirements before transitioning further. If you do not follow these rules then you’ll get the dreaded Invalid Stage Transition error.

Business Process Flow (BPF) and Stage Transitioning

Microsoft’s Developer Network Documentation: Model Business Process Flows

Keep in mind that the overall code can be condensed as I’ve inserted excessive tracing in order to convey how each section of the code works. I also use a tool called XrmToolBox along with the plugin DLaB Early Bound Generator for creating the necessary class objects that I interact with. Large sections of the code were taken from the provided Microsoft Documentation so kudos to the Microsoft team that created it. However, there are a few code lines here and there that I found rather obscure. I’ll try to dive into those sections in greater detail along with explaining the need for what seems to be randomly placed System.Threading.Thread.Sleep methods; for my Dynamics 365 Online instance, they seem to be valid. However, take them with a grain of salt when applying them to both your development and production instances as your systems speed and efficiency may differ greatly with mine. Development is usually slower and may need a longer pause than production.

Note: Please keep in mind that when you see the abbreviation “BPF”, I’m really just saying “Business Process Flow”.

Now, lets get to the solution.  This bit of code below is used twice in the solution, this first instance was used to pull a collection of BPFs associated with the parent entity. Depending upon how many times the parent entity has switched BPFs will determine the processCount in the returned response.

var processInstancesRequest = new RetrieveProcessInstancesRequest {
  EntityId = theEntity.Id,
  EntityLogicalName = theEntity.LogicalName
};

var processInstancesResponse = (RetrieveProcessInstancesResponse)serviceProxy.Execute(processInstancesRequest);
var processCount = processInstancesResponse.Processes.Entities.Count;

Assuming you have at least one BPF already associated to the parent entity (and you should so no worries), we can move beyond the count check. The next bit of code simply provides tracing details and retrieves our first input variable BpfEntityReference. From that we can pull the Unique Name, which in turn we’ll use to build our QueryExpression that’ll execute against the Workflow entity. The query is used to retrieve the required Id field in the Workflow entity of the BPF we plan on switching too.

if (processCount < 0) 
  tracingService.Trace("{0}: Count of Process Instances concurrently associated with the Entity record: {1}", CHILD_CLASS_NAME, processCount);
  tracingService.Trace("{0}: BPF Definition Name currently set for the Entity record: {1}, Id: {2}", CHILD_CLASS_NAME, processInstancesResponse.Processes.Entities[0].Attributes[CrmEarlyBound.Workflow.Fields.Name], processInstancesResponse.Processes.Entities[0].Id.ToString());
  var bpfEntityRef = this.BpfEntityReference.Get<EntityReference>(executionContext);
  var colSet = new ColumnSet();
  colSet.AddColumn(CrmEarlyBound.Workflow.Fields.UniqueName);
  var bpfEntity = serviceProxy.Retrieve(bpfEntityRef.LogicalName, bpfEntityRef.Id, colSet);

  tracingService.Trace("{0}: Switching to BPF Unique Name: {1}, Id: {2}", CHILD_CLASS_NAME, bpfEntity.Attributes[CrmEarlyBound.Workflow.Fields.UniqueName].ToString(), bpfEntity.Id.ToString());

  var bpfStageName = this.BpfStageName.Get<string>(executionContext).Trim();
  var qe = new QueryExpression {
    EntityName = CrmEarlyBound.Workflow.EntityLogicalName,
    ColumnSet = new ColumnSet(new string[] { CrmEarlyBound.Workflow.Fields.Name }),
    Criteria = new FilterExpression {
      Conditions = {
        new ConditionExpression {
          AttributeName = CrmEarlyBound.Workflow.Fields.UniqueName, Operator = ConditionOperator.Equal, Values = { bpfEntity.Attributes[CrmEarlyBound.Workflow.Fields.UniqueName] } //new_bpf_472aceaabf7c4f1db4d13ac3c7076c65
        }
      }
    },
    NoLock = true,
    Distinct = false
  };

  tracingService.Trace("{0}: Built BPF Query, Now Executing...", CHILD_CLASS_NAME);

  var entColByQuery = serviceProxy.RetrieveMultiple(qe).Entities; //// Execute Query with Filter Expressions

Executing the query should yield at least one result from the Workflow entity. Again, this is the BPF we plan on switching too. We really only need the bpfId field for switching but I also grab the name for tracing purposes.  I really haven’t tested in depth the 2 second pause that has been inserted into the code. It was advised in the Microsoft Documentation but without a reason as to why, so I simply went with it for now.  I’ll update this post if I can determine the need for it or if I permanently remove it.

tracingService.Trace("{0}: Found matching Business Process Flows...", CHILD_CLASS_NAME);

var bpfId = new Guid();
var bpfEntityName = String.Empty;

foreach (var entity in entColByQuery) { //// Loop related entities and retrieve Workflow Names
  bpfId = entity.Id;
  bpfEntityName = entity.GetAttributeValue<string>(CrmEarlyBound.Workflow.Fields.Name);
  break;
}

if (bpfId != Guid.Empty) {
  tracingService.Trace("{0}: Successfully retrieved the Business Process Flow that we'll be switching to: {1}, Id: {2}", CHILD_CLASS_NAME, bpfEntityName, bpfId.ToString());

  System.Threading.Thread.Sleep(2000); // Wait for 2 seconds before switching the process
  //*** Set to the new or same Business BpfEntityName Flow
  var setProcReq = new SetProcessRequest {
    Target = new EntityReference(theEntity.LogicalName, theEntity.Id),
    NewProcess = new EntityReference(CrmEarlyBound.Workflow.EntityLogicalName, bpfId)
  };

  tracingService.Trace("{0}: ***Ready To Update - Business Process Flow", CHILD_CLASS_NAME);
  var setProcResp = (SetProcessResponse)serviceProxy.Execute(setProcReq);
  tracingService.Trace("{0}: ***Updated", CHILD_CLASS_NAME);
}

The code here is a repeat and helpful in determining if your initial BPF change was successful. From there you can print out each of the BPFs that are associated to the parent entity. The first BPF in the list will always be the active BPF.

//*** Verify if the Process Instance was switched successfully for the Entity record
processInstancesRequest = new RetrieveProcessInstancesRequest {
  EntityId = theEntity.Id,
  EntityLogicalName = theEntity.LogicalName
};

processInstancesResponse = (RetrieveProcessInstancesResponse)serviceProxy.Execute(processInstancesRequest);
processCount = processInstancesResponse.Processes.Entities.Count;

if (processCount > 0) {
  var activeProcessInstance = processInstancesResponse.Processes.Entities[0]; //*** First Entity record is the Active Process Instance
  var activeProcessInstanceId = activeProcessInstance.Id; //*** Active Process Instance Id to be used later for retrieval of the active path of the process instance

  tracingService.Trace("{0}: Successfully Switched to '{1}' BPF for the Entity Record.", CHILD_CLASS_NAME, activeProcessInstance.Attributes[CrmEarlyBound.Workflow.Fields.Name]);
  tracingService.Trace("{0}: Count of process instances concurrently associated with the entity record: {1}.", CHILD_CLASS_NAME, processCount);
  var message = "All process instances associated with the entity record:";

  for (var i = 0; i < processCount; i++) {
    message = message + " " + processInstancesResponse.Processes.Entities[i].Attributes[CrmEarlyBound.Workflow.Fields.Name] + ",";
  }

  tracingService.Trace("{0}: {1}", CHILD_CLASS_NAME, message.TrimEnd(message[message.Length - 1]));

This section of code loops through each Stage of the active BPF and displays each of the Stages information along with determining the starting and ending positions of the required transition. Microsoft states that you should only transition one Stage at a time but where’s the fun in that. As long as you don’t have any field requirements in the stages that you are either switching from or skipping over then you can transition as many stages as you like.

//*** Retrieve the Active Stage ID of the Active Process Instance
var activeStageId = new Guid(activeProcessInstance.Attributes[CrmEarlyBound.ProcessStage.Fields.ProcessStageId].ToString());
var activeStagePosition = 0;
var newStageId = new Guid();
var newStagePosition = 0;

//*** Retrieve the BPF Stages in the active path of the Active Process Instance
var activePathRequest = new RetrieveActivePathRequest {
  ProcessInstanceId = activeProcessInstanceId
};
var activePathResponse = (RetrieveActivePathResponse)serviceProxy.Execute(activePathRequest);

tracingService.Trace("{0}: Retrieved the BPF Stages in the Active Path of the Process Instance:", CHILD_CLASS_NAME);

for (var i = 0; i < activePathResponse.ProcessStages.Entities.Count; i++) {
  var curStageName = activePathResponse.ProcessStages.Entities[i].Attributes[CrmEarlyBound.ProcessStage.Fields.StageName].ToString();

  tracingService.Trace("{0}: Looping Through Stage #{1}: {2} (StageId: {3}, IndexId: {4})", CHILD_CLASS_NAME, i + 1, curStageName, activePathResponse.ProcessStages.Entities[i].Attributes[CrmEarlyBound.ProcessStage.Fields.ProcessStageId], i);
  //*** Retrieve the Active Stage Name and Stage Position based on a successful match of the activeStageId
  if (activePathResponse.ProcessStages.Entities[i].Attributes[CrmEarlyBound.ProcessStage.Fields.ProcessStageId].Equals(activeStageId)) {
    activeStagePosition = i;
    tracingService.Trace("{0}: Concerning the Process Instance -- Initial Active Stage Name: {1} (StageId: {2})", CHILD_CLASS_NAME, curStageName, activeStageId);
  }
  //*** Retrieve the New Stage Id, Stage Name, and Stage Position based on a successful match of the stagename
  if (curStageName.Equals(bpfStageName, StringComparison.InvariantCultureIgnoreCase)) {
    newStageId = new Guid(activePathResponse.ProcessStages.Entities[i].Attributes[CrmEarlyBound.ProcessStage.Fields.ProcessStageId].ToString());
    newStagePosition = i;
    tracingService.Trace("{0}: Concerning the Process Instance -- Desired New Stage Name: {1} (StageId: {2})", CHILD_CLASS_NAME, curStageName, newStageId);
  }
}

Partial Log Example:

This snapshot view of the tracing out put displays an example all the stages related to one of my BPFs to give you an idea of what’s happening in the above code.

SetBizProcessFlowV2: Looping Through Stage #1: INITIATION (StageId: f0be05ac-51be-498c-b1d5-ebfc036095e2, IndexId: 0)
SetBizProcessFlowV2: Looping Through Stage #2: PREPARATION (StageId: a9fce3a5-d54c-41da-ac9f-56fa7f84404e, IndexId: 1)
SetBizProcessFlowV2: Concerning the Process Instance -- Initial Active Stage Name: PREPARATION (StageId: a9fce3a5-d54c-41da-ac9f-56fa7f84404e)
SetBizProcessFlowV2: Looping Through Stage #3: REVIEW (StageId: 0c63beac-b1dc-4db5-a4b5-0ffd9ac96cc9, IndexId: 2)
SetBizProcessFlowV2: Looping Through Stage #4: ATP (StageId: 213bdcdf-9ac2-415e-a204-59b261b42162, IndexId: 3)
SetBizProcessFlowV2: Concerning the Process Instance -- Desired New Stage Name: ATP (StageId: 213bdcdf-9ac2-415e-a204-59b261b42162)
SetBizProcessFlowV2: Looping Through Stage #5: PAPER PROCESSING (StageId: 350d89b3-df17-48d2-a930-91012686b62f, IndexId: 4)
SetBizProcessFlowV2: Looping Through Stage #6: DELIVERY (StageId: 4c3ed26b-4f86-450b-83c2-1c32d65cc6af, IndexId: 5)
SetBizProcessFlowV2: Looping Through Stage #7: FINALIZATION (StageId: d0bbe23f-e72f-48de-9b0e-ce410469a36f, IndexId: 6)
SetBizProcessFlowV2: Looping Through Stage #8: COMPLETED (StageId: d5242580-a6c3-4835-99a1-7b4a6cd910dc, IndexId: 7)

The previous section determined the transition requirements, while this section performs the actual transition via a loop so as to not illegally transition out of sequence. An illegal transition will result in an Invalid Stage Transition error. This part was rather frustrating for me because Microsoft’s Documentation was severely lacking in listing out what all affects the BPF transition process.

If your BPF has no field requirements (fields that must contain data before you change a Stage), you can still get the nasty Invalid Stage Transition error message while transitioning because you may have fields on the entity that are set or have been recently set to required. To help illuminate this issue I’ve placed a Try-Catch statement below that will catch the error and print out the child attribute clientdata of which contains all the fields of the entity.  This also includes the Boolean value used to identify if the field is required or not. This should help you in determining where the breakdown is actually occurring.

//***Update the Business Process Flow Instance record to the desired Active Stage
Entity retrievedProcessInstance;
ColumnSet columnSet;
var stageShift = newStagePosition - activeStagePosition;

if (stageShift > 0) {
  tracingService.Trace("{0}: Number of Stages Shifting Forward: {1}", CHILD_CLASS_NAME, stageShift);
  //*** Stages only move in 1 direction --> Forward
  for (var i = activeStagePosition; i <= newStagePosition; i++) {
    System.Threading.Thread.Sleep(1000);
    //*** Retrieve the Stage Id of the next stage that you want to set as active
    var newStageName = activePathResponse.ProcessStages.Entities[i].Attributes[CrmEarlyBound.ProcessStage.Fields.StageName].ToString();
    newStageId = new Guid(activePathResponse.ProcessStages.Entities[i].Attributes[CrmEarlyBound.ProcessStage.Fields.ProcessStageId].ToString());

    tracingService.Trace("{0}: Setting To Stage #{1}: {2} (StageId: {3}, IndexId: {4})", CHILD_CLASS_NAME, i + 1, newStageName, newStageId, i);
    //*** Retrieve the BpfEntityName Instance record to update its Active Stage
    columnSet = new ColumnSet();
    columnSet.AddColumn(ACTIVE_STAGE_ID);
    retrievedProcessInstance = serviceProxy.Retrieve(bpfEntity.Attributes[CrmEarlyBound.Workflow.Fields.UniqueName].ToString(), activeProcessInstanceId, columnSet);
    //*** Set the next Stage as the Active Stage
    retrievedProcessInstance[ACTIVE_STAGE_ID] = new EntityReference(CrmEarlyBound.ProcessStage.EntityLogicalName, newStageId); //(ProcessStage.EntityLogicalName, activeStageId);

    try {
      tracingService.Trace("{0}: ***Ready To Update -- BPF Stage", CHILD_CLASS_NAME);
      serviceProxy.Update(retrievedProcessInstance);
      tracingService.Trace("{0}: ***Updated", CHILD_CLASS_NAME);
    } catch (FaultException<OrganizationServiceFault> ex) { //*** Determine BPF Stage Requirements
      foreach (var stageAttribute in activePathResponse.ProcessStages.Entities[i].Attributes) {
        if (stageAttribute.Key.Equals("clientdata")) {
          tracingService.Trace("{0}: Attribute Key: {1}, Value: {2}", CHILD_CLASS_NAME, stageAttribute.Key, stageAttribute.Value.ToString());
          break;
        }
      }

      tracingService.Trace(FullStackTraceException.Create(ex).ToString());
      throw;
    }
  }
} else {
  tracingService.Trace("{0}: Number of Stages Shifting Backwards: {1}", CHILD_CLASS_NAME, stageShift);
}

Finally, this last bit of code is purely optional and really just meant for debugging purposes. The tracing output provides information on whether the active stage actually transitioned to the desired stage specified in the workflow activity parameters.

//***Retrieve the Business Process Flow Instance record again to verify its Active Stage information
columnSet = new ColumnSet();
columnSet.AddColumn(ACTIVE_STAGE_ID);
retrievedProcessInstance = serviceProxy.Retrieve(bpfEntity.Attributes[CrmEarlyBound.Workflow.Fields.UniqueName].ToString(), activeProcessInstanceId, columnSet);

var activeStageEntityRef = retrievedProcessInstance[ACTIVE_STAGE_ID] as EntityReference;
if (activeStageEntityRef != null) {
  if (activeStageEntityRef.Id.Equals(newStageId)) {
    tracingService.Trace("{0}: Concerning the Process Instance -- Modified -- Active Stage Name: {1} (StageId: {2})", CHILD_CLASS_NAME, activeStageEntityRef.Name, activeStageEntityRef.Id);
  }
}

I hope that this walk-thru has adequately demonstrated in greater detail how to properly perform a Business Process Flow change and/or BPF Stage transition in code.  While a lot of the code reflects what was written in Microsoft’s Documentation concerning the BPF, I think the additional tracing and explanations should at least point in in the right direction when trying to decipher potential shortcomings that may occur when interacting with the BPF.  In my next post for this series I’ll show you how to deploy your new custom workflow activity into your production solution.

I’ll post the project code up on GitHub soon so please check back…

Related Articles

 

Transition Between Dynamics 365 Business Process Flow Stages using a Workflow Activity and C# – Part 1

Introduction

I’ve been working with Microsoft CRM, now Dynamics 365 for the past few years and boy has it been mutating as Microsoft releases new versions with so many new features that it’s hard to keep up.  Microsoft has also been improving existing feature sets as well as fixing a lot of old bugs in the system.  My road trip concerning switching Business Process Flows and their related Stages has been long and difficult.  With the new Dynamics 365 Online v8.2 update things centered around the BPF have become a little interesting.  I’ll describe how I went about this process formally introducing myself to the world of code blogging in a series around Microsoft’s new and improved Business Process Flow.  Even though I am working with Microsoft Dynamics 365 Online version, this solution isn’t limited to just the online version and will work with the on-premise version as well.

Previous Workflow Activity Solution

A while back I had found a solution to my original problem when I was still on version 2013 of CRM Online.  It utilized LINQ and its rather unique ability to gain access to subsets of code without the need to really instantiate the required objects.  It was difficult if not impossible at the time to gain access to the required components in order to do a Business Process Flow change and/or Stage shift in code.  Furthermore, the out-of-the-box feature set didn’t allow for this type of manipulation, so creating a custom workflow activity was just about the only way to get around this problem.

Below is the code from my now deprecated workflow activity.  It was acquired from another blog posting written by Scott Durrow (MVP).  You can read more about the below solution code in his original posting.  However, I’ll briefly describe the main blocks of code from his solution below.

Retrieve the Process Using LINQ

This section obtains the Process from the Workflow entity using LINQ and the provided input variable containing the process name defined in the workflow activity.

//// Get the process id using the name provided
var process = (from p in ctx.CreateQuery<Workflow>()
              where p.Name == this.Process.Get<string>(executionContext) && p.StateCode == WorkflowState.Activated
             select new Workflow
                    {
                       WorkflowId = p.WorkflowId
                    }).FirstOrDefault();
if (process == null) {
  throw new InvalidPluginExecutionException(String.Format("Process '{0}' not found.", this.Process.Get<string>(executionContext)));
}

Retrieve the ProcessStage Using LINQ

This section obtains the ProcessStage from the Workflow entity using LINQ and the provided input variable containing the Stage name defined in the workflow activity and the previously retrieved Process.WorkflowId value.

//// Get the stage id using the name provided
var processStage = (from s in ctx.CreateQuery<ProcessStage>()
                   where s.StageName == this.ProcessStage.Get<string>(executionContext) && s.ProcessId.Id == process.WorkflowId
                  select new ProcessStage
                         {
                            ProcessStageId = s.ProcessStageId
                         }).FirstOrDefault();
if (processStage == null) {
  throw new InvalidPluginExecutionException(String.Format("Process Stage '{0}' not found.",this.ProcessStage.Get<string>(executionContext)));
}

Update the Entity

This section takes both the Process.WorkflowId and the ProcessStage.ProcessStageId, instantiates a new entity object and updates the current entity’s ProcessId and StageId field values.

var updatedStage = new Entity(context.PrimaryEntityName) { Id = context.PrimaryEntityId };
updatedStage["stageid"] = processStage.ProcessStageId;
updatedStage["processid"] = process.WorkflowId;
service.Update(updatedStage);

As with all things Microsoft, attitudes and code base change over time.  With that comes a fully supported set of operations needed to switch Business Process Flows through code; including actual Microsoft Documentation.  The supported process no longer required LINQ but instead provided a fully fledged feature set of classes to perform the required operations in code.

Problem

Now comes the fun part, the problem.  With the recent upgrade in versions came an apparent end to what I thought was the supported method of switching Business Process Flow Stages.  What I mean is that previously you could switch to whatever stage of the process you wanted without any errors.  That mean that if you had requirements in states 1, 2, 3, and 4 and you wanted to simply switch from stage 1 to 3, skipping 2, without completing any of the requirements for stage 2, the system allowed for this operation in code.

After speaking with Microsoft Tech Support they have confirmed that the described scenario was in fact a bug, a rather long outstanding bug in the system that they now have closed in Dynamics 365 v8.2.  You now no longer have the ability to shift to any stage of the Business Process Flow.  You are required to move in order and complete whatever requirements are set for that Stage.

If you tried to skip one too many stages then you received a rather nasty error message show below.  This error also occurs if you have requirements set for that stage and you try to switch to the next stage in the stage order.

SetBizProcessFlowV2: Setting To Stage #4: ATP (StageId: 9fb961ab-434b-4b80-bc6b-6cecc1f86f2c, IndexId: 3)
SetBizProcessFlowV2: ***Ready To Update -- BPF Stage
SetBizProcessFlowV2: Fault Exception: An error occurred during Plug-in execution
SetBizProcessFlowV2: Fault Timestamp: 8/21/2017 3:38:08 PM
SetBizProcessFlowV2: Fault Code: -2146885629
SetBizProcessFlowV2: Fault Message: Invalid stage transition. Transition to stage Step_39 is not in the process active path.

This error causes whatever workflow attached to the activity to error out.  Below is an image of the most likely errors you’ll find directly associated to this issue.

WF Activity - Waiting for retry due to error

Error Message: Waiting for retry due to error

This error type of Invalid Stage Transition will never complete thus the system log will clog up with a large amount of these error types.  The Status Reason is unique and not available in the Advance Find screen for selection at this time.  Since this error will never timeout, or at least in my experience timeouts were not occurring, you’ll need to perform a bulk cancellation followed up with a bulk deletion for the ones marked as Canceled.  To isolate this type of Status Reason filter this view All System Jobs for the value Waiting.  Then check all the results selecting the More Actions drop-down menu, where you’ll finally select the Cancel option.

D365 System Jobs - More Actions - Cancel

Getting Started

In order to get things started with my Dynamics 365 Workflow Activity I needed to make sure I had a few tools.  Listed below are a few things you’ll need to get the ball rolling.

I’m using Microsoft Visual Studio 2013 because the Dynamics CRM & 365 Developer Extensions currently does not support Visual Studio 2017; however this should change in the very near future. Dynamics 365 SDK should be downloaded for additional coding examples and also for a tool called the Plugin Registration Tool. I’ll show you how to use this tool to upload our custom assembly into Dynamics 365 Online.  If you’ve been working with Microsoft CRM for a while (now referred to as Dynamics 365), then you probably are already familiar with a very handy 3rd party application containing a collection of useful tools simply called XrmToolBox.  If you are unfamiliar with this app then please do download and get friendly with it as it’ll come in handy when dealing with Dynamics 365.

Related Articles