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) |
---|---|
Message | Update |
Primary Entity | new_project |
Stage | Pre-Operation (sync) |
Filtering Attributes | statecode, statuscode |
Pre-Image | Name = 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 anEntityReference
. - Use
formContext.data.refresh()
if users need instant visual confirmation.
No comments:
Post a Comment