Reactivate a Business Process Flow and Jump to a Target Stage with a Pre-Operation Plug-in

1 · Scenario

Problem statement ↴
Records previously flagged as Ineligible (statecode = Inactive) are later re-activated by users. Their associated Business Process Flow rows remain inactive and stuck on an early stage.



Goal ↴
When the record is re-activated, its BPF must automatically (1) reactivate and (2) advance directly to the “Review Completed” stage.


2 · Plug-in Registration

Setting Value (adjust to suit)
MessageUpdate
Primary Entitynew_project
StagePre-Operation (sync)
Filtering Attributesstatecode, statuscode
Pre-ImageName = PreImage → fields statecode, new_decision

Why Pre-Op?
• You still have the old state (Inactive) to confirm the transition.
• You can update the BPF row in the same database transaction.


3 · Full C# (target .-NET 4.x / C# 7.3)

using System;
using System.Linq;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

namespace Plugins
{
    /// Reactivates BPF then jumps to “Review Completed”.
    public class MoveBpfToReviewCompletedOnReactivate : IPlugin
    {
        private const int   StateActive        = 0;
        private const int   StateInactive      = 1;
        private const int   DecisionIneligible = 999999;          // ← your option set
        private const string MainTable         = "new_project";   // ← your table
        private const string BpfTable          = "new_projectbpf";
        private const string DecisionField     = "new_decision";
        private const string TargetStageName   = "Review Completed";

        public void Execute(IServiceProvider sp)
        {
            var trace = (ITracingService)sp.GetService(typeof(ITracingService));
            var ctx   = (IPluginExecutionContext)sp.GetService(typeof(IPluginExecutionContext));
            var svc   = ((IOrganizationServiceFactory)sp.GetService(typeof(IOrganizationServiceFactory)))
                        .CreateOrganizationService(ctx.UserId);
            trace.Trace("START Plug-in");

            var pre = ctx.PreEntityImages.Contains("PreImage") ? ctx.PreEntityImages["PreImage"] : null;

            if (!IsReactivation(ctx, pre, trace)) return;
            if (pre?.GetAttributeValue<OptionSetValue>(DecisionField)?.Value != DecisionIneligible)
            { trace.Trace("Decision ≠ Ineligible → exit"); return; }

            var recordId = ((Entity)ctx.InputParameters["Target"]).Id;
            var bpf      = GetBpfRow(svc, recordId, trace);
            if (bpf == null) { trace.Trace("No BPF row → exit"); return; }

            ActivateAndMove(svc, bpf, trace);
            trace.Trace("END Plug-in");
        }

        /* ――― helpers ――― */

        private static bool IsReactivation(IPluginExecutionContext ctx, Entity pre, ITracingService t)
        {
            var target = (Entity)ctx.InputParameters["Target"];
            if (!target.Contains("statecode")) return false;

            var from = pre?.GetAttributeValue<OptionSetValue>("statecode")?.Value ?? -1;
            var to   = ((OptionSetValue)target["statecode"]).Value;
            t.Trace($"State {from} → {to}");
            return from == StateInactive && to == StateActive;
        }

        private static Entity GetBpfRow(IOrganizationService svc, Guid id, ITracingService t)
        {
            var q = new QueryExpression(BpfTable)
            {
                ColumnSet = new ColumnSet("businessprocessflowinstanceid",
                                          "statecode","activestageid","processid")
            };
            q.Criteria.AddCondition($"bpf_{MainTable}id", ConditionOperator.Equal, id);
            var row = svc.RetrieveMultiple(q).Entities.FirstOrDefault();
            t.Trace("BPF found = " + (row != null));
            return row;
        }

        private static void ActivateAndMove(IOrganizationService svc, Entity bpf, ITracingService t)
        {
            var update  = new Entity(bpf.LogicalName, bpf.Id);
            var changed = false;

            /* 1 ▸ activate BPF */
            if (bpf.GetAttributeValue<OptionSetValue>("statecode")?.Value == StateInactive)
            {
                update["statecode"]  = new OptionSetValue(StateActive);
                update["statuscode"] = new OptionSetValue(1);
                changed = true;
                t.Trace("BPF activated");
            }

            /* 2 ▸ move to Review Completed */
            var currentStage = ((EntityReference)bpf["activestageid"])?.Id ?? Guid.Empty;
            var processId    = ((EntityReference)bpf["processid"]).Id;
            var targetStage  = LookupStageId(svc, processId, TargetStageName, t);

            if (targetStage != Guid.Empty && targetStage != currentStage)
            {
                update["activestageid"] = new EntityReference("processstage", targetStage);
                changed = true;
                t.Trace("Stage → Review Completed");
            }

            if (changed) svc.Update(update);
        }

        private static Guid LookupStageId(IOrganizationService svc, Guid processId,
                                          string name, ITracingService t)
        {
            var q = new QueryExpression("processstage")
            {
                ColumnSet = new ColumnSet("processstageid"), TopCount = 1
            };
            q.Criteria.AddCondition("processid", ConditionOperator.Equal, processId);
            q.Criteria.AddCondition("stagename", ConditionOperator.Equal, name);
            var stage = svc.RetrieveMultiple(q).Entities.FirstOrDefault();
            t.Trace($"Stage '{name}' found = " + (stage != null));
            return stage?.Id ?? Guid.Empty;
        }
    }
}

4 · Why the UI may lag ≈ 10 s

The server commits instantly, but model-driven forms refresh the BPF header on a background timer (about 10 seconds). Add this one-liner for immediate feedback:

setTimeout(() => formContext.data.refresh(false), 3000);

5 · Key Take-aways

  • Pre-Operation Update = atomic save + access to old statecode.
  • Always activate the BPF row before changing activestageid.
  • Handle activestageid as an EntityReference.
  • Use formContext.data.refresh() if users need instant visual confirmation.

No comments:

Post a Comment