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
activestageidas anEntityReference. - Use
formContext.data.refresh()if users need instant visual confirmation.
No comments:
Post a Comment