Sitecore Experience Commerce: Implementing Multi-Step Actions in the Business Tools

In this article, we will look at multi-step actions and how we can implement them for customising the Sitecore Commerce Business Tools.

What is a Multi-Step Action?

The multi-step action is the approach to building out entity view modals to act as a kind of wizard, where inputs from each step can affect the subsequent steps.

Adding a qualification/condition to a promotion - step 1.
Adding a qualification/condition to a promotion - step 2.

A Dive into the Multi-Step Action Implementation

We will take a dive into the pieces that make up the multi-step action implementation to see just how we should be building our custom multi-step modal, using the Add Qualification modal for promotions as our working example.

This section is focused on explaining what will be found in the current platform implementation, which can be useful if looking to extend the BizFx and Commerce Engine functionality, but can be skipped if you are only after Implementing Custom Multi-Step Actions as quick as possible.

MultiStepActionPolicy

The MultiStepActionPolicy houses a single property, FirstStep, which will contain the EntityActionView that will be populated in the modal entity view.

public MultiStepActionPolicy()
{
    this.FirstStep = new EntityActionView();
}

public EntityActionView FirstStep { get; set; }

In the following code snippet, we see that the MultiStepActionPolicy is added to the EntityActionView, in which the QualificationsDetails that is assigned to the EntityView will use to populate the first step of the add qualification modal.

Line 18 highlights the Name of the entity view action, which will be resolved to its localised value from the .../Commerce Terms/BusinessTools/ViewActionNames/AddQualification, and populates modal's title.

var actionPolicy = arg.GetPolicy<ActionsPolicy>();
actionPolicy.Actions.Add(
        new EntityActionView(new List<Policy>
        {
            new MultiStepActionPolicy
            {
                FirstStep = new EntityActionView
                {
                    Name = context.GetPolicy<KnownPromotionsActionsPolicy>().SelectQualification,
                    DisplayName = "Select Qualification",
                    Description = "Selects a Qualification",
                    IsEnabled = isEnabled,
                    EntityView = context.GetPolicy<KnownPromotionsViewsPolicy>().QualificationDetails
                }
            }
        })
        {
            Name = context.GetPolicy<KnownPromotionsActionsPolicy>().AddQualification,
            DisplayName = "Add Qualification",
            Description = "Adds a Qualification",
            IsEnabled = isEnabled,
            EntityView = string.Empty,
            Icon = "add"
        });
The entity view populated in the first step, of the Add Qualification modal, is resolved from the entity view action name of the EntityActionView, assigned to the MultiStepActionPolicy's FirstStep, which in this case is the SelectQualification action.

LocalizeEntityViewBlock

In the LocalizeEntityViewBlock, we find that the SelectQualification action name is being populated with the localised term, however as mentioned above, the modal utilises the AddQualification localised term instead, therefore we don't need to be concerned with this.

if (!action.HasPolicy<MultiStepActionPolicy>())
{
    continue;
}

var firstStepAction = action.GetPolicy<MultiStepActionPolicy>().FirstStep;
await SetActionLocalizedTerms(firstStepAction, context).ConfigureAwait(false);

MultiStepActionModel

This model is quite simple. The NextStep represents the name of the action to be executed when the modal form is submitted.

public MultiStepActionModel(string nextStep)
{
    this.NextStep = nextStep;
}

public string NextStep { get; set; }

In the following extract of DoActionSelectQualificationBlock, I have substituted out most of the code with comments, explaining the original code logic, to reduce the noise and keep focus on the multi-step implementation functionality.

In line 11, we see that the original condition property has been made readonly as we don't want the user to change their mind at this stage, however we don't want to hide the property as the user still needs to have context of what was previously selected.

Updating the readonly status of submitted view properties is considered a recommended practice.

Line 15 we have a comment that tells us that this is where the next step's view properties are added to the current entity view, building out the modal form.

This also means that the Do Action Blocks acts a pseudo Get Entity View Block as the initial modal's entity view is populated via the GetEntityView() API, utilising the IGetEntityViewPipeline under the hood, while subsequent updates in the multi-step implementation are triggered as the DoUxAction() API calls the IDoActionPipeline to process requests.

Finally, line 17 has the original entity view action name, 'AddQualification', added to the MultiStepActionModel as the NextStep and applied to the commerce context.

With the knowledge that adding the MultiStepActionModel to the context effectively creates the next step in the modal, there is no limit to how many steps we can add to a multi-step modal.

public override async Task<EntityView> Run(EntityView entityView, CommercePipelineExecutionContext context)
{
    /* validate action */

    /* validate promotion */

    var selectedCondition = entityView.Properties.FirstOrDefault(p => p.Name.Equals("Condition", StringComparison.OrdinalIgnoreCase));

    /* validate condition */

    selectedCondition.IsReadOnly = true;

    /* add and/or conditional operator. hide if no qualifications have been applied to the promotion so far */

    /* add new view properties for selected condition */

    context.CommerceContext.AddModel(new MultiStepActionModel(context.GetPolicy<KnownPromotionsActionsPolicy>().AddQualification));

    return entityView;
}
DoActionSelectQualificationBlock validates the Condition selection and updates the entity view to contain the remaining view properties required for configuring the 'Cart Has [count] Items?' qualification as the second step of this multi-step action modal.

CheckForMultiStepActionBlock

From the following snippet from CheckForMultiStepActionBlock, we see that it updates the current entity views action, removes the MultiStepActionModel from the commerce context, and adds the entity view to the commerce context.

In short, this logic is more of a helper, and we could get the same result by omitting the registration of the MultiStepActionModel, updating the entity view action, and add the entity view directly in the Do Action Block instead.

var multiAction = context.CommerceContext.GetModels<MultiStepActionModel>().FirstOrDefault();
if (string.IsNullOrEmpty(multiAction?.NextStep))
{
    return Task.FromResult(arg);
}

entityView.Action = multiAction.NextStep;
context.CommerceContext.RemoveModel(multiAction);
context.CommerceContext.AddModel(entityView);

The DoUxAction API

The last piece of the puzzle comes with BizFx's handling of the response of the DoUxAction API when we submit the modal dialog.

If an entity view object is returned in the API response object, BizFx will render the modal with the updated view instead of closing the modal and refreshing the current page view.

If we were to change the modal's entity view name during any of the steps it will not be reflected in the modal's title as there is no handling for this implemented by default in BizFx.

Summary

Let's review what we have learnt about multi-step actions.

  1. Adding the MultiStepActionPolicy to an EntityActionView basically swaps out the intended action from being executed in the initial GetEntityView() request, however the modal's title will reflect the initial EntityActionView's localised name.
  2. The LocalizeEntityViewBlock application to the EntityActionView in the MultiStepActionPolicy's FirstStep is superfluous and does not impact the multi-step implementation.
  3. Do Action Blocks act as pseudo Get Entity View Block for updating the modal's entity view.
  4. When updating entity views with multi-step actions, previously input view properties should be set to readonly as a recommended practice.
  5. The usage of the MultiStepActionModel is more of a helper model in CheckForMultiStepActionBlock, rather than a dependent piece of the implementation.
  6. There is no limit to how many steps we can add to a multi-step action modal.
  7. The modal title is not updated with an updated entity view.
  8. When the Commerce Engine's DoUxAction() API returns an entity view in the response model, BizFx will render it in the current modal view.

Implementing Custom Multi-Step Actions

For implementing custom multi-step actions we will skip the details about how to build out the pre-requisites, being the initial Populate View Actions Block to create the entity view action that will trigger the modal, and the Get Entity View Block that will populate the initial entity view that is rendered in the modal, and instead focus on the Do Action Block that will allow us to add the additional steps to the modal.

The multi-step sample show the initial entity view that we will create the second step for.

Essentially, we can implement multi-step actions without using any of the MultiStep classes, simply by adding the new entity view to the commerce context during a Do Action Block, however providing a more complete sample, the following code logic would be applied to our custom Do Action Block.

  1. Validate action
  2. Validate entity (if applicable)
  3. Validate current view properties
  4. Set current view properties to read only
  5. Add new view properties to entity view for next step
  6. Update entity view action with action to perform on the next time the modal is submitted
  7. Add entity view to the commerce context
public override async Task<EntityView> Run(EntityView entityView, CommercePipelineExecutionContext context)
{
    Condition.Requires(entityView).IsNotNull($"{Name}: The argument cannot be null");

    /* 1. Validate action */
    if (string.IsNullOrEmpty(entityView?.Action)
        || !entityView.Action.Equals("FirstAction", StringComparison.OrdinalIgnoreCase))
    {
        return await Task.FromResult(entityView).ConfigureAwait(false);
    }

    /* 2. Validate entity (if applicable) */
    // Not applicable

    /* Validate current view properties */
    // Not critical for sample implementation

    /* Set current view properties to read only */
    foreach (var property in entityView.Properties)
    {
        property.IsReadOnly = true;
    }

    /* Add new view properties to entity view for next step */
    entityView.Properties.Add(new ViewProperty
    {
        Name = "Step 2 Field 1"
    });

    entityView.Properties.Add(new ViewProperty
    {
        Name = "Step 2 Field 2",
        IsRequired = false
    });

    /* Update entity view action with action to perform on the next time the modal is submitted */
    entityView.Action = "SecondAction";

    /* Add entity view to the commerce context */
    context.CommerceContext.AddModel(entityView);

    return await Task.FromResult(entityView).ConfigureAwait(false);
}
Our multi-step sample entity view after the modal has been submitted, creating the second step view for completing data input.