Go to main content

Sync form submissions with Integration bus in Kentico Xperience

In this post, I will provide you with a code snippet and tips on how to synchronize new form submissions with an external system through its REST API using the Integration bus in Kentico Xperience.

Problem

When you need to react to events that happen in Xperience and communicate with external systems, you have multiple options of implementing it. One of the most popular approaches is Global event handlers. Regrettably, they are synchronous and you may lose your data when the external system is not available at the time of the synchronization.

Another approach is using the Integration bus. It could be implemented in an asynchronous fashion. Also, when the external system is not available, Xperience retries the synchronization task which prevents you from losing your data. Moreover, Xperience has a nice UI where you can manage the synchronization tasks.

In one of my projects, I decided to use the Integration bus to synchronize form submissions through REST API. Despite the fact that Xperience has complex documentation, I battled a few things during the implementation and I will mention them. Also, I will provide you with a code snippet that does the job.

Solution

I love code examples and the best way to start implementing the Integration bus in your project is the example outlined in the Xperience docs. When I finished implementing the example on my machine, I ended up with a partially working outcome. The form submission tasks did not get tracked by the Integration bus. The problem was that I did not reference the new Class library in both live site and Xperience administration projects.

The other problem was that I wasn't able to find an object type codename of form submissions. By specifying the object type, the Integration bus can subscribe to object events. Object types registered in Xperience can be found in the System > Object types application. Regrettably, there is no mention of an object type that would represent a form submission. In the end, I found out the object type codename has the following format bizformitem.bizform.<your_form_codename_lowercased>. This is not mentioned in the documentation.

In the following code snippet, I outline code for an Integration bus Class library. The code does the following actions:

  1. Subscribes to Create object event of your form submissions.
  2. When the event occurs, the form data get serialized in a JSON structure and sent through a REST API.
  3. The snippet also shows 
    • how to access form attachments and convert them in Base64,
    • how to log messages in the Event log
using System;
using System.Net;
using System.Text;
using System.IO;
using CMS;
using CMS.Core;
using CMS.Base;
using CMS.SynchronizationEngine;
using CMS.DataEngine;
using CMS.Synchronization;
using CMS.OnlineForms;
using CMS.Helpers;
using CMS.FormEngine;
using Newtonsoft.Json;

[assembly: RegisterCustomClass("IntegrationConnector", typeof(IntegrationConnector))]

public class IntegrationConnector : BaseIntegrationConnector
{
    protected const string FORM_OBJECTTYPE = PredefinedObjectType.BIZFORM_ITEM_PREFIX + "bizform.<your_form_codename_lowercased>";

    public override void Init()
    {
        ConnectorName = GetType().Name;
        SubscribeToObjects(TaskProcessTypeEnum.AsyncSimpleSnapshot, FORM_OBJECTTYPE, TaskTypeEnum.CreateObject);
    }

    public override IntegrationProcessResultEnum ProcessInternalTaskAsync(GeneralizedInfo infoObj, TranslationHelper translations, TaskTypeEnum taskType, TaskDataTypeEnum dataType, string siteName, out string errorMessage)
    {
        errorMessage = null;

        try
        {
            if (infoObj.TypeInfo.ObjectType == FORM_OBJECTTYPE)
            {
                ProcessSubmission(infoObj);
                return IntegrationProcessResultEnum.OK;
            }
            else
            {
                errorMessage = "Unknown object type: " + infoObj.TypeInfo.ObjectType;
                return IntegrationProcessResultEnum.ErrorAndSkip;
            }
        }
        catch (Exception ex)
        {
            errorMessage = ex.Message;
            Service.Resolve<IEventLogService>().LogError(ConnectorName, taskType.ToString(), errorMessage);
            return IntegrationProcessResultEnum.Error;
        }
        finally
        {
            ClearInternalTranslations();
        }
    }

    internal void ProcessSubmission(ISimpleDataContainer infoObj)
    {
        int itemId = ValidationHelper.GetInteger(infoObj.GetValue("FormID"), 0);
        string column = JsonConvert.ToString(ValidationHelper.GetString(infoObj.GetValue("ColumnName"), ""));
        string file = GetFileValue(itemId);
        string fileBase64 = GetFileBase64(file);

        HttpWebRequest request = (HttpWebRequest)WebRequest.Create("<your_endpoint_url>");
        string body = "{ \"columnName\": " + column + ", \"file\": \"" + fileBase64 + "\" }";
        Service.Resolve<IEventLogService>().LogInformation(ConnectorName, "HTTP_REQUEST", body);
        byte[] data = Encoding.ASCII.GetBytes(body);
        request.Method = "POST";
        request.ContentType = "application/json";
        request.ContentLength = data.Length;

        using (var stream = request.GetRequestStream())
        {
            stream.Write(data, 0, data.Length);
        }

        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        string responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
        Service.Resolve<IEventLogService>().LogInformation(ConnectorName, "HTTP_RESPONSE", responseString);
    }

    internal string GetFileValue(int itemId) 
    {
        BizFormInfo formObject = BizFormInfo.Provider.Get("<your_form_codename>", <your_site_id>);
        DataClassInfo formClass = DataClassInfoProvider.GetDataClassInfo(formObject.FormClassID);
        string className = formClass.ClassName;
        ObjectQuery<BizFormItem> data = BizFormItemProvider.GetItems(className);

        if (!DataHelper.DataSourceIsEmpty(data))
        {
            foreach (BizFormItem item in data)
            {
                if (item.GetIntegerValue("FormID", 0) == itemId)
                {
                    return item.GetStringValue("<form_attachment_field_codename>", "");
                }
            }
        }

        return "";
    }

    internal virtual string GetFileBase64(string file)
    {
        if (!String.IsNullOrEmpty(file))
        {
            byte[] attachmentData = GetFormAttachment(file);
            return Convert.ToBase64String(attachmentData);
        }

        return "";
    }

    protected byte[] GetFormAttachment(string attachmentFieldValue)
    {
        string fileName = FormHelper.GetGuidFileName(attachmentFieldValue);
        string filePath = FormHelper.GetFilePhysicalPath("<your_site_name>", fileName);

        if (File.Exists(filePath))
        {
            byte[] bytes = File.ReadAllBytes(filePath);

            return bytes;
        }

        return null;
    }
}

Syncing form submission from multiple forms

If you need to synchronize form submission from multiple forms that you are not able to specify with form codename, there is a way. This also applies to forms that will be created in the future. The Init method implementation is different. You need to specify the ObjectIntegrationSubscription object and then call the  SubscribeTo method with that object as a parameter.

The crucial aspect is the object type that uses a wildcard that ensures form submissions are observed in all forms.  here is the code snippet:

public override void Init()
    {
        ConnectorName = GetType().Name;
        ObjectIntegrationSubscription objSub = new ObjectIntegrationSubscription(ConnectorName, TaskProcessTypeEnum.AsyncSimpleSnapshot, TaskTypeEnum.CreateObject, null, BizFormItemProvider.BIZFORM_ITEM_PREFIX + "%", null);
        SubscribeTo(objSub);
    }

Further reading

all posts
  • Front-end & JavaScript

    Table of contents in Javascript

    In this post, I will provide you with a javascript code sample that transforms headings in a page into a structured table of contents with anchor links.

  • Kentico Xperience

    What is Kentico

    In this article you'll find a short summary about what Kentico is.

  • Front-end & JavaScript

    Why I don't use Bootstrap or any other complex UI framework

    In the past, I have developed quite a few sites using Bootstrap. After a while, I came to a decision not to use it because I saw a lot of problems related to slicing sites using Bo…