Monday, September 28, 2015

Sitecore Web Forms for Marketers as a Service

Customizing the WFFM module for Sitecore has provided post material for quite a few blogs, including this one. It's a favorite among clients, and it's fairly extensible. However, we all know that once in a while, we get the occasional request for a marketing form, which the module simply does not work for. My last endeavor into forms included a fancy animated multiple step wizard with interdependent fields, which would be an immense undertaking to implement with WFFM versus a simple custom .net form.

So the question became, how do I combine the value we get out of WFFM setup, save actions and reporting with the amazing designs and form flowcharts, which the design teams come up with and which our front end developers code into clean and beautiful html5. Especially in cases where we've already developed quite a few save actions that post to various client CRMs, create leads, and talk to third parties. Who wants to recode all that for a single custom form?

So I started working towards a concept, which would provide an endpoint for WFFM form submissions. From anywhere. It is fairly simple and straight forward to implement (wait till we get to the code part), but it becomes powerful in that it allows developers to have full control over the rendered html and still take advantage of the WFFM save actions.

Step 1. Run WFFM save actions on submitted form data.

We'll need to define a couple of classes for consumers of the WFFM service

   public class FormData
    {
        public string FormId { get; set; }

        public IEnumerable<FormField> Fields { get; set; } 
    }
    public class FormField
    {
        public string FieldName { get; set; }

        public string FieldValue { get; set; }
    }
And the meaty part - the FormProcessor, which is responsible for running the WFFM save actions, and of course has a dependency on the Sitecore and WFFM assemblies. With the help of a reflection tool, we can imitate what WFFM does behind the scenes here:

    public class FormProcessor
    {
        public FormProcessorResult Process(FormData data)
        {
            FormProcessorResult result = new FormProcessorResult();

            if (string.IsNullOrEmpty(data.FormId))
            {
                result.Success = false;
                result.ResultMessage = "Invalid Form Id";
            }
            else
            {
                bool failed = false;

                ID formId = new ID(data.FormId);
                FormItem formItem = FormItem.GetForm(formId);

                if (formItem != null)
                {
                    //Get form fields of the WFFM
                    FieldItem[] formFields = formItem.FieldItems;

                    //Create collection of fields
                    List<AdaptedControlResult> adaptedFields = new List<AdaptedControlResult>();
                    foreach (FormField field in data.Fields)
                    {
                        FieldItem formFieldItem = formFields.FirstOrDefault(x => x.Name == field.FieldName);
                        if (formFieldItem != null)
                        {
                            adaptedFields.Add(GetControlResult(field.FieldValue, formFieldItem));
                        }
                        else
                        {
                            // log and bail out
                            Log.Warn(string.Format("Field Item {0} not found for form with ID {1}", field.FieldName, data.FormId), this);
                            failed = true;
                            result.Success = false;
                            result.ResultMessage = string.Format("Invalid field name: {0}", field.FieldName);
                            break;
                        }
                    }

                    if (!failed)
                    {
                        // Get form action definitions
                        List<ActionDefinition> actionDefinitions = new List<ActionDefinition>();
                        ListDefinition definition = formItem.ActionsDefinition;
                        if (definition.Groups.Count > 0 && definition.Groups[0].ListItems.Count > 0)
                        {
                            foreach (GroupDefinition group in definition.Groups)
                            {
                                foreach (ListItemDefinition item in group.ListItems)
                                {
                                    actionDefinitions.Add(new ActionDefinition(item.ItemID, item.Parameters)
                                                              {
                                                                  UniqueKey = item.Unicid
                                                              });
                                }
                            }
                        }

                        //Execute form actions
                        foreach (ActionDefinition actionDefinition in actionDefinitions)
                        {
                            try
                            {
                                ActionItem action = ActionItem.GetAction(actionDefinition.ActionID);
                                if (action != null)
                                {
                                    if (action.ActionType == ActionType.Save)
                                    {
                                        object saveAction = ReflectionUtil.CreateObject(action.Assembly, action.Class,
                                                                                        new object[0]);
                                        ReflectionUtils.SetXmlProperties(saveAction, actionDefinition.Paramaters, true);
                                        ReflectionUtils.SetXmlProperties(saveAction, action.GlobalParameters, true);
                                        if (saveAction is ISaveAction)
                                        {
                                            ((ISaveAction) saveAction).Execute(formId, adaptedFields, null);
                                        }
                                    }
                                }
                            }
                            catch (Exception ex)
                            {
                                // log and bail out
                                Log.Warn(ex.Message, ex, this);
                                result.Success = false;
                                result.ResultMessage = actionDefinition.GetFailureMessage();
                                failed = true;

                                break;
                            }

                        }

                        if (!failed)
                        {
                            // set successful result
                            result.Success = true;
                            result.ResultMessage = formItem.SuccessMessage;
                        }
                    }
                }
                else
                {
                    result.Success = false;
                    result.ResultMessage = "Form not found: invalid form Id";
                }
            }

            return result;
        }

        private AdaptedControlResult GetControlResult(string fieldValue, FieldItem fieldItem)
        {
            //Populate fields with values
            ControlResult controlResult = new ControlResult(fieldItem.Name, HttpUtility.UrlDecode(fieldValue), string.Empty)
            {
                FieldID = fieldItem.ID.ToString(),
                FieldName = fieldItem.Name,
                Value = HttpUtility.UrlDecode(fieldValue),
                Parameters = string.Empty
            };
            return new AdaptedControlResult(controlResult, true);
        }
    }
Step 2. Create a WebApi endpoint for clients to post to.
Now that we have the basic setup, we can create a simple API controller:

    public class WffmController : ApiController
    {
        [HttpPost]
        public IHttpActionResult Post(FormData data)
        {
            FormProcessor processor = new FormProcessor();
            FormProcessorResult result = processor.Process(data);

            if (!result.Success)
            {
                return new BadRequestErrorMessageResult(result.ResultMessage, this);
            }

            return new OkResult(this);
        }
Step 3. Register routes

var config = GlobalConfiguration.Configuration;
            config.Routes.MapHttpRoute("DefaultApiRoute",
                                     "api/{controller}/{id}",
                                     new { id = RouteParameter.Optional });

Step 4. Use with any form anywhere.

            var formFields = new List<FormField>();
            formFields.Add(new FormField
            {
                FieldName = "First Name",
                FieldValue = data.FirstName
            });
            formFields.Add(new FormField
            {
                FieldName = "Last Name",
                FieldValue = data.LastName
            });
            formFields.Add(new FormField
            {
                FieldName = "Email",
                FieldValue = data.Email
            });

            FormData formData = new FormData();
            formData.FormId = MY_WFFM_FORM_ITEM_ID; // form item id from Sitecore
            formData.Fields = formFields;



            using (WebClient client = new WebClient())
            {
                client.UploadString(RemoteUrl, "POST", JsonConvert.SerializeObject(formData));
            }

Cons:
- certain WFFM out-of-the-box features will be lost - validation! validation! validation!
- once created, the form needs to remain fairly immutable since the form item ID and the field items are the contract with any client that will submit data.

I hope that someone would find this useful the next time they're faced with a similar problem.