EPiServer Custom Actors with Page Redirection and CRM Process
Sometimes a web-hook attached to a submit button in EPiServer forms is not informative enough to the user. This happens when the web-hook tries to execute an action in an external system like a CRM. No matter the result of that action, the user cannot know if the operation was successful or not because EPiServer forms at that point shows the user that the submit was successful, which it was, but no feedback about the integration call. In order to solve this issue we will create a custom form container block, a custom actor with a custom model an append to redirection service, and a form confirmation page which in conjunction will be capable of dealing with this scenario. It is important to emphasize that this example it is not at all perfect and can be greatly improved.
The steps to solve this problem are the following:
- Create a custom form container block to set some needed variables for this scenario
- Create a custom actor model to store the required information
- Create a custom actor which uses the previous model
- Create an append to redirection service to reformat the error message
- Create a form confirmation page which will process the error message
- Configure the new actor in the editor interface
So once again without more hustle lets code !!!
Create a custom form container block
We will begin creating a custom form container block which must inherit from FormContainerBlock and be decorated with the attribute ServiceConfiguration. You can add as many properties as you want or need in here.
[ContentType(DisplayName = "Custom Form Container", GUID = "8fc36398-05e3-44b7-a6fd-6f025700b202", Description = "A form container that adds some message fields.", Order = 4000)] [ServiceConfiguration(typeof(IFormContainerBlock))] public class CustomFormContainerBlock : FormContainerBlock { [Display(GroupName = SystemTabNames.Content, Order = 4)] [CultureSpecific] public virtual XhtmlString SuccessMessage { get; set; } [Display(GroupName = SystemTabNames.Content, Order = 5)] [CultureSpecific] public virtual XhtmlString FailureMessage { get; set; } // This variable is just passed to and from internal calls // Does not need to be editable [Ignore] public bool Succeed { get; set; } }
The Success and Failure messages can be used when the CRM or external system call succeed or fails respectively
Create a custom actor model
Then we will create a new actor model which must be serializable and implement the interfaces IPostSubmissionActorModel and ICloneable
[Serializable] public class ConfigurableActorModel : IPostSubmissionActorModel, ICloneable { [Display(Name = "Service Type", Order = 10)] public virtual string ServiceType { get; set; } #region ICloneable Members public object Clone() { return new ConfigurableActorModel { ServiceType = ServiceType }; } #endregion }
In this case we will be adding a service type variable which will be set in the editor to decide which service or action must be executed in the CRM or external system
Create a custom actor
Now, we will create a custom actor which must inherit from PostSubmissionActorBase and implement the interface IUIPropertyCustomCollection. Pay attention to the comments since this is quite a long class
public class ConfigurableActor : PostSubmissionActorBase, IUIPropertyCustomCollection { /// <summary> /// We set this variable to true because we are going to modify the response from the visitor /// </summary> public override bool IsSyncedWithSubmissionProcess { get { return true; } } public override object Run(object input) { var ret = string.Empty; #region Inspect some important inputs of this Actor // We will get the current form from the content repository var contentRepository = ServiceLocator.Current.GetInstance(); var currentForm = contentRepository.Get(FormIdentity.Guid) as FormContainerBlock; // Get all the information from the submitted form var transformedData = new Dictionary(); // It always succeed unless it goes to the CRM (It can fail there) if (currentForm != null) { currentForm.Succeed = true; foreach (var submissionKv in SubmissionData.Data) { // It takes friendly name from the field name var currentField = currentForm.ElementsArea.Items.SingleOrDefault(x => x.ContentLink.ID.ToString() == submissionKv.Key.Replace("__field_", "")); if (currentField != null) { var inputElement = contentRepository.Get(currentField.ContentGuid); transformedData.Add(inputElement.Content.Name, submissionKv.Value); } } } // Get information from Editor UI of this Actor var configs = Model as IEnumerable; var configurableActorModels = configs as IList ?? configs?.ToList(); // If the form does not have this field or does not have // configurations in this field return success if (configs == null || !configurableActorModels.Any()) { return ret; } // Get the service type name that comes from the editor interface var serviceType = configurableActorModels.FirstOrDefault()?.ServiceType; #endregion #region Execute main business of this actor // Send the transformedData to 3rd party server, or save to XML file if (serviceType != null && currentForm != null) { // Create a web service call object and transform the form data // to Json. Web service is a fictional class, you should replace // with your implementation which calls a real service API var service = new WebService(); var data = JToken.FromObject(transformedData); // If the variable set by the editor is sent-order, call the // fictitious external call if (serviceType.ToLower().Contains("send-order")) { // Send request with the data to the URL var result = service.post("/api/send-order", data); // If the request was successful return true in Succeed Boolean variable currentForm.Succeed = result == "OK"; } } #endregion return ret; } #region IUIPropertyCustomCollection Members public virtual Type PropertyType { get { // Set the property type of this actor to use the custom model previously defined return typeof(PropertyForDisplayingConfigurableActor); } } #endregion } /// <summary> /// Property definition for the Actor /// </summary> [EditorHint("ConfigurableActorPropertyHint")] [PropertyDefinitionTypePlugIn(DisplayName = "ConfigurableActorProp")] public class PropertyForDisplayingConfigurableActor : PropertyGenericList { } /// <summary> /// Editor descriptor class, for using Dojo widget CollectionEditor to render. /// Inherit from , it will be rendered as a grid UI. Not always necessary, it can be rendered as a input field too /// </summary> [EditorDescriptorRegistration(TargetType = typeof(IEnumerable), UIHint = "ConfigurableActorPropertyHint")] public class ConfigurableActorEditorDescriptor : CollectionEditorDescriptor { public ConfigurableActorEditorDescriptor() { ClientEditingClass = "epi-forms/contentediting/editors/CollectionEditor"; } }
We decided to render the custom actor model as a grid, but it can also be rendered different. The web service class is not a .NET framework or custom class. It must be changed with your own implementation that can call a real external service and return a success or failure answer
Create an append to redirection service
We will then create an append to redirection service which must inherit from DefaultAppendExtraInfoToRedirection and be decorated with the ServiceConfiguration attribute.
[ServiceConfiguration(typeof(IAppendExtraInfoToRedirection))] public class AppendInfoToRedirection : DefaultAppendExtraInfoToRedirection { public override IDictionary GetExtraInfo(FormIdentity formIden, Submission submission) { var contentRepository = ServiceLocator.Current.GetInstance(); var currentForm = contentRepository.Get(formIden.Guid) as CustomFormContainerBlock; var info = base.GetExtraInfo(formIden, submission); // If the current form is our custom one add as query string if the form succeeded. if (currentForm != null) { info.Add("success", currentForm.Succeed); } return info; } }
Just as a reminder, if the custom form container does not have the service type parameter configure in the editor. It will behave as a normal form which always returns true If the form does not contain a redirection page, the method in this class will not be executed at all.
Create a form confirmation page
Now, we will create a normal page that inherits from PageData and add to it some basic parameters
[SiteContentType(GUID = "5c2a6ae1-6880-4ffa-a72f-eb2e7e8ba65a", GroupName = "Pages")] public class FormConfirmationPage : PageData { [CultureSpecific] [Display(Order = 1, GroupName = SystemTabNames.Content)] public virtual string DefaultSuccessTitle { get; set; } [CultureSpecific] [Display(Order = 2, GroupName = SystemTabNames.Content)] public virtual string DefaultFailureTitle { get; set; } [CultureSpecific] [Display(Order = 3, GroupName = SystemTabNames.Content)] public virtual XhtmlString DefaultSuccessText { get; set; } [CultureSpecific] [Display(Order = 4, GroupName = SystemTabNames.Content)] public virtual XhtmlString DefaultFailureText { get; set; } }
Then, we will create the controller for this page.
// Normal page controller public class FormConfirmationPageController : PageController { public ActionResult Index(FormConfirmationPage currentPage) { // Process the error from the query string var result = Request.QueryString[Global.QueryStrings.Success]; var success = !string.IsNullOrEmpty(result) && bool.Parse(result.ToLower()); var formGuid = Request.QueryString[Global.QueryStrings.FormGuid]; if (result == null || formGuid == null) { return GetViewVerifyingAge(currentPage); } var successMessage = currentPage.DefaultSuccessText; var failureMessage = currentPage.DefaultFailureText; if (!string.IsNullOrEmpty(formGuid)) { var contentRepository = ServiceLocator.Current.GetInstance(); var currentForm = contentRepository.Get(new Guid(formGuid)) as ReyesFormContainerBlock; if (currentForm != null) { if (currentForm.SuccessMessage != null) { successMessage = currentForm.SuccessMessage; } if (currentForm.FailureMessage != null) { failureMessage = currentForm.FailureMessage; } } } string finalMessage; string finalTitle; if (success) { finalTitle = currentPage.DefaultSuccessTitle; finalMessage = successMessage.ToHtmlString(); } else { finalTitle = currentPage.DefaultFailureTitle; finalMessage = failureMessage.ToHtmlString(); } ViewBag.finalMessage = finalMessage; ViewBag.finalTitle = finalTitle; return GetViewVerifyingAge(currentPage); } } }
Here we process the error that comes from the query string. If the error is not present, the default data in the fields of the page will be shown. Now, we also add the view for the current page
@using EPiServer.Editor @using EPiServer.Web.Mvc.Html @using Reyes.Util.Helpers @model FormConfirmationPage @if (Model == null) { return; } <div class="content-frame"> @if (PageEditing.PageIsInEditMode) { if (!Model.HideTitle) { <div class="styled-border heading-wrapper"> <h1>@Html.PropertyFor(x => x.DefaultSuccessTitle)</h1> </div> } if (!string.IsNullOrEmpty(Model.DefaultSuccessText)) { <div class="rtf rtf--narrow"> @Html.PropertyFor(x => x.DefaultSuccessText)</div> } if (!Model.HideTitle) { <div class="styled-border heading-wrapper"> <h1>@Html.PropertyFor(x => x.DefaultFailureTitle)</h1> </div> } if (!string.IsNullOrEmpty(Model.DefaultFailureText)) { <div class="rtf rtf--narrow"> @Html.PropertyFor(x => x.DefaultFailureText)</div> } } else { <div class="styled-border heading-wrapper"> <h1>@ViewBag.finalTitle</h1> </div> <div class="rtf rtf--narrow"> @Html.Raw(ViewBag.finalMessage)</div> }</div>
We take into account the edition and view mode for this form confirmation page view
Configure the new actor in the editor interface
Finally, we will go to the CMS editor and add a new form confirmation page
Then in a old form or a new created one using the custom form container block, we will link the field Display page after submission to the previously created form confirmation page
We can optionally add the success and failure message in the form data
Then we go to the settings tab and in the Service Type parameter we will add the service name we want to call. In our case send-order
Do not forget to remove the webhook if it was enabled
And that is all. Every time the user press the submit button the append to custom actor will execute the external call and save the error message. The append to redirection will process the error message as query string and the form confirmation page controller will validate this message and show the proper one with fall out behaviors in the view.
I hope it will help someone and as always keep learning !!!
Leave a Reply