Tuesday 11 November 2014

Umbraco Contour - Campaign Monitor Integration

There are a number of blog posts, forum posts and documents floating around about how to integrate with Campaign Monitor from Umbraco Contour. I used all of these to reach my eventual solution.

Rather than say what was wrong with each of these, I’d say that they were all instrumental in achieving the final result, but none of them on their own was enough and I would like to repay the help by making this the definitive post on how to achieve this.

Please note that this solution was developed in Umbraco 6.2.2 and I have not yet tried within an Umbraco 7 installation, though my assumption is that it should work there too.

Also to note that while this post will get you a working solution, I was up against a deadline and I have not had as much time to spend on this as I would like to fully understand the code, therefore at the end of the post I have included a ‘what’s next’ section with some thoughts and when time permits I intend to follow up on this to refactor the code once I fully understand what’s happening.

I’d like to acknowledge the following:
·         Tim Geyssens - https://twitter.com/timgeyssens
·         Greg Fyans - https://twitter.com/gfyans
·         Ravi Motha - https://twitter.com/ravimotha

Requirements

One thing that some of the other resources were lacking was to tell you which dll’s you need. You will need:
·         Createsend_dotnet – a .NET wrapper for the campaign monitor API. You can get this from GitHub: https://github.com/campaignmonitor/createsend-dotnet
·         System.Web.UI.WebControls
·         Umraco.Forms.Core

API Key

You will need the campaign monitor API key. This page will show you how to find it: http://help.campaignmonitor.com/topic.aspx?t=206
I saved mine as an appSettings entry in web.config in case it changes later.

Client ID

You will also need a client ID. Within campaign monitor you may have several clients and each of those may have several lists, therefore, we need to ensure we’re getting the lists of the correct client. This page will show you how to find this: https://www.campaignmonitor.com/api/getting-started/#clientid
Again I saved this in web.config.

Render Control

The first thing we need is a checkbox list containing each of the lists within campaign monitor to display in Contour.

To do this we need a class which inherits from FieldSettingType class.

public class CampaignMonitorListsFieldSettingsType : FieldSettingType
{

We also need to instantiate a new CheckBoxList object whose class is contained within System.Web.UI.WebControls namespace.

private CheckBoxList cbl = new CheckBoxList();
private string _val = string.Empty;

We need to assign the returned CheckBoxList to a string which is decorated with a special attribute which Contour recognises and creates a space in memory whenever contour is loaded, or so the documentation tells me.

[Umbraco.Forms.Core.Attributes.Setting("ListsToInclude",
description = "The selected lists to which the user can subscribe",
control = "MyApp.WebApp.Utils.CampaignMonitorListsFieldSettingsType",
assembly = "MyApp.WebApp")]
public string ListsToInclude { get; set; }

Notice that the control is the name of our class fully namespaced and the assembly is, well, the assembly J

Now we can go about overriding the methods:

public override string Value
{
get
       {
              return cbl.Items.ToString();
       }
       set
       {
              if (!string.IsNullOrEmpty(value))
                _val = value;
       }
 }
This basically gets the value of the checked lists in Contour and saves them. This will be clearer in a minute.

public override WebControl RenderControl(Umbraco.Forms.Core.Attributes.Setting setting, Form form)
        {
            cbl.ID = setting.GetName();
            cbl.RepeatLayout = RepeatLayout.Flow;
            cbl.CssClass = "cml";
            string apiKey = ConfigurationManager.AppSettings["CampaignMonitorAPIKey"] as string;
            string clientId = ConfigurationManager.AppSettings["CampaignMonitorClientID"] as string;
            AuthenticationDetails authDetails = new ApiKeyAuthenticationDetails(apiKey);
            Client client = new Client(authDetails, clientId);
            IEnumerable<BasicList> lists = client.Lists();
            ListItem li;
            foreach (BasicList list in lists)
            {
                li = new ListItem(list.Name, list.ListID);
                cbl.Items.Add(li);
            }
            IEnumerable<string> vals = _val.Split(',');
            foreach (string v in vals)
            {
                ListItem selLi = cbl.Items.FindByValue(v);
                if (selLi != null)
                {
                    selLi.Selected = true;
                }
            }
            return cbl;
        }
What we are doing in this method is going to Campaign Monitor API and getting all the lists that belong to the client ID and returning them as a CheckBoxList.

Pre Value Source

The next class we need, displays the lists as a CheckBoxList so that the user can select which lists they would like to include.

public class CampaignMonitorExtension : FieldPreValueSourceType
    {
        [Umbraco.Forms.Core.Attributes.Setting("ListsToInclude",
       description = "The selected lists to which the user can subscribe",
       control = "MyApp.WebApp.Utils.CampaignMonitorListsFieldSettingsType",
       assembly = "MyApp.WebApp")]
        public string ListsToInclude { get; set; }

        public CampaignMonitorExtension()
        {
            this.Id = new Guid("9fd33370-2fb6-4fbb-88e2-3d14d2e65772");
            this.Name = "Campaign Monitor Mailing Lists";
            this.Description = "List of mailing lists available within campaign monitor";
        }

        public override List<PreValue> GetPreValues(Field field)
        {
            List<PreValue> pvs = new List<PreValue>();
            string apiKey = ConfigurationManager.AppSettings["CampaignMonitorAPIKey"] as string;
            string clientId = ConfigurationManager.AppSettings["CampaignMonitorClientID"] as string;
            AuthenticationDetails authDetails = new ApiKeyAuthenticationDetails(apiKey);
            Client client = new Client(authDetails, clientId);
            IEnumerable<BasicList> lists = client.Lists();
            PreValue preValue;
            IEnumerable<string> vals = ListsToInclude.Split(',');
            foreach (string v in vals)
            {
                BasicList l = lists.FirstOrDefault(s => s.ListID == v);
                preValue = new PreValue();
                preValue.Value = l.Name;
                preValue.Id = l.ListID;
                pvs.Add(preValue);
            }
            return pvs;
        }

        public override List<Exception> ValidateSettings()
        {
            List<Exception> exs = new List<Exception>();
            if (string.IsNullOrEmpty(ListsToInclude))
            {
                exs.Add(new Exception("Lists To Include is Empty"));
            }
            return exs;
        }
    }

Workflow

The final piece of the jigsaw is a workflow type.

Please pay special attention to the code which signs the user up – very important!

This is what happens when a user clicks the checkbox(es) and submitting the form:

public class CampaignMonitorSend : WorkflowType
    {
        [Umbraco.Forms.Core.Attributes.Setting("Lists To Include",
        description = "The selected lists to which the user can subscribe",
        control = "MyApp.WebApp.Utils.CampaignMonitorListsFieldSettingsType",
        assembly = "MyApp.WebApp")]
        public String ListsToAddTo { get; set; }

        public CampaignMonitorSend()
        {
            this.Id = new Guid("9fd33370-2fb6-4fbb-88e2-3d14d2e65772");
            this.Name = "Campaign Monitor Mailing Lists";
            this.Description = "List of mailing lists available within campaign monitor";
        }

        public override WorkflowExecutionStatus Execute(Record record, RecordEventArgs e)
        {
            string apiKey = ConfigurationManager.AppSettings["CampaignMonitorAPIKey"] as string;
            string clientId = ConfigurationManager.AppSettings["CampaignMonitorClientID"] as string;
            AuthenticationDetails authDetails = new ApiKeyAuthenticationDetails(apiKey);
            Client client = new Client(authDetails, clientId);
            IEnumerable<string> listIds = ListsToAddTo.Split(',');

            foreach (string listId in listIds)
            {
                //Only add to campaign monitor if the signup checkbox is checked
                if (listId.Length > 2 && record.GetRecordField(new Guid("16922c9c-ffb6-4986-9c1e-324a9fa87e6a")).Values[0].ToString().ToLower() == "true")
                {
                    Subscriber s = new Subscriber(authDetails, listId);
                    string name = (record.GetRecordField("Name").Values.Count > 0) ? record.GetRecordField("Name").Values[0].ToString() : string.Empty;
                    string sid = s.Add(record.GetRecordField("Email").Values[0].ToString(), name, null, true);
                }
            }

            return Umbraco.Forms.Core.Enums.WorkflowExecutionStatus.Completed;
        }

        public override List<Exception> ValidateSettings()
        {
            List<Exception> exs = new List<Exception>();

            if (string.IsNullOrEmpty(ListsToAddTo))
                exs.Add(new Exception("Lists to add to are empty"));

            return exs;
        }
    }

By the time you have this code in place, you will have a working solution similar to what this blog post tried to achieve: http://www.liquidint.com/blog/extending-umbraco-contour-campaign-monitor-integration/

What’s next

As I said at the beginning of this post, I did not have time to refactor and fully understand this code, but my plan is to do that and then possibly develop a package which a number of people have been requesting in the forums.

A few things to note:

  • When you have added your custom pre value source in contour and you select lists in the Checkbox list and save, the values ARE updated in Umbraco but the checkbox state is not remembered, so the next time you visit this you will see a list of unchecked boxes.  I plan to fix this ASAP.
  • The code is supposed to allow you to choose this in the workflow UI, so that you can display whichever lists you want to sign users up to in the front end. This does not currently work. My personal feeling on this is that you may not wish to hand that control over to front end users, but in some cases you might so this is also planned for fix.
  • Subscribing users
//Only add to campaign monitor if the signup checkbox is checked
if (listId.Length > 2 && record.GetRecordField(new Guid("16922c9c-ffb6-4986-9c1e-324a9fa87e6a")).Values[0].ToString().ToLower() == "true")
{
Subscriber s = new Subscriber(authDetails, listId);
string name = (record.GetRecordField("Name").Values.Count > 0) ? record.GetRecordField("Name").Values[0].ToString() : string.Empty;
string sid = s.Add(record.GetRecordField("Email").Values[0].ToString(), name, null, true);
}

This is the section of code which has changed from the blog post I followed. In order to check if the checkbox was checked when the form was submitted, we have to look at the value of it but this is done by GUID. I had to debug the app and take a note of the GUID (you could also get this from the database) and hard code it. This is not ideal because if the checkbox was deleted and recreated in the form designer, the GUID would change and the code inside the if statement would never be reached. Either that or it would fall over when it couldn’t find the GUID. It is my top priority to find a better solution to this.
  • In both CampaignMonitorListsFieldSettingsType and CampaignMonitorExtension classes we are going to the CampaignMonitor API to do the same thing – get lists of lists. I am not convinced this is necessary, it certainly doesn’t make sense to hit the API twice but I would like to find out if this is actually required for the integration to work.

 So, that's it. I hope this is of use and please feel free to get in touch if there are any questions or suggestions, or even any mistakes - Bear in mind I intend to fix all of the above and then hopefully make this into a package. Soon!

No comments:

Post a Comment