Views: 1026
Number of votes: 13
Average rating:

Templating emails from Episerver Forms

This may sound obvious but maintaining brand consistency across channels is pretty important yet, all too often, transactional emails seem to fall through the cracks. Am I alone in feeling slightly disappointed when I fill in a form on a website only to receive an unbranded, generic, text-only “thanks for filling in our form” email which could have come from anyone?

A fairly comprehensive solution to this problem (and several others) is to invest in marketing automation technology such as Episerver Campaign and an increasing number of companies are going down that route but there are still many situations where that kind of approach is overkill. In some circumstances it’s sufficient to just use Episerver Forms, particularly given that, should your requirements grow to include marketing automation, you can extend the functionality of Forms using one of the ready-built connectors.

A big advantage of Episerver Forms over its predecessor (XForms 😱) is that Forms includes a mechanism out of the box to allow you to send different emails with editor controllable content to multiple email addresses (including the person filling in the form), functionality which we used to have to write ourselves in XForms. The downside of these emails is that you only get a WYSIWYG editor to manage the full content of the email which makes it difficult to produce fully branded emails and, as each email is created individually, this increases the burden on editors when it comes to managing essential shared text such as disclaimers which are required across all emails. If we could apply a template to those emails, wrapping the editable content, then we alleviate both of those issues.

Within Forms, the processing of submitted form data is handled using a concept called actors and, as developers, we’re free to create as many actors as we like. Given that we already have an actor which does most of what we need it feels unnecessary to write a whole new actor from scratch, so I’d like to take the existing SendEmailAfterSubmissionActor as a base for my templated version and add in the functionality to allow the editor to choose from a list of available templates for the email.

Let’s tackle the easy bit first – managing the email templates. We could upload these as files in our solution the same way as we do for views but I can envisage many situations where we’d need to make minor tweaks to the templates which couldn’t really wait for a deployment cycle so I’m going to manage the email templates as content and, as they won’t be accessed directly via a URL, it makes sense to use a block like this.

[ContentType(DisplayName = "Email Template", GUID = "03daf669-5884-47f2-840f-04056afc9536", Description = "Content type for adding email templates")]
public class EmailTemplateBlock : BlockData
{
    [CultureSpecific]
    [Display(
        Name = "Content",
        Description = "The HTML/Text content of the email, use #CONTENT# to represent the editable section of the email",
        GroupName = SystemTabNames.Content,
        Order = 20)]
    [UIHint(UIHint.Textarea)]
    public virtual string Content { get; set; }
}

The slight downside of this approach is that blocks are usually previewed in situ within a page so we need to give our EmailTemplateBlock its own preview controller to allow us to preview the email templates without the site header & footer wrapped around them.

[TemplateDescriptor(
    Inherited = true,
    TemplateTypeCategory = TemplateTypeCategories.MvcController, //Required as controllers for blocks are registered as MvcPartialController by default
    Tags = new[] { RenderingTags.Preview, RenderingTags.Edit },
    AvailableWithoutTag = false)]
[VisitorGroupImpersonation]
[RequireClientResources]
public class EmailTemplatePreviewController : ActionControllerBase, IRenderTemplate<EmailTemplateBlock>
{
    public EmailTemplatePreviewController()
    {
    }

    public ActionResult Index(EmailTemplateBlock currentContent)
    {
        // Display the template with some dummy content
        return Content((currentContent.Content ?? string.Empty).Replace("#CONTENT#", "<p><strong>Lorem ipsum dolor sit amet</strong>,<br/> consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>"));
    }
}

Next, we need to modify the UI for the SendEmailAfterSubmissionActor to allow the user to pick a template along with the rest of the details. This is done in a similar way to code-first content types in that we create a class and decorate its properties to say how we’d like them displayed. To avoid reinventing the wheel, I’m going to inherit from the model used by the default SendEmailAfterSubmissionActor and just add my additional “EmailTemplateId” field which uses a selection factory to show as a dropdown.

public class TemplatableEmailTemplateActorModel : EmailTemplateActorModel
{
    [Display(
        Name = "Template",
        Order = 9999)]
    [SelectOne(SelectionFactoryType = typeof(EmailTemplateSelectionFactory))]
    public virtual int EmailTemplateId { get; set; }
}

public class EmailTemplateSelectionFactory : ISelectionFactory
{
    public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
    {
        yield return new SelectItem { Text = "None", Value = 0 };

        var contentModelUsage = ServiceLocator.Current.GetInstance<IContentModelUsage>();
        var contentTypeRepo = ServiceLocator.Current.GetInstance<IContentTypeRepository>();
        var templateType = contentTypeRepo.Load("EmailTemplateBlock");

        var content = contentModelUsage.ListContentOfContentType(templateType) //Get all instances of EmailTemplateBlock
            .GroupBy(x => x.ContentLink.ID).Select(x => x.First()) //Remove duplicates
            .OrderBy(x => x.Name); //Order alphabetically by name

        foreach (var item in content)
        {
            yield return new SelectItem { Text = item.Name, Value = item.ContentLink.ID };
        }
    }
}

Finally, we put it all together by creating our overridden version of the SendEmailAfterSubmissionActor, using our custom model as shown above. In our actor we override the “run” method and use it to modify the model to grab the selected template and wrap it around the email content before putting it back into the email body property of the model and letting the base SendEmailAfterSubmissionActor process the emails as it normally would.

public class CustomSendEmailAfterSubmissionActor : SendEmailAfterSubmissionActor
{
    private Injected<IContentLoader> _contentLoader;
    private string _contentPlaceholder = "#CONTENT#";

    public override Type PropertyType => typeof(PropertyTemplatableEmailActor);

    public override object Run(object input)
    {
        var emailList = Model as IEnumerable<TemplatableEmailTemplateActorModel>;
        foreach (var email in emailList)
        {
            // Load the template in the correct language, allowing for fallbacks
            var languageLoaderOptions = new LoaderOptions();
            languageLoaderOptions.Add(new LanguageLoaderOption { FallbackBehaviour = LanguageBehaviour.FallbackWithMaster, Language = new System.Globalization.CultureInfo((input as EPiServer.Forms.Core.Models.FormIdentity).Language) });
            if (email.EmailTemplateId > 0 && _contentLoader.Service.TryGet(new ContentReference(email.EmailTemplateId), languageLoaderOptions, out EmailTemplateBlock emailTemplateBlock) && email.Body != null)
                {
                email.Body = new XhtmlString(emailTemplateBlock.Content.Replace(_contentPlaceholder, email.Body.ToHtmlString()));
            }
        }
        return base.Run(input);
    }
}

/// <summary>
/// Property definition for the Actor
/// </summary>
[EditorHint("TemplatableEmailActorPropertyHint")]
[PropertyDefinitionTypePlugIn(DisplayName = "TemplatableEmail")]
public class PropertyTemplatableEmailActor : PropertyGenericList<TemplatableEmailTemplateActorModel> { }


/// <summary>
/// Editor descriptor class, for using Dojo widget CollectionEditor to render.
/// Inherit from <see cref="CollectionEditorDescriptor"/>, it will be rendered as a grid UI.
/// </summary>
[EditorDescriptorRegistration(TargetType = typeof(IEnumerable<TemplatableEmailTemplateActorModel>), UIHint = "TemplatableEmailActorPropertyHint")]
public class ConfigurableActorEditorDescriptor : CollectionEditorDescriptor<TemplatableEmailTemplateActorModel>
{
    public ConfigurableActorEditorDescriptor()
    {
        // N.B. This is a special ClientEditingClass just for the EmailTemplateActorModel.
        // Using the expected value of "epi-forms/contentediting/editors/CollectionEditor" will display incorrectly
        ClientEditingClass = "epi-forms/contentediting/editors/EmailTemplateActorEditor";
    }
}

At this point our custom actor will display in addition to the inbuilt one so we’ll want to hide the inbuilt one. We can do this using an EditorDescriptor to override metadata.ShowForEdit as shown below however, before you do this, make sure you've cleared out any existing email responses which have been set up in the inbuilt actor as, once it's hidden, we won't be able to modify any existing emails.

[EditorDescriptorRegistration(
        TargetType = typeof(IEnumerable<EmailTemplateActorModel>),
        UIHint = "EmailTemplateActorEditor",
        EditorDescriptorBehavior = EditorDescriptorBehavior.OverrideDefault)]
public class EmailTemplateActorEditorDescriptor : CollectionEditorDescriptor<EmailTemplateActorModel>
{
    public EmailTemplateActorEditorDescriptor()
    {
        //ClientEditingClass = "epi-forms/contentediting/editors/EmailTemplateActorEditor";
    }
    public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
    {
        metadata.ShowForEdit = false;
    }
}

And there we have it, templated emails sent through Episerver Forms.

As always, I’ve created this as a proof-of-concept rather than battle-hardened, production-ready code so use with caution. The full source can be found on GitHub here:
https://gist.github.com/PaulGruffyddAmaze/6ce76f3c6ffb78f0b568c3a2f0998c06

Aug 08, 2019

Bartosz Sekula
(By Bartosz Sekula, 8/8/2019 8:26:12 PM)

Excellent stuff Paul!

Darren S
(By Darren S, 8/9/2019 11:05:58 AM)

That's awesome Paul. Being Blocks, do you think having an Email Template in different languages would require be difficult to achieve? I'll grab your code and have a play. 

Ravindra S. Rathore
(By Ravindra S. Rathore, 8/9/2019 3:56:34 PM)

Nice article!!

Paul Gruffydd
(By Paul Gruffydd, 8/9/2019 4:07:16 PM)

Thanks for the comments.

@Darren - Languages on the email template block shouldn't be an issue as you can translate blocks in the same way as any other localisable content but you would have an issue with actors in Episerver Forms. By default they're created as not culture-specific so, in the case of the email actor, this means you can't create individual emails for individual locales. You can change that manually under "content types" in the admin section of Episerver or, as I've done in the past, you could create an initializable module to do that for you (takes out that element of human error when moving between environments). Because of that limitation I didn't make any special allowances for languages but I might make a sneaky edit to allow for the selection of a template of the appropriate language. The change required would be to swap out the content loading from:

_contentLoader.Service.TryGet(new ContentReference(email.EmailTemplateId), out EmailTemplateBlock emailTemplateBlock)

to:

var languageLoaderOptions = new LoaderOptions();
languageLoaderOptions.Add(new LanguageLoaderOption { FallbackBehaviour = LanguageBehaviour.FallbackWithMaster, Language = new System.Globalization.CultureInfo((input as EPiServer.Forms.Core.Models.FormIdentity).Language) });
_contentLoader.Service.TryGet(new ContentReference(email.EmailTemplateId), languageLoaderOptions, out EmailTemplateBlock emailTemplateBlock)

Quan Tran
(By Quan Tran, 8/19/2019 5:32:25 AM)

This is great. I have to login to up vote.

Please login to comment.