Messaging, Channels, Rules and Tokens - Part 2

<-- Read Part 1

 

In this part, we will be implementing the following:

  • Two IParserEngine classes: SimpleTextParserEngine and RazorParserEngine;
  • Site scope settings to configure which parser to use by default;
  • Content type/part settings to allow the user to override the site level settings abd configure which parser to use on a per content type level basis;
  • Improve the template editing experience with CodeMirror.

Let's start with the template parsers. 

ParserBaseEngine

To make implementing parsers as easy as possible, we will define a parser base class that will provide a default implementation of the Id and DisplayText properties.

Parsers/ParserEngineBase.cs:

using Orchard;
using Skywalker.Messaging.Models;
using Skywalker.Messaging.Services;
 
namespace Skywalker.Messaging.Parsers {
 public abstract class ParserEngineBase : ComponentIParserEngine {
  public virtual string Id {
   get { return GetType().FullName; }
  }
 
  public virtual string DisplayText {
   get { return GetType().Name; }
  }
 
  public abstract string LayoutBeacon { get; }
  public abstract string ParseTemplate(MessageTemplatePart templateParseTemplateContext context);
 }
}

 

 

SimpleTextParserEngine

The SimpleTextParserEngine will implement a simple text replacing mechanism. Strings surrounded with square brackets will be replaced with a value that we receive via the ViewBag property.

So, if the ViewBag is of type IDictionary<string, string> and contains a key "CustomerName", any occurrence of [CustomerName] in the template will be replaced with the value from the ViewBag.

Let's see how that works:

Parsers/SimpleTextParserEngine.cs:

using System.Collections.Generic;
using System.Linq;
using System.Text;
using Orchard.Environment.Extensions;
using Skywalker.Messaging.Models;
 
namespace Skywalker.Messaging.Parsers {
 [OrchardFeature("Skywalker.Messaging")]
 public class SimpleTextParserEngine : ParserEngineBase {
  public override string DisplayText {
   get { return "Simple Text Parser"; }
  }
 
  public override string LayoutBeacon {
   get { return "[Body]"; }
  }
 
  public override string ParseTemplate(MessageTemplatePart templateParseTemplateContext context) {
   var layout = template.Layout;
   var templateContent = new StringBuilder(template.Text);
   var viewBag = context.ViewBag;
 
   // If the template specified a Layout, proccess that layout by replacing its "LayoutBeacon" with the text of the template being processed
   if (layout != null) {
    templateContent = new StringBuilder(layout.Text.Replace(LayoutBeacontemplateContent.ToString()));
   }
 
   // If we have a view bag and it is a dictionary of strings, proces the template by replacing each [key] with the value from the view bag dictionary.
   if (viewBag != null) {
    var variables = viewBag as IEnumerable<KeyValuePair<stringstring>>;
    if (variables != null) {
     templateContent = variables.Aggregate(templateContent, (currentvariable=> current.Replace(string.Format("[{0}]"variable.Key), variable.Value));
    }
   }
 
   return templateContent.ToString();
  }
 }
}

 

 

Essentially we are checking if the specified template has a Layout specified. If it has one, we replace its "layout beacon" value with the contents of the specified layout. After that, we will simply iterate over the ViewBag property (if specified and if it's a dictionary of strings). In this example, we are leveraging Linq's Aggregate method instead of writing a foreach loop ourselves.

 

RazorParserEngine

The SimpleTextParserEngine is here primarily for demo purposes. What would be really interesting is if we could create our email templates with Razor syntax (or Smarty Tags or any templating engine that you prefer).

There are a couple of approaches that we can take. One approach would be to use Orchard's Shape mechanism to process Razor views. Another approach would be to talk to the Razor engine directly without relying on System.Web classes. I chose the latter.

I found two libraries that wrap the Razor engine: https://github.com/Antaris/RazorEngine and https://github.com/jlamfers/RazorMachine. I used to use RazorEngine, but found that it did not yet support Razor 2, and there were some issues when using Layouts. RazorMachine on the other hand already supports Razor 2, implements Layouts and is easy to use. So let's go with that.

The first thing we will want to do is create a RazorMachine wrapper class and give it a lifetime scope of Singleton. The reason for this is that initializing the Razor engine is expensive in terms of initialization time. Alternatively we could leverage the Cache to hold a single instance of RazorMachine, but that's up to you.

Once you have downloaded the RazorMachine binaries (just a single Xipton.Razor.dll file), copy it to a new folder in your module called Libs/RazorMachine and add a reference to that DLL.

In our module, Razor parsing will be an optional feature, so let's define a new feature called "Skywalker.Messaging.Parser.Razor" in the module manifest:

Module.txt:

Name: Skywalker Messaging
AntiForgery: enabled
Author: Skywalker Software Development
Website: http://skywalkersoftwaredevelopment.net
Version: 1.0
OrchardVersion: 1.6
Description: Message Template Management features.
Features:
    Skywalker.Messaging:
        Name: Skywalker Messaging
        Description: Message Template Management features.
        Category: Messaging
        Dependencies: Orchard.Messaging, Orchard.Tokens
    Skywalker.Messaging.Parsers.Razor:
        Name: Skywalker Razor Parser
        Description: Implements Razor as the parser engine for message templates.
        Category: Parsers
        Dependencies: Skywalker.Messaging

 

 

 

Next, create a new interface called IRazorMachine and a class called RazorMachineWrapper that implements the interface:

Parsers/RazorMachine.cs:

using Orchard;
using Orchard.Environment.Extensions;
using Xipton.Razor;
 
namespace Skywalker.Messaging.Parsers {
    public interface IRazorMachine : ISingletonDependency {
        ITemplate ExecuteContent(string templateContentobject model = nullobject viewBag = null);
        void RegisterLayout(string virtualPathstring templateContent);
    }
 
    [OrchardFeature("Skywalker.Messaging.Parsers.Razor")]
    public class RazorMachineWrapper : IRazorMachine {
        private readonly RazorMachine _razorMachine;
 
        public RazorMachineWrapper() {
            _razorMachine = new RazorMachine();
        }
 
        public ITemplate ExecuteContent(string templateContentobject model = nullobject viewBag = null) {
            return _razorMachine.ExecuteContent(templateContentmodelviewBag);
        }
 
        public void RegisterLayout(string virtualPathstring templateContent) {
            _razorMachine.RegisterTemplate(virtualPathtemplateContent);
        }
    }
}

 

When you compile the code now, you'll get a compilation error stating that you need to add a referece to System.Web.Razor. You will find this assembly in the lib folder that comes with the Orchard Source package.

Notice that the interface derives from ISingleton. As mentioned, this will cause the lifetime scope of an instance of IRazorMachine to be that of a singleton.

The class is a simple wrapper around the Xipton.Razor.RazorMachine class. Notice that we decorated the class with the new feature we introduced: "Skywalker.Messaging.Parser.Razor".

Next, lets create the actual RazorParserEngine:

Parsers/RazorParserEngine.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Orchard.Environment.Extensions;
using Orchard.Logging;
using Skywalker.Messaging.Models;
using Xipton.Razor.Core;
 
namespace Skywalker.Messaging.Parsers {
    [OrchardFeature("Skywalker.Messaging.Parsers.Razor")]
    public class RazorParserEngine : ParserEngineBase {
        private readonly IRazorMachine _razorMachine;
 
        public RazorParserEngine(IRazorMachine razorMachine) {
            _razorMachine = razorMachine;
        }
 
        public override string DisplayText {
            get { return "Razor Engine"; }
        }
 
        public override string LayoutBeacon {
            get { return "@RenderBody()"; }
        }
 
        public override string ParseTemplate(MessageTemplatePart templateParseTemplateContext context) {
            var layout = template.Layout;
            var templateContent = template.Text;
            var viewBag = context.ViewBag;
 
            if (layout != null) {
                _razorMachine.RegisterLayout("~/shared/_layout.cshtml"layout.Text);
                templateContent = "@{ Layout = \"_layout\"; }\r\n" + templateContent;
            }
 
            try {
                // Convert viewBag to string/object pairs if required, because that's what Razor expects for a ViewBag.
                if (viewBag != null) {
                    if (viewBag is IEnumerable<KeyValuePair<stringstring>>)
                        viewBag = ((IEnumerable<KeyValuePair<stringstring>>viewBag)
                            .ToDictionary(x => x.Keyx => (object)x.Value);
                }
                var tmpl = _razorMachine.ExecuteContent(templateContentcontext.ModelviewBag);
                return tmpl.Result;
            }
            catch (TemplateCompileException ex) {
                if(layout != null)
                    Logger.Log(LogLevel.Errorex"Failed to parse the {0} Razor template with layout {1}"template.Titlelayout.Title);
                else
                    Logger.Log(LogLevel.Errorex"Failed to parse the {0} Razor template"template.Title);
                return BuildErrorContent(extemplatelayout);
            }   
        }
 
        private static string BuildErrorContent(Exception exMessageTemplatePart templatePartMessageTemplatePart layout) {
            var sb = new StringBuilder();
            var currentException = ex;
 
            while (currentException != null) {
                sb.AppendLine(currentException.Message);
                currentException = currentException.InnerException;
            }
 
            sb.AppendFormat("\r\nTemplate ({0}):\r\n"templatePart.Title);
            sb.AppendLine(templatePart.Text);
 
            if (layout != null) {
                sb.AppendFormat("\r\nLayout ({0}):\r\n"layout.Title);
                sb.AppendLine(layout.Text);
            }
            return sb.ToString();
        }
    }
}

 

Pretty straightforward. We're injecting an IRazorMachine, use it to register a Layout (if the specified template is setup with one), convert the view bag to a dictionary of objects (since that's the required type for a ViewBag in Razor views), execute the template, and returning the resulting string. Et voila, we have a Razor parser.

You may have noticed that we didn't use the LayoutBeacon property here, since the Razor engine itself will know how to deal with that. Couldn't we have done without an explcitly defined LayoutBeacon property at all? Yes we could have, but there's another reason that we need this beacon, which we will see when we implement CodeMirror and the preview pane.

 

Site Scope Settings & Content Type/Part Settings

Allright, we created two parsers, but which one will be used? Well it depends on what the user wants to use. We could let the user decide which engine to use using just Site Scope Settings, but for this demo we will not only implement Site Scope Settings, but Content Type/Part scope settings as well.

Content Type/Part scope settings are settings you configure in the Content Type editor. For example, the AutoroutePart implements Type/Part settings:

 

Site Scope Settings are the settings you find via the admin menu on the left under the Settings section:

 

Let's start with implementing Site Scope Settings first.

 

Site Scope Settings

Implementing site scope settings requires a couple of steps and there are two approaches: one approach is the "do-it-all-your-self" approach, which requires one more step but also offers more flexibility, and then there's the "do-it-all-your-self-except-for-the-driver" approach.

The latter approach is made possible thanks to a content filter called TemplateFilterForRecord. It looks very much like a driver, so you don't have to create a driver yourself. It uses a record as the model instead of a part. Using this method is nice because it saves you from having to write your own driver. However, if you have for example a view model that you want to use, you should implement a driver instead. Which is exactly what we will be doing here. If you're interested in the TemplateFilterForRecord, checkout the Orchard.Users module and its RegistrationSettingsPartHandler.

For our custom site settings, we'll follow these steps:

  1. Implement a content part and a record;
  2. Implement a driver;
  3. Implement an editor template;
  4. Attach the content part to the Site content type using the ActivatingFilter;
  5. Specify which "group" our settings belong to. When we specify a group, Orchard will render our settings on our own sub menu underneath the "Settings" section.

 

MessageSiteSettingsPart

This will be a very simple part, as it will only store one single setting for now: which parser to use by default.

Models/MessageSiteSettingsPart.cs:

using Orchard.ContentManagement;
using Orchard.ContentManagement.Records;
 
namespace Skywalker.Messaging.Models {
    public class MessagingSiteSettingsPart : ContentPart<MessagingSiteSettingsPartRecord> {
        public string DefaultParserId {
            get { return Record.DefaultParserId; }
            set { Record.DefaultParserId = value; }
        }
    }
 
    public class MessagingSiteSettingsPartRecord : ContentPartRecord {
        public virtual string DefaultParserId { getset; }
    }
}

 

Migrations

We will add a migration step to create a table for the MessagingSiteSettingsRecord class.

Normally when you implement a content part, you typically define this part in your migration file as well. In the case of site settings, we don;t have to, as we will be welding on this part dynamically using the ActivatingFilter. No need for this part to appear in the dashboard nor be attachable nor create a relationship in the database between our part and the Site content type.

Migrations.cs:

public int UpdateFrom1() {
    // Create a table for the MessagingSiteSettingsPartRecord class
    SchemaBuilder.CreateTable("MessagingSiteSettingsPartRecord"table => table
            .ContentPartRecord()
            .Column<string>("DefaultParserId"));
 
    return 2;
}

 

 

MessageSiteSettingsPartDriver

As mentioned, this step is only required if we don't make use of the TemplateFilterForRecord. In this demo we will create our own driver so we can use a custom view model for our editor template.

Drivers/MessageSiteSettingsPartDriver.cs:

using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.ContentManagement.Handlers;
using Orchard.Environment.Extensions;
using Skywalker.Messaging.Models;
using Skywalker.Messaging.ViewModels;
 
namespace Skywalker.Messaging.Drivers {
    [OrchardFeature("Skywalker.Messaging")]
    public class MessagingSiteSettingsPartDriver : ContentPartDriver<MessagingSiteSettingsPart> {
 
        /// <summary>
        /// Always implement Prefix to avoid potential model binding naming collisions when another part uses the same property names.
        /// </summary>
        protected override string Prefix {
            get { return "MessagingSiteSettings"; }
        }
 
        protected override DriverResult Editor(MessagingSiteSettingsPart partdynamic shapeHelper) {
            return Editor(partnullshapeHelper);
        }
 
        protected override DriverResult Editor(MessagingSiteSettingsPart partIUpdateModel updaterdynamic shapeHelper) {
            var viewModel = new MessageTemplateSettingsViewModel();
 
            if (updater != null) {
                // We are in "postback" mode, so update our part
                if (updater.TryUpdateModel(viewModelPrefixnullnull)) {
                    part.DefaultParserId = viewModel.ParserId;
                }
            }
            else {
                // We are in render mode (not postback), so initialize our view model.
                viewModel.ParserId = part.DefaultParserId;
            }
 
            // Return the EditorTemplate shape, configured with proper values.
            return ContentShape("Parts_MessagingSiteSettings_Edit", () => 
                shapeHelper.EditorTemplate(TemplateName"Parts/MessagingSiteSettings"ModelviewModelPrefixPrefix))
 
                // Assign our editor to the "Messaging" group
                .OnGroup("Messaging");
        }
 
        protected override void Importing(MessagingSiteSettingsPart partImportContentContext context) {
            context.ImportAttribute(part.PartDefinition.Name"DefaultParserEngine"x => part.DefaultParserId = x);
        }
 
        protected override void Exporting(MessagingSiteSettingsPart partExportContentContext context) {
            context.Element(part.PartDefinition.Name).SetAttributeValue("DefaultParserEngine"part.DefaultParserId);
        }
    }
}

 

 

Notice how we are using the OnGroup method? This is key A in order for our settings editor to be rendered underneath a section called "Messaging".

 

MessageTemplateSettingsViewModel

We will create a view model for our editor template. Technically, we could have used the content part, but we will making use of the UIHintAttribute again to render a list of available parsers, and since that's more of a UI concern, I prefer to create a view model for it instead of decorating the content part itself with that attribute. But you should feel free to do it differently.

 

ViewModels/MessageTemplateSettingsViewModel.cs:

using System.ComponentModel.DataAnnotations;
 
namespace Skywalker.Messaging.ViewModels {
 public class MessageTemplateSettingsViewModel {
  [UIHint("ParserPicker")]
  public string ParserId { getset; }
 }
}

 

We'll see the implementation for the "ParserPicker" template later on.

 

MessagingSiteSettingsPartHandler

The handler for the site settings will be responsible for 3 things:

  1. Adding a StorageFilter for the MessageSiteSettingsPartRecord repository;
  2. Adding an ActivatingFilter for the MessageSiteSettingsPart and the Site content type. This filter will weld on the MessageSiteSettingsPart whenever the Site content item is activated;
  3. Implementing the OnGetContentItemMetadata method for our content part to return information about which group this part belongs to. This will be key B in order for our settings editor to be rendered underneath a section called "Messaging".

 

Handlers/MessagingSiteSettingsPartHandler.cs:

using Orchard.ContentManagement;
using Orchard.ContentManagement.Handlers;
using Orchard.Data;
using Orchard.Environment.Extensions;
using Orchard.Localization;
using Skywalker.Messaging.Models;
 
namespace Skywalker.Messaging.Handlers {
    [OrchardFeature("Skywalker.Messaging")]
    public class MessagingSiteSettingsPartHandler : ContentHandler {
        public MessagingSiteSettingsPartHandler(IRepository<MessagingSiteSettingsPartRecord> repository) {
            Filters.Add(StorageFilter.For(repository));
            Filters.Add(new ActivatingFilter<MessagingSiteSettingsPart>("Site"));
            T = NullLocalizer.Instance;
            OnGetContentItemMetadata<MessagingSiteSettingsPart>((contextpart=> context.Metadata.EditorGroupInfo.Add(new GroupInfo(T("Messaging"))));
        }
 
        public Localizer T { getset; }
    }
}

 

 

MessagingSiteSettings Editor Template

 

Views/EditorTemplates/Parts/MessagingSiteSettings.cshtml:

@model Skywalker.Messaging.ViewModels.MessageTemplateSettingsViewModel
@{
    Style.Include("Common.css""Common.min.css");
}
<fieldset>
    <legend>@T("Template Parser")</legend>
    <div class="editor-field">
        @Html.LabelFor(m => m.ParserIdT("Default Parser"))
        @Html.EditorFor(m => m.ParserId)
        <span class="hint">@T("The template parser engine to use by default.")</span>
    </div>
</fieldset>

 

Nothing we haven't seen befre here. Let's see how the "ParserPicker" is implemented (which is used in the UIHintAttribute of the view model).

 

ParserPicker (1/2)

Views/EditorTemplates/ParserPicker.cshtml:

@Display.ParserPicker()

 

That looks simple enough. But why are we rendering an ad-hoc shape called ParserPicker? Couldn't we have done that straight from the MessageSiteSettings.cshtml view? Let's discuss both questions one at a time:

  1. We are rendering an ad hoc shape so that we can leverage a IShapeTableProvider class that injects a list of available parsers. We could have done this straight from the template, but doing this from a logically separated layer promotes separation of concerns as well as reusability: if we were to render another ParserPicker shape from someplace else, the same IShapeTableProvider would kick in and inject required data.
  2. When invoking the Html.EditorFor helper, it will setup some model metadata that we need when rendering the dropdownlist, such as the name for the field.

 

ParserPicker (2/2)

Views/ParserPicker.cshtml:

@using System.Linq
@using Skywalker.Messaging.Services
@{
 var parsers = ((IEnumerable<IParserEngine>)Model.Parsers).ToList();
 var currentValue = ViewData.TemplateInfo.FormattedModelValue as string;
 var options = parsers.Select(x => new SelectListItem { Text = x.DisplayTextValue = x.Id.ToString(), Selected = x.Id == currentValue });
}
@Html.DropDownList(""optionsT("(Default)").ToString())

 

There we go. What we're doing here is simply fetch a list of Parsers. project them into a list of SelectListItems, which we feed into the DropDownList html helper.

But where do the Parsers come from?

You guessed it: from an IShapeTableProvider.

 

ParserPickerShape

ParserPickerShape is our implementation of IShapeTableProvider:

 

Shapes/ParserPickerShape.cs:

 

using System.Linq;
using Orchard.DisplayManagement.Descriptors;
using Orchard.Environment;
using Orchard.Environment.Extensions;
using Skywalker.Messaging.Services;
 
namespace Skywalker.Messaging.Shapes {
 [OrchardFeature("Skywalker.Messaging")]
 public class ParserPickerShape : IShapeTableProvider {
  private readonly Work<IMessageTemplateService> _messageTemplateService;
 
  // Inject a Work of IMessageTemplateService.
  // Even though IShapeTableProvider derives fro IDependency,
  // the actual list of IShapeTableProviders is stored in an object that implements an interface that derives from ISingleton.
  // Therefore odds are that when it's time to display the shape, our IMessageTemplateService will be bound to an expired HTTP context.
  // Injecting a Work will solve this, because it will automatically resolve an instance of the specified type in the current HTTP context scope
  public ParserPickerShape(Work<IMessageTemplateService> messageTemplateService) {
   _messageTemplateService = messageTemplateService;
  }
 
  public void Discover(ShapeTableBuilder builder) {
   // Whenever a shape called "ParserPicker" is about to be rendered:
   builder.Describe("ParserPicker").OnDisplaying(context => {
    // Set a property on that shape called "Parsers". The value will be a list of IParserEngine objects
    context.Shape.Parsers = _messageTemplateService.Value.GetParsers().ToList();
   });
  }
 }
}

 

Whenever Orchard is about to render a shape (e.g. because of a call to @Display), it will go through all registered IShapeTableProviders that "describe" the shape about to be rendered. This is where we have a chance to provide additional data to our shape. In our case, we are setting a list of parsers.

Notice that we are injecting a Work of IMessageTemplateService instead of a IMessageTemplateService directly. The reason for that is that although IShapeTableProvider itself derives from IDependency, the object that holds the list of IShapeTableProviders (being DefaultShapeTableManager) implements an interface (IShapeTableManager) that derives from ISingleton.

That would cause our injected instance of IMessageTeplateService to be bound to an invalid HTTP context scope on subsequent calls, leading to all sorts of nasty errors.

The Work<T> class solves this issue nice and clean, because injecting this one will only resolve an IMessageTemplateService when we invoke its Value property, which we only do in the OnDisplaying method.

 

Placement.info 

Lastly, we need to register the editor shape with Placement.info by adding the following line:

<Place Parts_MessagingSiteSettings_Edit="Content:1" />

 

 

Let's see what we got so far:

 

Site Settings Result

 

Beautiful. And if we enable the Razor feature:

 

We will automatically see Razor Parser become available as well when we go to our Messaging settings:

 

All good! The user is now able to select which parser he wants to use for messaging templates.

Next, we will see how we can implement settings on a per content type/part level, so that the user can override this Default Parser setting on a per content type level.

 

Content Type/Part Settings

 

Content Type/Part scope settings are settings stored on a per content type/part level basis. This will allow the user to create ultiple template types, and choose a different parser engine per type.

The key to implementing such settings is to implement a class that derives from ContentDefinitionEditorEventsBase.

 

MessageTemplatePartSettingsEvents

The implementation looks like this:

Settings/MessageTemplatePartSettingsEvents.cs:

using System.Collections.Generic;
using Orchard.ContentManagement;
using Orchard.ContentManagement.MetaData;
using Orchard.ContentManagement.MetaData.Builders;
using Orchard.ContentManagement.MetaData.Models;
using Orchard.ContentManagement.ViewModels;
using Orchard.Environment.Extensions;
 
namespace Skywalker.Messaging.Settings {
    [OrchardFeature("Skywalker.Messaging")]
    public class MessageTemplatePartSettingsEvents : ContentDefinitionEditorEventsBase {
        
        public override IEnumerable<TemplateViewModel> TypePartEditor(ContentTypePartDefinition definition) {
            if (definition.PartDefinition.Name != "MessageTemplatePart")
                yield break;
 
            var model = definition.Settings.GetModel<MessageTemplatePartSettings>();
            yield return DefinitionTemplate(model);
        }
 
        public override IEnumerable<TemplateViewModel> TypePartEditorUpdate(ContentTypePartDefinitionBuilder builderIUpdateModel updateModel) {
            if (builder.Name != "MessageTemplatePart")
                yield break;
 
            var model = new MessageTemplatePartSettings();
 
            if (updateModel.TryUpdateModel(model"MessageTemplatePartSettings"nullnull)) {
                builder.WithSetting("MessageTemplatePartSettings.DefaultParserId"model.DefaultParserId);
 
                yield return DefinitionTemplate(model);
            }
        }
    }
}

 

It is basically a simplified driver specifically designed for content definition editors. We start with overriding the TypePartEditor method, which will deserialize any existing settings for our type/part into a model called MessageTemplatePartSettings (which we will define shortly) and yields a TemplateViewModel using the DefinitionTemplate method.

Notice that the base class ContentDefinitionEditorEventsBase contains the word "EditorEvents". Inheriting from this base class enables us to do advanced stuff when the user is editing not only a field, a part, or a type.

It also enables us to provide custom UI per Part in the Content editor screen, which we are doing here. This class provides us with granular control over customizing the UI for editing metadata of the content type system.

 

Settings/MessageTemplatePartSettings.cs:

using System.ComponentModel.DataAnnotations;
 
namespace Skywalker.Messaging.Settings {
    public class MessageTemplatePartSettings {
        [UIHint("ParserPicker")]
        public string DefaultParserId { getset; }
    }
}

 

Notice that again we're using the UIHintAttribute using the "ParserPicker" value. Since we already implemented that editor template, it will work without additional work. 

The second method in the MessageTemplatePartSettingsEvents is the "post back" version. This method is called for all implementations of IContentDefinitionEditorEvents (which ContentDefinitionEditorEventsBase implements), so we need to check for the name of the builder being passed in. If it's not the name of our part, we short circuit. If it is the name of our part, we new up a view model for modelbinding, and if that succeeds, we register a setting with the builder. Finally, we return a TemplateViewModel using the DefinitionTemplate method to render the view.

But which view exactly?

This mechanism supports a convention where we don't have to specify the name for the template to be used. We could use our own template filenames if we wanted to by instantiating a TemplateViewModel ourselves our by overwriting its TemplateName property, but let's stick with the convention: create a folder in the Views folder called DefinitionTemplates and add a file called MessageTemplatePartSettings.cs:

 

MessageTemplatePartSettings (View)

Lastly, we implement the view template for our custom content type/part settings.

Views/DefinitionTemplates/MessageTemplatePartSettings.cshtml:

@model Skywalker.Messaging.Settings.MessageTemplatePartSettings
@{
 Style.Include("Common.css""Common.min.css");
}
<fieldset>
 <div class="editor-field">
  @Html.LabelFor(m => m.DefaultParserIdT("Default Parser"))
  @Html.EditorFor(m => m.DefaultParserId)
  <span class="hint">@T("The template parser engine to use by default.")</span>
 </div>
</fieldset>

 

 If all went well, this is what the Content Type/Part level editor should look like:

 

 

Very cool. We managed to implement site level settings as well as content type/part level settings. We will put these settings to good use when we complete the implementation of the MessageTemplateService in the next part.

But before we end this part, let's enhance the Template editor with CodeMirror and implement a preview pane.

 

CodeMirror

CodeMirror is a very nice & simple to use JavaScript library that turns any textarea into a fullblown code editor. It supports a long list of syntaxes such as C, C++, C#, HTML, XML, JavaScript, CSS and many more. Except for Razor.

But that's no biggy, we'll just use the HTML Embedded mode. And, if you wanted to, you could totally create your own Razor mode: http://codemirror.net/doc/manual.html#modeapi

 

To turn a textarea into a code editor, all we have to do is include codemirror.js, the required files for the syntax/mode that we want to use, and invoke the CodeMirror.fromTextArea() method which takes a TextArea DOM element as well as an options object.

Let's see how it's done.

 

Downloading CodeMirror

The first thing we'll do is download the latest release from their website: http://codemirror.net/codemirror.zip

 

Next, extract the contents of the root folder of the zip file into Scripts/CodeMirror:

 

Now it's time to update the MessageTemplate.cshtml editor template in Views/EditorTemplates/Parts.

We'll surround the textarea currently used to render the template text value with a <div> element so that we can apply a little styling to the code editor.

In Views/EditorTemplates/Parts/MessageTemplate.cshtml, replace the following line:

@Html.TextAreaFor(m => m.Textnew { @class = "template-editor"})

 

With:

<div class="codemirror">
    @Html.TextAreaFor(m => m.Textnew { @class = "template-editor"})
</div>

 

We'll update the TemplateEditor.less file in a moment, for now let's focus on including the required CodeMirror stylesheets and scripts.

In the same file, at the top, replace the following:

@{
    Style.Include("Common.css""Common.min.css");
    Style.Include("TemplateEditor.css""TemplateEditor.min.css");
}

 

With:

@{
    Style.Include("~/Modules/Skywalker.Messaging/Scripts/CodeMirror/lib/codemirror.css");
    Style.Include("~/Modules/Skywalker.Messaging/Scripts/CodeMirror/theme/vibrant-ink.css");
    Style.Include("Common.css""Common.min.css");
    Style.Include("TemplateEditor.css""TemplateEditor.min.css");
    Script.Require("jQuery");
    Script.Include("CodeMirror/lib/codemirror.js");
    Script.Include("CodeMirror/mode/xml/xml.js");
    Script.Include("CodeMirror/mode/javascript/javascript.js");
    Script.Include("CodeMirror/mode/css/css.js");
    Script.Include("CodeMirror/mode/htmlmixed/htmlmixed.js");
    Script.Include("CodeMirror/mode/htmlembedded/htmlembedded.js");
    Script.Include("TemplateEditor.js");
}

 

 

Notice that because CodeMirrors comes with its own CSS files that are not stored in the conventional Styles folder, we have to include the full path.

We included all required files to enable CodeMirror and the HTMLEmbedded mode (which itself depends on xml.js, javascript.js, css.js and htmlmixed.js).

The last script file is a file we will create next:

 

Scripts/TemplateEditor.js

(function ($) {
 
    var initializeEditor = function() {
        var textArea = $("textarea.template-editor")[0];
        var editor = CodeMirror.fromTextArea(textArea, {
            lineNumbers: true,
            mode: "application/x-ejs",
            indentUnit: 4,
            indentWithTabs: true,
            enterMode: "keep",
            tabMode: "shift",
            theme: "vibrant-ink",
            autoCloseTags: true
        });
    };
 
    $(function() {
        initializeEditor();
    });
})(jQuery);

 

Basically, this script defines a function called initializeEditor which is invoked on page load.

The function searches for the texarea we designated as the template editor and invoke CodeMirror.fromTextArea passing in the textare as the argument, along with some options.

Let's see how it looks:

 

Just the way I like it!

 

Implementing a Preview Pane

Now, we talked a little about the Master Layout feature, where one template could have another template as its Layout.

Wouldn't it be nice if we could actually have a preview pane that renders a preview of the HTML, taking into account the selected Layout? Of course it would!

So let's do it.

First of all, we'll add the required markup to the MessageTemplate.cshtml view.

Views/EditorTemplates/Parts/MessageTemplate.cshtml:

@model Skywalker.Messaging.ViewModels.MessageTemplateViewModel 
@{
    Style.Include("~/Modules/DarkSky.Messaging/Scripts/CodeMirror/lib/codemirror.css");
    Style.Include("~/Modules/DarkSky.Messaging/Scripts/CodeMirror/theme/vibrant-ink.css");
    Style.Include("Common.css""Common.min.css");
    Style.Include("MessageTemplateEditor.css""MessageTemplateEditor.min.css");
    Script.Require("jQuery");
    Script.Include("CodeMirror/lib/codemirror.js");
    Script.Include("CodeMirror/mode/xml/xml.js");
    Script.Include("CodeMirror/mode/javascript/javascript.js");
    Script.Include("CodeMirror/mode/css/css.js");
    Script.Include("CodeMirror/mode/htmlmixed/htmlmixed.js");
    Script.Include("CodeMirror/mode/htmlembedded/htmlembedded.js");
    Script.Include("TemplateEditor.js");
}
<fieldset class="message-template-editor" data-layout-beacon="@Model.ExpectedParser.LayoutBeacon">
    <div class="editor-field">
        @Html.LabelFor(m => m.TitleT("Title"))
        @Html.TextBoxFor(m => m.Titlenew { required = "required"@class = "text large" })
        @Html.ValidationMessageFor(m => m.Title)
        <span class="hint">@T("The title of the message")</span>
    </div>
    <div class="editor-field">
        @Html.LabelFor(m => m.SubjectT("Subject"))
        @Html.TextBoxFor(m => m.Subjectnew { required = "required"@class = "text large" })
        @Html.ValidationMessageFor(m => m.Subject)
        <span class="hint">@T("The subject to use when sending a message")</span>
    </div>
    @if (Model.Layouts.Any()) {
        <div class="editor-field layout-selector-wrapper">
            @Html.LabelFor(m => m.LayoutIdT("Layout"))
            @Html.EditorFor(m => m.LayoutIdnew { Model.Layouts })
            <span class="hint">@T("Optionally select another template to use as the layout / masterpage template")</span>
        </div>
    }
    <div class="row">
        <div class="span6">
            <div class="editor-field">
                @Html.LabelFor(m => m.TextT("Template. (Expected parser: {0})"Model.ExpectedParser.DisplayText))
                <div class="codemirror">
                    @Html.TextAreaFor(m => m.Textnew { @class = "template-editor"})
                </div>
                @Html.ValidationMessageFor(m => m.Text)
                <span class="hint">@T("The template of the message")</span>
            </div>
        </div>
        <div class="span6">
            <div class="editor-field">
                <label>@T("Preview")</label>
                <div class="codemirror-preview">
                    <iframe></iframe>
                </div>
                <span class="hint">@T("The preview of the template")</span>
            </div>
        </div>
    </div>
    <div class="editor-field">
        <input type="checkbox" name="@Html.FieldNameFor(m => m.IsLayout)" id="@Html.FieldIdFor(m => m.IsLayout)" value="True" @if(Model.IsLayout){<text>checked="checked"</text>} />
        <label for="@Html.FieldIdFor(m => m.IsLayout)" class="forcheckbox">@T("This is a Layout")</label>
        <span class="hint">@T("Check this option to use it as the layout (a.k.a. Master Page) for other templates.")</span>
    </div>
</fieldset>

 

The majority of the added code has replaced the initial markup of the <div class="codemirror"> element (which is still there, it's just wrapped with some html to neatly lay it out next to a preview pane).

The Preview pane is implemented as an iframe, which will load the HTML defined in our codemirror textarea. Notice that we are now actually using the ExpectedParser property of the Model. We need to set this property from our driver, which we'll update in a minute.

Next, we need to write more javascript to have the preview pane (iframe) updated whenever the user makes a change in the textarea.

And not only that, we want to wrap the HTML inside of the selected Layout, if any has been selected.

Let's look at the code.

Scripts/TemplateEditor.js

(function($) {
    var layoutDictionary = {};
    var currentLayoutContent = null;
    
    // Loads the HTML of the currently selected layout.
    var getLayoutContent = function(layoutPicker) {
 
        if (!currentLayoutContent) {
            var layoutId = parseInt(layoutPicker.val());
            
            // See if we have a cached version of the layout's content.
            currentLayoutContent = layoutDictionary[layoutId];
 
            if (!currentLayoutContent && !isNaN(layoutId)) {
                // Load the template text of the selected layout.
                var url = layoutPicker.data("load-layout-url") + "/" + layoutId;
                var promise = $.ajax({
                    url: url
                });
 
                // When the promised data is available, store it in the dictionary for future lookups.
                promise.done(function(html) {
                    layoutDictionary[layoutId] = html;
                    currentLayoutContent = html;
                });
 
                // Return the promise
                return promise;
            }
        }
 
        // Return a promise that will resolve immediately.
        return $.Deferred().resolve(currentLayoutContent);
    };
 
    // Updates the preview iframe, taking into account any selected layout.
    var updatePreview = function(editor, previewFrame, layoutPicker, layoutBeacon) {
        var frame = previewFrame[0];
        var preview = frame.contentDocument || frame.contentWindow.document;
        var content = editor.getValue();
        var layoutContentPromise = getLayoutContent(layoutPicker);
 
        // When the layoutcontent is available, process it.
        layoutContentPromise.done(function(layoutContent) {
            if (layoutContent != null && layoutContent != "") {
                // Processing the layout is a simple matter of replacing the layoutBeacon with the content of the current template being edited.
                content = layoutContent.replace(layoutBeacon, content);
            }
            
            // Write the final template to the iframe.
            preview.open();
            preview.write(content);
            preview.close();
        });
 
    };
 
    var initializeEditors = function() {
        var textArea = $("textarea.template-editor")[0];
        var layoutPicker = $(this).find(".layout-selector-wrapper select");
        var previewPane = $(this).find("iframe");
        var layoutBeacon = $(this).data("layout-beacon");
        var editor = CodeMirror.fromTextArea(textArea, {
            lineNumbers: true,
            mode: "application/x-ejs",
            indentUnit: 4,
            indentWithTabs: true,
            enterMode: "keep",
            tabMode: "shift",
            theme: "vibrant-ink",
            autoCloseTags: true
        });
 
        var delay = 10;
        var delayTimerCallback = function() { updatePreview(editor, previewPane, layoutPicker, layoutBeacon); };
        var delayTimer = setTimeout(delayTimerCallback, delay);
 
        editor.on("change", function() {
            clearTimeout(delayTimer);
            delayTimer = setTimeout(delayTimerCallback, delay);
        });
 
        layoutPicker.on("change", function() {
            currentLayoutContent = null;
            updatePreview(editor, previewPane, layoutPicker, layoutBeacon);
        });
    };
 
    $(function() {
        initializeEditors();
    });
})(jQuery);

 

The first function, getLayoutContent, loads the template text of the selected Layout. Notice that it implements a caching mechanism using a lookup dictionary. This prevents loading the layout using AJAX on each keypress. Notice that it uses the url fetched from a data attribute. This attribute will be added to the markup in the TemplateLayoutPicker.cshtml view shortly, after we implement the controller that handles the request.

The second function, updatePreview, is responsible for updating the iframe after it processes the selected layout, if any. And this is the second time where the LayoutBeacon comes into play: it's a simple mechanism that replaces the beacon value in the layout template with the template text that the user is currently editing.

The last function we already know, but it has been extended to add 2 event handlers: one for the change event of the textarea, and another one for the change event of the layout picker select list. Both events will trigger a call to update the preview pane.

 

The controller that returns the template text looks like this:

Controllers/MessageTemplateController.cs:

using System;
using System.Web.Mvc;
using Orchard.Environment.Extensions;
using Orchard.UI.Admin;
using Skywalker.Messaging.Services;
 
namespace Skywalker.Messaging.Controllers {
    [AdminOrchardFeature("Skywalker.Messaging")]
    public class MessageTemplateController : Controller {
        private readonly IMessageTemplateService _messageTemplateService;
 
        public MessageTemplateController(IMessageTemplateService messageTemplateService) {
            _messageTemplateService = messageTemplateService;
        }
 
        public string LayoutContent(int id) {
            var template = _messageTemplateService.GetTemplate(id);
 
            if(!template.IsLayout)
                throw new InvalidOperationException("That is not a Layout");
 
            return template.Text;
        }
    }
}

 

It's a simple controller that exposes an action called LayoutContent accepting the ID of a template. If the template is in fact a Layout, it returns its text. Simple as that.

Finally, let's see the updated markup of the TemplateLayoutPicker view:

Views/EditorTemplates/TemplateLayoutPicker.js:

@using System.Linq
@using Skywalker.Messaging.Models
@{
    Script.Include("TemplateLayoutPicker.js");
    var layouts = ((IEnumerable<MessageTemplatePart>)ViewBag.Layouts).ToList();
    var currentValue = ViewData.TemplateInfo.FormattedModelValue as int?;
    var options = layouts.Select(x => new SelectListItem { Text = x.TitleValue = x.Id.ToString(), Selected = x.Id == currentValue });
    var editUrl = currentValue != null ? Url.Action("Edit""Admin"new { id = layouts[0].Idarea = "Contents" }) : "#";
    var style = currentValue != null ? "display:inline;" : "display:none;";
}
<div class="layout-picker-wrapper">
    @Html.DropDownList(""options""new RouteValueDictionary { { "data-load-layout-url"Url.Action("LayoutContent""MessageTemplate"new { area = "Skywalker.Messaging" }) } })
    <a href="@editUrl" style="@style" data-href="@Html.Raw(Url.Action("Edit""Admin"new { id = "id"area = "Contents" }))">@T("Edit Layout")</a>
</div>

 

Not only are we adding a data-load-layout-url attribute, we're also adding an "Edit Layout" link next to the select list so that the user can quickly edit the layout if he sees something that needs to be changed. To facilitate that, we need some additional scripting, which is implemented in a new file called TemplateLayoutPicker.js:

Scripts/TemplateLayoutPicker.js:

(function ($) {
 
    $(function() {
        $(".layout-picker-wrapper").on("change", "select", function() {
            var id = $(this).val();
            var link = $(this).parents(".layout-picker-wrapper:first").find("a[data-href]");
            link.attr("href", link.data("href").replace("id", id));
 
            if (id == "")
                link.hide();
            else
                link.show();
        });
    });
})(jQuery);

 

It dynamically updates the href attribute of the "Edit Layout" link when the user changes the selected layout.

Finally, let's update the MessageTemplatePartDriver to set the ExpectedParser property. This will hint the user as to what syntax to use for the template.

Drivers/MessageTemplatePartDriver.cs:

In this file, replace these lines:

var viewModel = new MessageTemplateViewModel {
 
    // The editor will render a dropdown list of available templates that are marked as Layout.
    // The following call will return all content items with a MessageTemplatePart where IsLayout == true.
    // We are also filtering out the current content item ID, because we cannot allow the user to set the layout of a template to itself.
    Layouts = _messageTemplateService.GetLayouts().Where(x => x.Id != part.Id).ToList(),
};

 

With the following:

var viewModel = new MessageTemplateViewModel {
 
    // The editor will render a dropdown list of available templates that are marked as Layout.
    // The following call will return all content items with a MessageTemplatePart where IsLayout == true.
    // We are also filtering out the current content item ID, because we cannot allow the user to set the layout of a template to itself.
    Layouts = _messageTemplateService.GetLayouts().Where(x => x.Id != part.Id).ToList(),
 
    // Set the expected parser based on the template part so that the user knows what syntax is expected.
    ExpectedParser = _messageTemplateService.SelectParser(part),
};

 

If you remember, we never implemented the SelectParser method, so let's do it now:

Services/MessageTemplateService.cs:

public IParserEngine SelectParser(MessageTemplatePart template) {
    // Get the parser ID configured on the content type/part level.
    var parserId = template.DefaultParserId;
    IParserEngine parser = null;
 
    // If a parser ID was specified, try and select that parser.
    if (!string.IsNullOrWhiteSpace(parserId)) {
        parser = GetParser(parserId);
    }
 
    // If we didn't find a parser, try and select the parser configured on the site scope level.
    if (parser == null) {
        parserId = _services.WorkContext.CurrentSite.As<MessagingSiteSettingsPart>().DefaultParserId;
        parser = GetParser(parserId);
    }
 
    // If we still found no parser, return the first one.
    return parser ?? _parsers.First();
}

 

All this method is doing is checing if the specified template has a parser configured using the DefaultParserId, which we'll see now:

Models/MessageTemplatePart.cs:

/// <summary>
/// Returns the parser ID configured on the content type/part level.
/// </summary>
public string DefaultParserId {
    get { return Settings.GetModel<MessageTemplatePartSettings>().DefaultParserId; }
}

 

We're using the GetModel<T> method that internally model binds the Settings dictionary to whatever we specify as T. Quite neat if you ask me!

Returning back to the SelectParser method, we will try and find a parser with the returned ID.

If no parser could be found, we use our site scoped settings part to get the configured parser, if any.

If no default parser was configured there either, we'll fall back on the first parser we find. There will always be at least one parser, since the two parsers we implemented are bound to the same feature that this service is bound to.

Before we will see how things look, we need to update Common.less with some code to make the preview pane ook nice:

Styles/Common.less:

.editor-field {
    margin-bottom1em;    
}
 
.row {
    min-height1%;
 
    &:after {
        clearboth;
        content".";
        displayblock;
        height0;
        visibilityhidden;
        font-size0;
    }
 
    [class*="span"] {
        displayblock;
        width100%;
        min-height0;
        -webkit-box-sizingborder-box;
        -moz-box-sizingborder-box;
        box-sizingborder-box;
        floatleft;
        margin-left2.127659574468085%;
        *margin-left2.074468085106383%;
 
        &:first-child {
            margin-left0;
        }
    }
 
    .span6 {
        width48.93617021276595%;
        *width48.88297872340425%;
    }
}

 

 

Let's see how things are working out. To demonstrate the Layout feature, let's create another Email Template that will use our Master template as its Layout.

 

The Master Layout:

 

 

An Email Template using the Master Layout:

 

As you can see, te selected layout is neatly applied to the template being edited.

 

Summary

In this part, we implemented two Template Parsers, demonstrated how to implement site-wide settings as well as content type/part scoped settings and how to use these settings.

We enhanced the template editor with CodeMirror and implemented a preview pane.

In the next part of this series, we will finally see how to leverage these templates and parsers, when we have a look at creating a Rule, implementing a custom Action, and integrate Tokens for use with the Subject field as well as exposing information to the template parsers.

 

Download the source code: Skywalker.Messaging.Part2.zip.

 

Read Part 3

9 comments

  • ... Posted by Abhishek Luv Posted 11/07/2014 07:38 AM

    Awesome spiky love your blog

  • ... Posted by Michael Sawczyn Posted 11/07/2014 07:39 AM

    "What would be really interesting is if we could create our email templates with Razor syntax"

    Take a look at Postal (https://github.com/andrewdavey/postal) We've used it in a few projects and it works nicely.

  • ... Posted by Kees Schouten Posted 11/07/2014 07:39 AM

    Thanks! Looking forward for next part.

  • ... Posted by GadgetMadGeek Posted 11/07/2014 07:40 AM

    When I set 'Razor' as the default parser, create a template, it works fine. If I then go and change the default parser to 'simple text parser', save, then select 'New -> Email template', it still shows 'Expected Parser: Razor Engine'. ??

  • ... Posted by Sipke Schoorstra Posted 11/07/2014 07:40 AM [http://www.ideliverable.com]

    That's a bug, will be fixed in the next part. Thanks for reporting!

  • ... Posted by Tobias Burger Posted 11/07/2014 07:41 AM

    many thanks for this tremendous blog series! I've learned so many things! And I'm shure I will learn a lot of cool stuff in the upcoming blog posts...

    I have one question: can I use nuget to get the dependencies instead of downloading the binary? I've used nuget to download RazorMachine (CodeMirror on nuget is only available in an older version or in a weirdly splitted newer version). does something speek against it?

    PS: I prefer the monokai theme for the code editor! ;)

  • ... Posted by Sipke Schoorstra Posted 11/07/2014 07:41 AM [http://www.ideliverable.com]

    Absolutely, you can totally use Nuget to get these dependencies instead of downloading the library manually. The reason I didn't is because I prefer to keep things inside of the module if possible, so it becomes easy to redistribute it. Interesting theme you have there. A little too happy for my taste though :)

  • ... Posted by Tobias Burger Posted 11/07/2014 07:41 AM

    If the Orchard module manager downloads the dependencies from nuget when installing a module this would be nice (have to give it a try if this actually works).

    One reason why I prefer the nuget approach is that the binaries don't need to be included in the version control and are restored by the nuget package manager (I can restrict the allowed version in the packages.config file if I want to support a specific version of an assembly).

  • ... Posted by Sander Posted 03/04/2015 03:35 PM

    Seems worth mentioning that the dependency in the module.txt on Orchard.Messaging causes the module not be able to be enabled (Orchard 1.x)

Leave a comment