Identifying visitor in Web forms for marketers without login in

As you might know, out of the box, it is not possible to just identify a contact with Web forms for marketers (WFFM) module in Sitecore without either creating or login into an account. This makes it troublesome if you for example just want to have something simple like a newsletter subscription form with just an email field. Why create an account for that, right?

So what to do then? We create our own Save Action to do this! And this is what I’ll present in this post.

Save action editing

So where do we start? Well first of, what do I want? I want this Save Action to be dynamic, so nothing hardcoded. I need to be able to select what field in the form that I want to use a an identifier when identifying the current visitor. So I started looking at the existing Save Action and found that the Register Campaign hade an editor with a tree to select a campaign. Nice, this I can use. So I copied it’s editor xml control \sitecore\shell\Applications\Modules\Web Forms for Marketers\Dialogs\Action Editor\CampaignEditor.xml and named it IdentifyContact.xml and created a new codebesid file to be able to set the correct root item and also to pass the selected field back to the save action with the key FieldId.

<?xml version="1.0" encoding="utf-8" ?>
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense"  xmlns:content="http://www.sitecore.net/content">
  <IdentifyContact.Editor>

    <FormDialog ID="Dialog" Icon="Business/32x32/money2.png" >

      <Stylesheet Src="FormBuilder.css" DeviceDependant="true"/>

      <CodeBeside Type="Namespace.IdentifyContactEditor,Assembly"/>

      <DataContext ID="ItemDataContext" DataViewName="Master" Database="master" ShowRoot="true" />

      <GridPanel Columns="1" CellPadding="4" Width="100%" Height="100%" Style="table-layout:fixed">
        <Border Width="100%" Height="100%">
          <Literal ID="SelectCampaignEventLiteral" />
          <Scrollbox Width="100%" Height="460px" Background="white" Border="1px inset" Margin="0px 0px 10px 0px">
            <DataTreeview ID="ItemLister" MultiSelect="false" DataContext="ItemDataContext" AllowDragging="false"  />
          </Scrollbox>
        </Border>
      </GridPanel>

    </FormDialog>
  </IdentifyContact.Editor>
</control>
public class IdentifyContactEditor : DialogForm
{
    public static readonly string FieldIdKey = "FieldId";
    private readonly IResourceManager resourceManager;
    protected DataTreeview ItemLister;
    protected DataContext ItemDataContext;
    protected Literal SelectCampaignEventLiteral;
    protected XmlControl Dialog;
    private NameValueCollection nvParams;
    public Database CurrentDatabase
    {
        get
        {
            return Factory.GetDatabase(Sitecore.Web.WebUtil.GetQueryString("db"));
        }
    }

    public string CurrentID
    {
        get
        {
            return Sitecore.Web.WebUtil.GetQueryString("id");
        }
    }
    public FormItem FormItem
    {
        get
        {
            return new FormItem(this.CurrentDatabase.GetItem(this.CurrentID, this.CurrentLanguage));
        }
    }

    public Language CurrentLanguage
    {
        get
        {
            return Language.Parse(Sitecore.Web.WebUtil.GetQueryString("la"));
        }
    }
    public string Params
    {
        get
        {
            return HttpContext.Current.Session[Sitecore.Web.WebUtil.GetQueryString("params")] as string;
        }
    }
    public string FieldIdValue
    {
        get
        {
            return this.GetValueByKey(IdentifyContactEditor.FieldIdKey);
        }
        set
        {
            this.SetValue(IdentifyContactEditor.FieldIdKey, value);
        }
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        if (Sitecore.Context.ClientPage.IsEvent) return;

        this.Localize();
        this.ItemDataContext.Root = FormItem.ID.ToString();

        if (!string.IsNullOrEmpty(FieldIdValue))
            this.ItemDataContext.DefaultItem = FieldIdValue;            
    }

    protected virtual void Localize()
    {
        this.Dialog["Header"] = (object)"Identify contact";
        this.Dialog["Text"] = (object)"Configure what field data should be used when identifying the current visitor session";
        this.SelectCampaignEventLiteral.Text = "Select forms field to used when identifying:";
    }


    protected override void OnOK(object sender, EventArgs args)
    {
        var selectedItem = this.ItemLister.GetSelectionItem();
        if(selectedItem != null)
            this.SetValue(IdentifyContactEditor.FieldIdKey, selectedItem.ID.ToString());

        string str1 = ParametersUtil.NameValueCollectionToXml(this.nvParams ?? new NameValueCollection());
        if (str1.Length == 0)
            str1 = "-";
        SheerResponse.SetDialogValue(str1);
        base.OnOK(sender, args);
    }
        
    private void SetValue(string key, string value)
    {
        if (this.nvParams == null)
            this.nvParams = StringUtil.GetNameValues(this.Params, '=', '&');
        this.nvParams[key] = value;
    }

    private string GetValueByKey(string key)
    {
        if (this.nvParams == null)
            this.nvParams = ParametersUtil.XmlToNameValueCollection(this.Params);
        return this.nvParams[key] ?? string.Empty;
    }
}

After that I created a Save Action item at the WFFM Save Action folder in Sitecore and pointed out what Editor I wanted by entering control:IdentifyContact.Editor in the Editor field.

Great now I have a way to add the Save action and select what field I want to use to identify the visitor with like this:

Save action execution

Next thing is to have this Save Action do something when a form is submitted. This is kind of straight forward. I created a class IdentifyContact which inherits Sitecore.WFFM.Actions.Base.WffmSaveAction that looks like this

public class IdentifyContact : Sitecore.WFFM.Actions.Base.WffmSaveAction
{
    public string FieldId { get; set; }
        

    public override void Execute(ID formId, AdaptedResultList adaptedFields, ActionCallContext actionCallContext = null, params object[] data)
    {
        if (!Tracker.IsActive)
            Tracker.StartTracking();

        if (string.IsNullOrEmpty(FieldId)) return;
        if (!ID.IsID(FieldId)) return;
            
        var field = adaptedFields.FirstOrDefault(x => x.FieldID.Equals(FieldId, StringComparison.CurrentCultureIgnoreCase));
        Assert.ArgumentNotNull(field, "field");

        var identifier = field.Value;
        if(IsEmail(identifier))
        {
            var userName = Membership.GetUserNameByEmail(identifier);
            identifier = string.IsNullOrEmpty(userName) ? identifier : userName;
        }

        Tracker.Current.Session.Identify(identifier);
    }

    private bool IsEmail(string value)
    {
        return Regex.IsMatch(value, @"\A(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\Z", RegexOptions.IgnoreCase);
    }
}

Here the selected field id is loaded automatically into the property FieldId (done by WFFM module, it’s like magic) which I used to find the field with entered value in the adaptedFields list. For good measure if the selected field value is an email I check if there is a user with that email and if so we used the username to identify and if not we used the value entered in the field to identify the current visitor.

Next step is to add this class to the Save Action item by entering the Namespace and Assembly to the Assembly and Class fields. Also do not forget to check the checkbox Client Action since we want this action to be performed on the Content Delivery server and not the Content Management server.

This was all developed in Sitecore XP 8.2 Update-1 using Web forms for marketers 8.2 Update-1