Crownpeak DXM integration with Salesforce Marketing Cloud forms

Partner
2 0 480

In this post, I wanted to show the steps to create an experience in Crownpeak DXM, for marketers & content managers, to build and deploy Salesforce Marketing Cloud (formerly known as Exact Target) forms without going through a development cycle. On submit, these forms in-turn creates a subscriber inside Exact Target (ET) and saves the same data to a Salesforce data extension.

 

Here are the steps we’ll follow:


1) In DXM, create the form builder for your users to create custom ET forms.


2) Create a middleware, an API / Server Side rendered page. To keep things simple here, we are using a plain ASP.Net page to do the trick. The server-side code uses Salesforce FuelSDK & talks to ET APIs to process the request.

3) Finally, use the form builder to publish a test, so you can confirm this works with different subscriber and data extension keys.

 

Let's first create a form builder template in a Crownpeak Instance or through the CDC.

It will create form fields using a simple input template and generate the form as the output.

When deployed, the form will submit data to an asp.net page that connects to ET, creates a subscriber entry and also then creates a new row of data into the data extension by using FuelSDK.

For simplicity, we'll follow the classic templating approach instead of using components for input/output code creation.

Create a template named ETFormBuilder. Open the input.aspx file and place the below code in the file.

 

    Dictionary<string, string> dic_control_type = new Dictionary<string, string>();
    dic_control_type.Add("Textbox", "txt");
    dic_control_type.Add("Radio Button", "rds");
    dic_control_type.Add("Checkbox", "chk");
    dic_control_type.Add("Drop Down List", "drp");
    dic_control_type.Add("HiddenField", "hdn");

    Dictionary<string, string> txt_type = new Dictionary<string, string>();
    txt_type.Add("Email Address", "email");
    txt_type.Add("TextArea", "textarea");
    txt_type.Add("Default", "default");


    Input.StartControlPanel("Exact Target Form Builder");

    Input.ShowTextBox("DE External Key", "de_ext_key", width: 50, helpMessage: "ET: Data Extension Key");
    Input.ShowTextBox("External Key", "ext_key", width: 50, helpMessage: "ET: Subscriber Consumer Key");

    while (Input.NextPanel("AddControl", displayName: "Add Control"))
    {
        Input.StartDropDownContainer("Field Type", "dd_fieldtype", dic_control_type);

        Input.ShowTextBox("Placeholder Text", "txt_placeholder", width: 50);
        Input.ShowTextBox("Label", "txt_label", width: 50);
        Input.ShowTextBox("Field ID", "txt_id", width: 50);
        Input.ShowRadioButton("Textbox Type", "rd_textType", txt_type, "default");
        Input.ShowCheckBox("", "chkTextArea", "yes", "Display as TextArea");

        Input.NextDropDownContainer();
        Input.ShowTextBox("Label", "txt_rdlabel", width: 50);
        Input.ShowTextBox("Field ID", "txt_rdid", width: 50);

        while (Input.NextPanel("rd_FieldValues", displayName: "FieldValues", min: 2))
        {
            Input.ShowTextBox("Value", "txt_rdval");
            Input.ShowTextBox("Label", "txt_rdlab");
        }

        Input.NextDropDownContainer();
        Input.ShowTextBox("Label", "txt_chklabel", width: 50);
        Input.ShowTextBox("Field ID", "txt_chkid", width: 50);

        while (Input.NextPanel("chk_FieldValues", displayName: "FieldValues", min: 2))
        {
            Input.ShowTextBox("Value", "txt_chkval");
            Input.ShowTextBox("Label", "txt_chklab");
        }

        Input.NextDropDownContainer();
        Input.ShowTextBox("Label", "txt_ddlabel", width: 50);
        Input.ShowTextBox("Field ID", "txt_ddid", width: 50);

        while (Input.NextPanel("dd_FieldValues", displayName: "FieldValues", min: 2))
        {
            Input.ShowTextBox("Value", "txt_ddval");
            Input.ShowTextBox("Label", "txt_ddlab");
        }

        Input.NextDropDownContainer();
        Input.ShowTextBox("Field ID", "txt_hdnid", width: 50);
        Input.ShowTextBox("Value", "txt_hdnvalue", width: 50);

        Input.EndDropDownContainer();
    }

    Input.EndControlPanel();

The code above will create form fields in the output. We also have two input fields, one for the data extension key and other for the subscriber consumer key. These fields are going to be part of form as hidden fields. In the form builder, each field id should exactly match with the column name of the data extension.

 

1 (1).png

 

Now let's do the output based on the input created. Open the output.aspx layout file and do the output as below:

 

<input type="hidden" name="hdn_de_external_key" id="hdn_de_external_key" value="<%=asset["de_ext_key"] %>">
<input type="hidden" name="hdn_sb_external_key" id="hdn_sb_external_key" value="<%=asset["ext_key"] %>">

<% 
foreach (PanelEntry pe in asset.GetPanels("addcontrol")) {

    if (pe["dd_fieldtype"].Equals("txt"))
    {
        if (pe["rd_textType"].Equals("email"))
        {%>
            <span><%=pe["txt_label"] %></span><input type="email" name="<%=pe["txt_id"] %>" id="<%=pe["txt_id"] %>" placeholder="<%=pe["txt_placeholder"] %>" />  
        <%}
        else if (pe["chkTextArea"].Equals("textarea"))
        {%>
            <span><%=pe["txt_label"] %></span> <textarea name="<%=pe["txt_id"] %>" id="<%=pe["txt_id"] %>" placeholder="<%=pe["txt_placeholder"] %>"></textarea>
        <%}
        else 
        {%>
            <span><%=pe["txt_label"] %></span> <input type="text" name="<%=pe["txt_id"] %>" id="<%=pe["txt_id"] %>" placeholder="<%=pe["txt_placeholder"] %>" />  
        <%}
    }
    else if (pe["dd_fieldtype"].Equals("rds"))
    {
        //Define Radiobuttom html as per above sample
    }
    else if (pe["dd_fieldtype"].Equals("chk"))
    {
        //Define Checkbox html as per above sample
    }
    else if (pe["dd_fieldtype"].Equals("drp"))
    {
        //Define select list html as per above sample
    }
    else if (pe["dd_fieldtype"].Equals("hdn"))
    {
        //Define hiddenfield html as per above sample
    }
}

%>

 

This code still requires a form tag implementation. We will fill it in after we create the middleware with ET APIs.

Now, let’s create a asp.net page, which reads and process the data to ET i.e. it saves the data into data extension, creates and triggers subscriber mail.

To do that, open Visual Studio (we are using VS2017) and create an empty application using the .net framework. We created this using framework 4.6.1. Once the solution is created, install SalesForce SDK from the package manager.

You can see an example in the screenshots below.

 

2 (1).png

 

Type FuelSDK inside the search box, list will appear as shown in the SS

 

 

3 (1).png

 

 

Select SFMC.FuelSDK, on the right bar select the required version, and install. This will add the necessary DLLs to work with the SDK. Now we are ready to move forward with the implementation.

Open the web.config file and this to setup the clientId and clientSecret to access the API through the SDK.

 

<configuration>
  <configSections>
    <section name="fuelSDK" type="FuelSDK.FuelSDKConfigurationSection, FuelSDK" />
  </configSections>

  <fuelSDK
    appSignature="none"
    clientId="CLIENT_ID"
    clientSecret="CLIENT_SECRET"
    />

</configuration>

 

clientId and clientSecret are necessary for SalesForce authentication. FuelSDK will automatically fetch it from the web.config file now to keep it secure.

Now we'll create a page. To keep it simple, we are using an asp.net form page and will perform all the actions on the Page_load event. At the top of the page, import the namespace below.

 

<%@ Import Namespace="Newtonsoft.Json" %>
<%@ Import Namespace="FuelSDK" %>

Add two functions, which will convert the form data into JSON String, and the JSON String to a list of object.

 

    protected string getJsonStringFromRequest(HttpRequest request)
    {
        string jsonString;
        var dict = HttpUtility.ParseQueryString(request.Form.ToString());
        jsonString = new JavaScriptSerializer().Serialize(
                            dict.AllKeys.ToDictionary(k => k, k => dict[k])
                   );
        if (String.IsNullOrWhiteSpace(jsonString))
        {
            jsonString = "{}";
        }
        return jsonString;
    }

    protected List<FormData> deserialize(string json)
    {
        try
        {
            List<FormData> formData = new List<FormData>();
            var dictData = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);

            foreach(var kv in dictData)
            {
                FormData dt = new FormData();
                dt.Key = kv.Key;
                dt.Value = kv.Value;
                formData.Add(dt);
            }
            return formData;
        }
        catch
        {
            return null;
        }
    }

 In the above code block, getJsonStringFromRequest method takes an httpRequest as parameter and converts the posted data into a json string. The second method, deserialize, converts the json string into a list of object of type FormData. FormData is a class and is defined below:

 

public class FormData
{
	public string Key { get; set; }
	public string Value { get; set; }
}

 Now, in the Page_Load event, fetch the forms data and store it into attributes list as shown below.

 

var formsData = deserialize(getJsonStringFromRequest(Request)); 
var attributes = new List<FuelSDK.Attribute>();
formsData.Where(d => !isEmail(d)).ToList().ForEach(d =>
{
	if (!d.Key.Equals("hdn_de_external_key") && !d.Key.Equals("hdn_sb_external_key"))
	{
		attributes.Add(new FuelSDK.Attribute
		{
			Name = d.Key,
			Value = d.Value
		});
	}
});

Next, create the subscriber entry with the below code:

 

var postSub = new ETSubscriber
{
	AuthStub = myclient,
	CustomerKey = sb_consumer_key,
	Attributes = attributes.ToArray()
};
var postResponse = postSub.Post();

If the subscriber is created successfully, we’ll store the same data into a Salesforce data extension, as shown below:

 

var deRowPost = new ETDataExtensionRow
{
	AuthStub = myclient,
	DataExtensionCustomerKey = de_external_key,
};

Dictionary<string, string> deRow = attributes.ToDictionary(de => de.Name, de => de.Value);
deRowPost.ColumnValues = deRow;
var prRowResponse = deRowPost.Post();

After combining all of the above code blocks and adding the logic, the page will contain the below code:

 

//Code Block ETAPI.aspx

<%@ Page Language="C#" Debug="true" %>

<%@ Page Language="C#" Debug="true" %>

<%@ Import Namespace="System" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.ServiceModel" %>
<%@ Import Namespace="System.ServiceModel.Channels" %>
<%@ Import Namespace="System.Linq" %>
<%@ Import Namespace="System.Web" %>
<%@ Import Namespace="System.Net" %>
<%@ Import Namespace="System.Net.Mail" %>
<%@ Import Namespace="System.Text" %>
<%@ Import Namespace="System.Reflection" %>
<%@ Import Namespace="System.Web.Services" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Collections.Specialized" %>
<%@ Import Namespace="System.Web.Script.Serialization" %>
<%@ Import Namespace="Newtonsoft.Json" %>
<%@ Import Namespace="FuelSDK" %>


<script runat="server">
    protected void Page_Load(object sender, EventArgs e)
    {
        Response.Clear();

        var myclient = new ETClient();
        var subscriberEmail = new FormData();
        string de_external_key = Request.Form["hdn_de_external_key"];
        string sb_consumer_key = Request.Form["hdn_sb_external_key"];

        var formsData = deserialize(getJsonStringFromRequest(Request));

        if (formsData != null && formsData.Any(d => isEmail(d)))
            subscriberEmail = formsData.Where(d => isEmail(d)).ToList().FirstOrDefault();
        else
            subscriberEmail = null;

        if (string.IsNullOrWhiteSpace(sb_consumer_key) || string.IsNullOrWhiteSpace(de_external_key))
        {
            Response.StatusCode = 400;
            Response.Write("The external key have not been set");
            Response.End();
            return;
        }

        if (formsData == null)
        {
            Response.StatusCode = 400;
            Response.Write("The request body failed to deserialize");
            Response.End();
            return;
        }


        if (subscriberEmail == null)
        {
            Response.StatusCode = 400;
            Response.Write("No receiver email address found in the request body");
            Response.End();
            return;
        }

        var deRows = new Dictionary<string, string>();

        var attributes = new List<FuelSDK.Attribute>();
        formsData.Where(d => !isEmail(d)).ToList().ForEach(d =>
        {
            if (!d.Key.Equals("") && !d.Key.Equals("hdn_sb_external_key"))
            {
                attributes.Add(new FuelSDK.Attribute
                {
                    Name = d.Key,
                    Value = d.Value
                });
            }
        });

        var postSub = new ETSubscriber
        {
            AuthStub = myclient,
            CustomerKey = sb_consumer_key,
            Attributes = attributes.ToArray()
        };

        var postResponse = postSub.Post();

        if (postResponse.Results == null || postResponse.Results[0].StatusCode != "OK")
        {
            Response.StatusCode = 400;
            Response.Write("The request failed with the following message: " + postResponse.Results[0].StatusMessage);
            Response.End();
            return;
        }
        else
        {
            var deRowPost = new ETDataExtensionRow
            {
                AuthStub = myclient,
                DataExtensionCustomerKey = de_external_key,
            };

            Dictionary<string, string> deRow = attributes.ToDictionary(de => de.Name, de => de.Value);
            deRowPost.ColumnValues = deRow;
            var prRowResponse = deRowPost.Post();

            if (prRowResponse.Results == null || prRowResponse.Results[0].StatusCode != "OK")
            {
                Response.StatusCode = 201;
                Response.Write(postResponse.Results[0].StatusCode + ": " + postResponse.Results[0].StatusMessage + " : " + prRowResponse.Results[0].StatusMessage);
                Response.End();
            }
        }

        Response.StatusCode = 201;
        Response.Write(postResponse.Results[0].StatusCode + ": " + postResponse.Results[0].StatusMessage);
        Response.End();

    }
    protected string getJsonStringFromRequest(HttpRequest request)
    {
        string jsonString;

        var dict = HttpUtility.ParseQueryString(request.Form.ToString());
        jsonString = new JavaScriptSerializer().Serialize(
                            dict.AllKeys.ToDictionary(k => k, k => dict[k])
                   );

        if (String.IsNullOrWhiteSpace(jsonString))
        {
            jsonString = "{}";
        }

        return jsonString;

    }
    protected List<FormData> deserialize(string json)
    {
        try
        {
            List<FormData> formData = new List<FormData>();
            var dictData = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);

            foreach(var kv in dictData)
            {
                FormData dt = new FormData();
                dt.Key = kv.Key;
                dt.Value = kv.Value;
                formData.Add(dt);
            }
            return formData;
        }
        catch
        {
            return null;
        }
    }
    protected bool isEmail(FormData obj)
    {
        try
        {
            MailAddress m = new MailAddress(obj.Value);
            return true;
        }
        catch (FormatException)
        {
            return false;
        }
    }
    public class FormData
    {
        public string Key { get; set; }
        public string Value { get; set; }
    }
</script>

 

This completes SalesForce integration with the asp.net page. When the form builder is made available to users, we will deploy this page along with the necessary dlls and web.config through Crownpeak DXM.

Before moving further, we still must complete the form builder output by adding the form tag and a submit button. The form action will be asp.net page created above, and method will be POST.

The output should look as below:

 

<form action="url_of_asp.net_page" method="post">

<input type="hidden" name="hdn_de_external_key" id="hdn_de_external_key" value="<%=asset["de_ext_key"] %>">
<input type="hidden" name="hdn_sb_external_key" id="hdn_sb_external_key" value="<%=asset["ext_key"] %>">

<% 
foreach (PanelEntry pe in asset.GetPanels("addcontrol")) {

    if (pe["dd_fieldtype"].Equals("txt"))
    {
        if (pe["rd_textType"].Equals("email"))
        {%>
            <span><%=pe["txt_label"] %></span><input type="email" name="<%=pe["txt_id"] %>" id="<%=pe["txt_id"] %>" placeholder="<%=pe["txt_placeholder"] %>" />  
        <%}
        else if (pe["chkTextArea"].Equals("textarea"))
        {%>
            <span><%=pe["txt_label"] %></span> <textarea name="<%=pe["txt_id"] %>" id="<%=pe["txt_id"] %>" placeholder="<%=pe["txt_placeholder"] %>"></textarea>
        <%}
        else 
        {%>
            <span><%=pe["txt_label"] %></span> <input type="text" name="<%=pe["txt_id"] %>" id="<%=pe["txt_id"] %>" placeholder="<%=pe["txt_placeholder"] %>" />  
        <%}
    }
    else if (pe["dd_fieldtype"].Equals("rds"))
    {
        //Define Radiobuttom html as per above sample
    }
    else if (pe["dd_fieldtype"].Equals("chk"))
    {
        //Define Checkbox html as per above sample
    }
    else if (pe["dd_fieldtype"].Equals("drp"))
    {
        //Define select list html as per above sample
    }
    else if (pe["dd_fieldtype"].Equals("hdn"))
    {
        //Define hiddenfield html as per above sample
    }
}
%>
    <input type="submit" value="Submit" />
</form>

Finally, it is time to test the whole setup – set up a model and create a few ET forms from the CMS with different subscriber and data extension keys.

 

Let me know how that works for you!