Save Actions in Sitecore WFFM are a great way of isolating single units of work and letting each unit do just what it’s meant for and nothing more than that. Huge processes can be split up in small tasks which are executed synchronously in the order they are configured.
For this blog post, we assume we have a registration flow where a website visitor wants to create a user account. On form submission, we configure 2 custom save actions.
- Create User Account
- Send Mail
This is a perfectly valid scenario of having two separate save actions, because the Send Mail action can be built in a generic way and reused across multiple forms. After the user account is created, we send an e-mail to the website visitor which in this case might be a confirmation mail. But what if the first save action fails for some reason? Either some unexpected error (e.g. database connection lost) or maybe even an exception that we throw ourselves. The process is then in a faulted state and should be terminated. One would say this is the logical consequence and should be handled by WFFM.
Unfortunately, it’s not. WFFM does handle faulted state in form verification actions, but not in save actions. A while ago, I have filed a support ticket (#441302) for this incorrect behavior and the support team has registered it as a bug in Sitecore WFFM 8.0 rev. 150224. That was when I used Sitecore 8.0 Update 2. Now, with Sitecore 8.1 Update 2, still the same version of WFFM is used and that means the bug is still present.
In order to work-around this issue, we need to add validation in our custom save actions (can’t do in out-of-the-box ones unfortunately). So let’s create a class called SaveActionFaultedStateWorkaround.
namespace Exlrt.Forms.Utilities
{
using Sitecore.Data;
using System;
public sealed class SaveActionFaultedStateWorkaround
{
private const string HasFaultedStateKey = "HasFaultedState";
public static void ExecuteIfNotInFaultedState(ID formId, Action action)
{
if (action != null && ShouldExecute(formId))
{
try
{
action();
}
catch
{
RegisterFaultedState(formId);
throw;
}
}
}
private static void RegisterFaultedState(ID formId)
{
Sitecore.Context.Items[GetKey(formId)] = true;
}
private static bool ShouldExecute(ID formId)
{
object value = Sitecore.Context.Items[GetKey(formId)];
return (value == null || Convert.ToBoolean(value) == false);
}
private static string GetKey(ID formId)
{
return $"{HasFaultedStateKey}_{formId}";
}
}
}
Each custom save action should make use of this class and if it fails, register the faulted state for the current form and ensure that all imminent save actions configured for this form will not execute.
namespace Exlrt.Forms.Actions
{
using Sitecore.Data;
using Sitecore.Form.Core.Client.Data.Submit;
using Sitecore.Form.Submit;
using System;
using Utilities;
public class FailingAction : ISaveAction
{
publicvoidExecute(ID formid, AdaptedResultList fields, paramsobject[] data)
{
SaveActionFaultedStateWorkaround.ExecuteIfNotInFaultedState(formid, () =>
{
thrownewException("This save action is about to fail.");
});
}
}
}
namespace Exlrt.Forms.Actions
{
using Sitecore.Data;
using Sitecore.Diagnostics;
using Sitecore.Form.Core.Client.Data.Submit;
using Sitecore.Form.Submit;
using Utilities;
public class FinalAction : ISaveAction
{
public void Execute(ID formid, AdaptedResultList fields, paramsobject[] data)
{
SaveActionFaultedStateWorkaround.ExecuteIfNotInFaultedState(formid, () =>
{
Log.Info("Final save action executed.", this);
});
}
}
}
Of course, the FinalAction save action will be instantiated, but at least we make sure that it’s real logic is not executed.
Very simple yet effective.
Update March 24th:
Apparently, my SIM 1.4 didn’t offer me the latest Sitecore WFFM package (Web Forms for Marketers 8.1 rev. 160304). In this latest version, the above mentioned bug has been resolved. This work-around is thus not necessary anymore, but might come handy for those who cannot upgrade! Unhappy to see that other very old bugs still aren’t resolved like getting a password field by its name. Still returning null and returns only something if the fields TITLE is passed. This of course eliminates multi-language password fields…