Changing Business Process Flow Stage in C# Plugin

2019-04-07 07:09发布

问题:

I am following this article to change my Business Process Flow stage within a c# plugin. I am able to move the stage forward to the next stage, but I am receiving an error when I try to move back to a previous stage. The error below is what I receive in the UI from Dynamics. When I debug the plugin, I receive a FaultException<OrganizationServiceFault> exception that doesn't contain any information. Why am I receiving an error and how can I modify my code to successfully go back to a previous stage in my Business Process Flow?

Error

Unhandled Exception: System.ServiceModel.FaultException`1[[Microsoft.Xrm.Sdk.OrganizationServiceFault, Microsoft.Xrm.Sdk, Version=8.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]]: An unexpected error occurred.
Detail: <OrganizationServiceFault xmlns="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <ActivityId>5df51362-b7c1-4817-a8d0-de2d63b15c17</ActivityId>
  <ErrorCode>-2147220970</ErrorCode>
  <ErrorDetails xmlns:a="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
  <Message>An unexpected error occurred.</Message>
  <Timestamp>2018-07-19T18:55:42.6625925Z</Timestamp>
  <ExceptionSource i:nil="true" />
  <InnerFault>
    <ActivityId>5df51362-b7c1-4817-a8d0-de2d63b15c17</ActivityId>
    <ErrorCode>-2147220970</ErrorCode>
    <ErrorDetails xmlns:a="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
    <Message>System.NullReferenceException: Microsoft Dynamics CRM has experienced an error. Reference number for administrators or support: #0D309052</Message>
    <Timestamp>2018-07-19T18:55:42.6625925Z</Timestamp>
    <ExceptionSource i:nil="true" />
    <InnerFault i:nil="true" />
    <OriginalException i:nil="true" />
    <TraceText i:nil="true" />
  </InnerFault>
  <OriginalException i:nil="true" />
  <TraceText i:nil="true" />
</OrganizationServiceFault>

Plugin

if (localContext == null)
{
    throw new ArgumentNullException("localContext");
}

IPluginExecutionContext context = localContext.PluginExecutionContext;
IOrganizationService service = localContext.OrganizationService;

Client client = (Client)service.Retrieve(
    Client.LogicalName,
    new Guid("75FE165F-848B-E811-80F3-005056B33317"),
    new ColumnSet(new String[]{
        Client.Properties.ClientId
    })
);

client.ChangeStage(service);

Change Stage

public void ChangeStage(IOrganizationService service)
{
    // Get Process Instances
    RetrieveProcessInstancesRequest processInstanceRequest = new RetrieveProcessInstancesRequest
    {
        EntityId = this.Id,
        EntityLogicalName = this.LogicalName
    };

    RetrieveProcessInstancesResponse processInstanceResponse = (RetrieveProcessInstancesResponse)service.Execute(processInstanceRequest);

    // Declare variables to store values returned in response
    int processCount = processInstanceResponse.Processes.Entities.Count;
    Entity activeProcessInstance = processInstanceResponse.Processes.Entities[0]; // First record is the active process instance
    Guid activeProcessInstanceID = activeProcessInstance.Id; // Id of the active process instance, which will be used later to retrieve the active path of the process instance

    // Retrieve the active stage ID of in the active process instance
    Guid activeStageID = new Guid(activeProcessInstance.Attributes["processstageid"].ToString());

    // Retrieve the process stages in the active path of the current process instance
    RetrieveActivePathRequest pathReq = new RetrieveActivePathRequest
    {
        ProcessInstanceId = activeProcessInstanceID
    };
    RetrieveActivePathResponse pathResp = (RetrieveActivePathResponse)service.Execute(pathReq);

    string activeStageName = "";
    int activeStagePosition = -1;

    Console.WriteLine("\nRetrieved stages in the active path of the process instance:");
    for (int i = 0; i < pathResp.ProcessStages.Entities.Count; i++)
    {
        // Retrieve the active stage name and active stage position based on the activeStageId for the process instance
        if (pathResp.ProcessStages.Entities[i].Attributes["processstageid"].ToString() == activeStageID.ToString())
        {
            activeStageName = pathResp.ProcessStages.Entities[i].Attributes["stagename"].ToString();
            activeStagePosition = i;
        }
    }

    // Retrieve the stage ID of the next stage that you want to set as active
    activeStageID = (Guid)pathResp.ProcessStages.Entities[activeStagePosition - 1].Attributes["processstageid"];

    // Retrieve the process instance record to update its active stage
    ColumnSet cols1 = new ColumnSet();
    cols1.AddColumn("activestageid");
    Entity retrievedProcessInstance = service.Retrieve("ccseq_bpf_clientsetup", activeProcessInstanceID, cols1);

    // Set the next stage as the active stage
    retrievedProcessInstance["activestageid"] = new EntityReference(ProcessStage.LogicalName, activeStageID);
    service.Update(retrievedProcessInstance);
}

Update

I found this article that explains how to update the Stage using the Web API. When I try this method, I get the error:

An undeclared property 'activestageid' which only has property annotations in the payload but no property value was found in the payload. In OData, only declared navigation properties and declared named streams can be represented as properties without values.

I've tried a few varieties of 'activestageid' without success (ActiveStageId, _activestageid_value).


Update 2

Based on Arun's feedback, I tried the below Web API calls without success. The ID inside the brackets in the url (ccseq_bpf_clientsetups(###)) I pulled from the BusinessProcessFlowInstanceId on the ccseq_bpf_clientsetups table. The processstages ID I pulled from the ProcessStageId in the ProcessStageBase table

// Attempt 1
PATCH /COHEN/api/data/v8.2/ccseq_bpf_clientsetups(bc892aec-2594-e811-80f4-005056b33317) HTTP/1.1
{ "ActiveStageID@odata.bind": "/processstages(70018854-db7c-4612-915b-2ad7870a8574)"}

// Attempt 2
PATCH /COHEN/api/data/v8.2/ccseq_bpf_clientsetups(bc892aec-2594-e811-80f4-005056b33317) HTTP/1.1
{ "activestageid@odata.bind": "/processstages(70018854-db7c-4612-915b-2ad7870a8574)"}

// Attempt 3
PATCH /COHEN/api/data/v8.2/ccseq_bpf_clientsetups(bc892aec-2594-e811-80f4-005056b33317) HTTP/1.1
{ "ActiveStageId@odata.bind": "/processstages(70018854-db7c-4612-915b-2ad7870a8574)"}

Update 3

I downloaded jLattimer's CRM Rest Builder and tried running the JavaScript his tool generated. The code was identical to what I had written previously and unfortunately did not work. At this point I'm fairly confident that changing stages is not supported in v8.2 of the Web API.

回答1:

I've got some code that attempts to move the Business Process Flow stage forward, as a custom workflow step (rather than a plugin). I've posted it below.

The difference that I see are:

  • I'm moving forwards (not backwards)
  • I'm probably not following best practice :)
  • I'm not retrieving the active path, I'm just getting all available stages for the process
  • I'm also setting the TraversedPath property

Code:

var activeInstancesRequest = new RetrieveProcessInstancesRequest
{
    EntityId          = TargetEntity.Id,
    EntityLogicalName = TargetEntity.LogicalName
};
var activeInstancesResponse = (RetrieveProcessInstancesResponse)base.OrgService.Execute(activeInstancesRequest);
var process = activeInstancesResponse.Processes.Entities.Select(x => x.ToEntity<BusinessProcessFlowInstance>()).ToList();
var stages = base.XrmContext.ProcessStageSet
    .Where(s => s.ProcessId.Id == process.FirstOrDefault().ProcessId.Id)
    .Select(s => new ProcessStage
    {
        ProcessStageId = s.ProcessStageId,
        StageName = s.StageName
    })
    .ToList();

var targetStage = stages.Where(stage => stage.StageName == targetStageName).FirstOrDefault();
if (targetStage != null)
{
    crmWorkflowContext.Trace($"BPF contains target stage (\"{targetStageName}\"). Attempting to update BPF");

    // Setting the Traversed Path is necessary for the Business Process Flow to show the active Stage
    // If this is not updated then although the new Stage is set as current, the previous Stage remains actively selected
    var traversedPath = $"{bpf.TraversedPath},{targetStage.ProcessStageId.Value}";
    var update = new BusinessProcessFlowInstance()
    {
        BusinessProcessFlowInstanceId = bpf.BusinessProcessFlowInstanceId,
        ProcessStageId                = targetStage.ProcessStageId,
        TraversedPath                 = traversedPath   
    };

    xrmContext.Attach(update);
    xrmContext.UpdateObject(update);
}


回答2:

I tested quickly, I was able to move forward/backward within Lead to Opportunity Sales Process in vanilla v9 online org.

I simply tested with Web API PATCH request from CRM REST builder successfully.

Develop to Propose (forward)

var entity = {};
entity["activestageid@odata.bind"] = "/processstages(3A275C22-FC45-4E89-97FC-41E5EC578743)";

var req = new XMLHttpRequest();
req.open("PATCH", Xrm.Page.context.getClientUrl() + "/api/data/v8.2/leadtoopportunitysalesprocesses(1674DB10-1994-E811-A969-000D3A1A9FA9)", true);

Propose to Develop (backward)

var entity = {};
entity["activestageid@odata.bind"] = "/processstages(BFC9108C-8389-406B-9166-2C3298A2E41F)";

var req = new XMLHttpRequest();
req.open("PATCH", Xrm.Page.context.getClientUrl() + "/api/data/v8.2/leadtoopportunitysalesprocesses(1674DB10-1994-E811-A969-000D3A1A9FA9)", true);

It doesn't anything else other than marked as needed below.

Not needed
1574DB10-1994-E811-A969-000D3A1A9FA9 - leadid
919E14D1-6489-4852-ABD0-A63A6ECAAC5D - processid
f99b4d48-7aad-456e-864a-8e7d543f7495,bfc9108c-8389-406b-9166-2c3298a2e41f - traversedpath

Needed
1674DB10-1994-E811-A969-000D3A1A9FA9 - businessprocessflowinstanceid
BFC9108C-8389-406B-9166-2C3298A2E41F - activestageid develop
3A275C22-FC45-4E89-97FC-41E5EC578743 - activestageid propose


Update:

I have tested the below snippet successfully in v8 as well [Version 1612 (8.2.2.2160) (DB 8.2.2.2160) online]

In fact it was moving backward/forward without traversedpath.

var entity = {};
entity["activestageid@odata.bind"] = "/processstages(BFC9108C-8389-406B-9166-2C3298A2E41F)";
entity.traversedpath = "f99b4d48-7aad-456e-864a-8e7d543f7495,bfc9108c-8389-406b-9166-2c3298a2e41f";

var req = new XMLHttpRequest();
req.open("PATCH", Xrm.Page.context.getClientUrl() + "/api/data/v8.0/leadtoopportunitysalesprocesses(E5B70E69-2094-E811-8145-C4346BDCF2F1)", true);

But got Bad request error with below:

entity.activestageid = {
            Id: "3A275C22-FC45-4E89-97FC-41E5EC578743",
            LogicalName: "processstage"
        };


回答3:

I was having the same issue. It's a lot simpler than you think. OK, open advanced find, select {your BPF}. Add two columns to query: {your Entity} {traversed path}.

Ok, so look at the traversed path for an entity that is actually in the previous stage (the one you want to go back to).

With your code, you need to dynamically break down the traversed path (.Split(',')) or something similar...remove the last stage (the one you're currently in), and voila! You're cooking with gasoline.

if the current traversed path were an array:

string[] currentPath = {"A", "B", "C", "D"};

your previous path needs to be:

string[] previousPath = {"A", "B", "C"};

Here's how you could do it in code, assuming 'entity' is your retrieved entity:

string traversedPath = (string) entity["traversedpath"];
string[] stages = traversedPath.Split(',');
string newPath = "";
//use length - 1 to omit last stage (your current stage)
for (int i = 0; i < stages.Length - 1; i++) 
{ 
     if (i != stages.Length - 1)
     newPath += stages[i] + ",";
     else
     newPath += stages[i];

}
entity["processid"] = new Guid("BPF Guid") //may be optional
entity["stageid"] = new Guid("previous stage guid");
entity["traversedpath"] = newPath;
service.Update(entity);

Basically, the traversed path does not += 'your previous stage' to the end of the traversed path. It wants to set the traversed path to the ORIGINAL traversed path for 'your previous stage'. Find out what the traversed path is for the stage desired, and either hardcode that sucker into a string (if it's only gonna go to that stage, ever)..or programmatically do it via the .Split(',') method on the Entity["traversedpath"] attribute in code.

Remember, you are subtracting from, not adding to...the traversed path. It took me a lot of invalid traversed path errors to come to this conclusion...and it works. Good luck!