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.

epi_server_logo_detail

The steps to solve this problem are the following:

  1. Create a custom form container block to set some needed variables for this scenario
  2. Create a custom actor model to store the required information
  3. Create a custom actor which uses the previous model
  4. Create an append to redirection service to reformat the error message
  5. Create a form confirmation page which will process the error message
  6. 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 =&gt;

                        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 &amp;&amp; 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 =&gt; x.DefaultSuccessTitle)</h1>
</div>
}

        if (!string.IsNullOrEmpty(Model.DefaultSuccessText))

        {
<div class="rtf rtf--narrow">
                @Html.PropertyFor(x =&gt; x.DefaultSuccessText)</div>
}

        if (!Model.HideTitle)

        {
<div class="styled-border heading-wrapper">
<h1>@Html.PropertyFor(x =&gt; x.DefaultFailureTitle)</h1>
</div>
}

        if (!string.IsNullOrEmpty(Model.DefaultFailureText))

        {
<div class="rtf rtf--narrow">
                @Html.PropertyFor(x =&gt; 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>

b2

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

b1

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

b3

We can optionally add the success and failure message in the form data

b4

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

b5

Do not forget to remove the webhook if it was enabled

b6

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.

b7

I hope it will help someone and as always keep learning !!!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s