Promotions: Customize Master view by using Entity Views and Action API
This blog provides a step-by-step guide on how to customize a Master view for a promotion item programmatically.
Promotion defines a set of qualifying rules that must be met in order to apply benefits at the cart level or for specific sellable items within a cart. We can create a promotion after associating a promotion book with a catalog. The master view of a promotion item looks like the image below:
Let's assume that we have to meet a specific set of requirements, shown below:
- Extend the master view to show additional information of a promotion. The additional view should include the following fields:
Disclaimer: Single Line Text
Promotion Type: Drop Down
Combinable: Checkbox
- If a user chooses to click the tick button, then save information to the database
- Display the information once saved
Expected Result:
Implementation Approach
1. Add a new Blocks folder in your custom Plugin.
2.Create a new view and add additional properties to the view inside an EntityView class defined under the Blocks folder.
3. Define an action policy to populate the new view and its settings.
4. Execute DoAction on click of the tick button which performs a transaction and validates and stores the information in the database.
5. Display the saved information by entity view class defined in step 1.
Code
Create a new view and add additional properties to the view inside an EntityView class defined under Blocks folder:
namespace Plugin.Demo.Promotion.Pipelines.Blocks.EntityViews { [PipelineDisplayName(PromotionConstants.Pipelines.Blocks.GetPromotionAdditionalInformationViewBlock)] public class GetPromotionAdditionalInformationViewBlock : PipelineBlock { private readonly ViewCommander _viewCommander; public GetPromotionAdditionalInformationViewBlock(ViewCommander viewCommander) { this._viewCommander = viewCommander; } public override Task Run(EntityView arg, CommercePipelineExecutionContext context) { Condition.Requires(arg).IsNotNull($"{Name}: The argument cannot be null."); var request = this._viewCommander.CurrentEntityViewArgument(context.CommerceContext); KnownPromotionsViewsPolicy promotionsViewsPolicy = context.GetPolicy(); // Only proceed if the current entity is a promotion item if (!(request.Entity is Sitecore.Commerce.Plugin.Promotions.Promotion)) { return Task.FromResult(arg); } var promotionItem = (Sitecore.Commerce.Plugin.Promotions.Promotion)request.Entity; var component = promotionItem.GetComponent(); var promotionAdditionalInformationViewsPolicy = context.GetPolicy(); var targetView = arg; // Check if the add action was requested var isAddView = !string.IsNullOrEmpty(arg.Action) && arg.Action.Equals("AddAdditionalInformation", StringComparison.OrdinalIgnoreCase); // Create a new view and add it to the current entity view. if (!isAddView && arg?.ChildViews.Where(cv => cv.Name.Equals(additionalInformationViewsPolicy.Name)).Any() == false) { var view = new EntityView { Name = additionalInformationViewsPolicy.Name, DisplayName = additionalInformationViewsPolicy.DisplayName, EntityId = arg.EntityId, ItemId = arg.ItemId, EntityVersion = arg.EntityVersion }; if (component.Disclaimer == string.Empty) { view.UiHint = "Table"; } arg.ChildViews.Add(view); targetView = view; AddPropertiesToView(targetView, component, true); } if(isAddView && promotionItem != null) { AddPropertiesToView(targetView, component, true); } return Task.FromResult(arg); } private void AddPropertiesToView(EntityView entityView, PromotionAdditionalInformationComponent component, bool isReadOnly) { if (!entityView.ContainsProperty(nameof(PromotionAdditionalInformationComponent.Disclaimer)) && !entityView.ContainsProperty(nameof(PromotionAdditionalInformationComponent.Combinable))) { entityView.Properties.Add( new ViewProperty { Name = nameof(PromotionAdditionalInformationComponent.Disclaimer), RawValue = component.Disclaimer, IsReadOnly = false, IsRequired = true, OriginalType = "string" }); ViewProperty promotionTypes = new ViewProperty { Name = nameof(PromotionAdditionalInformationComponent.PromotionType), RawValue = component.PromotionType.ToString(), IsReadOnly = false, IsRequired = true, UiType = "Dropdown", DisplayName = component.GetDisplayName(nameof(PromotionAdditionalInformationComponent.PromotionType)) }; var promotionTypeList = new List(); promotionTypeList.Add(new Selection() { Name = "None", IsDefault = true, DisplayName = "None" }); promotionTypeList.Add(new Selection() { Name = "Default", IsDefault = false, DisplayName = "Default" }); promotionTypeList.Add(new Selection() { Name = "Shipping", IsDefault = false, DisplayName = "Shipping" }); promotionTypeList.Add(new Selection() { Name = "Autoshipment", IsDefault = false, DisplayName = "Autoshipment" }); promotionTypes.Policies = new List() { new AvailableSelectionsPolicy() { List = promotionTypeList } }; entityView.Properties.Add(promotionTypes); entityView.Properties.Add( new ViewProperty { Name = nameof(PromotionAdditionalInformationComponent.Combinable), RawValue = component.Combinable, IsReadOnly = false, IsRequired = false, }); } } } }
Define an action policy to populate the new view and its settings:
namespace Plugin.Demo.Promotion.Pipelines.Blocks.EntityViews { [PipelineDisplayName(PromotionConstants.Pipelines.Blocks.PopulatePromotionAdditionalInformationActionsBlock)] public class PopulatePromotionAdditionalInformationActionsBlock: PipelineBlock { public override Task Run(EntityView arg, CommercePipelineExecutionContext context) { Condition.Requires(arg).IsNotNull($"{Name}: The argument cannot be null."); var viewsPolicy = context.GetPolicy(); if (string.IsNullOrEmpty(arg?.Name) || !arg.Name.Equals(viewsPolicy.Name, StringComparison.OrdinalIgnoreCase)) { return Task.FromResult(arg); } var actionPolicy = arg.GetPolicy(); actionPolicy.Actions.Add( new EntityActionView { Name = context.GetPolicy().Name, DisplayName = context.GetPolicy().DisplayName, Description = context.GetPolicy().Description, IsEnabled = true, EntityView = arg.Name, Icon = context.GetPolicy().Icon }); return Task.FromResult(arg); } } }
Execute DoAction on click of the tick button which performs a transaction and validates and stores the information in the database:
namespace Plugin.Demo.Promotion.Pipelines.Blocks.DoActions { public class DoActionAddPromotionAdditionalInformationBlock : PipelineBlock { private readonly Commands.AddPromotionAdditionalInformationCommand _addPromotionAdditionalInformationCommand; public DoActionAddPromotionAdditionalInformationBlock(AddPromotionAdditionalInformationCommand addPromotionAdditionalInformationCommand) : base() { this._addPromotionAdditionalInformationCommand = addPromotionAdditionalInformationCommand; } public override async Task Run(EntityView arg, CommercePipelineExecutionContext context) { var prmotionActionsPolicy = context.GetPolicy(); // Only proceed if the right action was invoked if (string.IsNullOrEmpty(arg.Action) || !arg.Action.Equals("AddAdditionalInformation", StringComparison.OrdinalIgnoreCase)) { return arg; } // Get the promotion item from the context Sitecore.Commerce.Plugin.Promotions.Promotion promotion1 = context.CommerceContext.GetObject(x => x.Id.Equals(arg.EntityId) && x.EntityVersion == arg.EntityVersion); if (promotion1 == null) { return arg; } component.Disclaimer = arg.Properties.FirstOrDefault(x => x.Name.Equals(nameof(PromotionAdditionalInformationComponent.Disclaimer), StringComparison.OrdinalIgnoreCase))?.Value; PromotionType tempPromotionType = PromotionType.None; viewProperty = arg.Properties.FirstOrDefault(x => x.Name.Equals(nameof(PromotionAdditionalInformationComponent.PromotionType), StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrEmpty(viewProperty?.Value) && Enum.TryParse(viewProperty?.Value, out tempPromotionType)) component.PromotionType = tempPromotionType; Sitecore.Commerce.Plugin.Promotions.Promotion promotion2 = await this._addPromotionAdditionalInformationCommand.Process(context.CommerceContext, promotion1, , component.Disclaimer, component.PromotionType, component.Combinable ); return arg; } } } namespace Plugin.Demo.Promotion.Commands { public class AddPromotionAdditionalInformationCommand: PromotionsCommerceCommand { private readonly IAddPromotionAdditionalInformationPipeline _addPromotionAdditionalInformationPipeline; public AddPromotionAdditionalInformationCommand( IAddPromotionAdditionalInformationPipeline addPromotionAdditionalInformationPipeline, IFindEntityPipeline findEntityPipeline, IServiceProvider serviceProvider) : base(findEntityPipeline, serviceProvider) { this._addPromotionAdditionalInformationPipeline = addPromotionAdditionalInformationPipeline; } public virtual async Task Process( CommerceContext commerceContext, Sitecore.Commerce.Plugin.Promotions.Promotion promotion, string disclaimer="", PromotionType promotionType = PromotionType.None, bool combinable = false) { AddPromotionAdditionalInformationCommand addPromotionAdditionalInformationCommand = this; Sitecore.Commerce.Plugin.Promotions.Promotion result = (Sitecore.Commerce.Plugin.Promotions.Promotion)null; Sitecore.Commerce.Plugin.Promotions.Promotion promotion1; using (CommandActivity.Start(commerceContext, (CommerceCommand)addPromotionAdditionalInformationCommand)) { CommercePipelineExecutionContextOptions contextOptions = commerceContext.GetPipelineContextOptions(); using (CommandActivity.Start(commerceContext, (CommerceCommand)addPromotionAdditionalInformationCommand)) { await addPromotionAdditionalInformationCommand.PerformTransaction(commerceContext, (Func)(async () => { CommercePipelineExecutionContextOptions pipelineContextOptions = commerceContext.GetPipelineContextOptions(); result = await this._addPromotionAdditionalInformationPipeline.Run(new PromotionAdditionalInformationArgument(promotion, disclaimer, promotionType, combinable), (IPipelineExecutionContextOptions)pipelineContextOptions); })); promotion1 = result; } return promotion1; } } } } namespace Plugin.Demo.Promotion.Pipelines.Blocks { public class AddAdditionalPromotionInformationBlock: PipelineBlock { public override async Task Run( PromotionAdditionalInformationArgument arg, CommercePipelineExecutionContext context) { AddAdditionalPromotionInformationBlock additionalPromotionInformationBlock = this; // ISSUE: explicit non-virtual call Condition.Requires(arg).IsNotNull((additionalPromotionInformationBlock.Name) + ": The block argument cannot be null."); // ISSUE: explicit non-virtual call Condition.Requires(arg.Promotion).IsNotNull((additionalPromotionInformationBlock.Name) + ": The promotion cannot be null."); // ISSUE: explicit non-virtual call Condition.Requires(arg.Disclaimer).IsNotNullOrEmpty((additionalPromotionInformationBlock.Name) + ": The disclaimer cannot be null or empty."); Sitecore.Commerce.Plugin.Promotions.Promotion promotion = arg.Promotion; CommercePipelineExecutionContext executionContext; if (!promotion.IsDraft(context.CommerceContext)) { executionContext = context; CommerceContext commerceContext = context.CommerceContext; string error = context.GetPolicy().Error; object[] args = new object[1] { (object) promotion.FriendlyId }; string defaultMessage = "'" + promotion.FriendlyId + "' cannot be edited or deleted because is approved."; executionContext.Abort(await commerceContext.AddMessage(error, "EntityIsApproved", args, defaultMessage), (object)context); executionContext = (CommercePipelineExecutionContext)null; return (Sitecore.Commerce.Plugin.Promotions.Promotion)null; } PromotionAdditionalInformationComponent promotionAdditionalInformationComponent = promotion.GetComponent(); return promotion; } } }