Virtual Happy Hour this month, Jun 28, we'll be getting a sneak preview at our soon to launch SaaS CMS!

Try our conversational search powered by Generative AI!

Paul Gruffydd
Aug 8, 2019
(14 votes)

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
        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)]
    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.

    Inherited = true,
    TemplateTypeCategory = TemplateTypeCategories.MvcController, //Required as controllers for blocks are registered as MvcPartialController by default
    Tags = new[] { RenderingTags.Preview, RenderingTags.Edit },
    AvailableWithoutTag = false)]
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
        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>
[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.

        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:

Aug 08, 2019


Bartosz Sekula
Bartosz Sekula Aug 8, 2019 06:26 PM

Excellent stuff Paul!

Darren Stahlhut
Darren Stahlhut Aug 9, 2019 09:05 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
Ravindra S. Rathore Aug 9, 2019 01:56 PM

Nice article!!

Paul Gruffydd
Paul Gruffydd Aug 9, 2019 02:07 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)


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
Quan Tran Aug 19, 2019 03:32 AM

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

Please login to comment.
Latest blogs
Remove a segment from the URL in CMS 12

Problem : I have created thousands of pages dynamically using schedule jobs with different templates (e.g. one column, two columns, etc..) and stor...

Sanjay Kumar | Jun 21, 2024

Copying property values part 2

After publishing my last article about copying property values to other language versions, I received constructive feedback on how could I change t...

Grzegorz Wiecheć | Jun 18, 2024 | Syndicated blog

Enhancing online shopping through Optimizely's personalized product recommendations

In this blog, I have summarized my experience of using and learning product recommendation feature of Optimizely Personalization Artificial...

Hetaxi | Jun 18, 2024

New Series: Building a .NET Core headless site on Optimizely Graph and SaaS CMS

Welcome to this new multi-post series where you can follow along as I indulge in yet another crazy experiment: Can we make our beloved Alloy site r...

Allan Thraen | Jun 14, 2024 | Syndicated blog