Messaging, Channels, Rules and Tokens - Part 1

This module will enable our users to do the following:

  • Create and manage Messaging Templates (which will typically be used for email messages) using parser engine specific syntax (which will be Razor by default);
  • Setup Rules that sends a templated message when one or more events occur;
  • Use Tokens in the Subject field as well in a field that is part of the SendTemplatedMessage action;
  • Configure which parser engine to use by default using site scope settings and content type scope settings.

 

At the end of this tutorial we will not only have a useful module, we will have learned how we can wire things together with the Rules and Tokens modules and have a better understanding of how they work and can be leveraged in other modules.

 

Template Management

In this first part, we will start simple by creating the module and implementing a basic Template Management System.

We will:

  • Define a MessageTemplatePart that stores information about a template, such as the template text itself;
  • Define an EmailTemplate content type that uses the MessageTemplatePart;
  • Implement an IMessageTemplateService that helps with quering templates.
  • Learn how to implement import/export for custom parts that reference other content items using the ImportContentContext.GetItemFromSession method.

All in all, this first part will handle nothing but basic module development except maybe for a couple of small but interesting details. For those who are just getting started with Orchard Module development, this is a probably a good intro.

For those who have done this a hundred times already, you could simply download the zip file and head over to the next part, but I recommend skimming over this post to get quick glimpse of what we're doing and where we're headed.

 

Let's get started!

 

Creating the Module

I'll assume you know how to generate a new module by now. If not, please check out the following resources:

 

Go ahead and codegen a new module called Skywalker.Messaging.

 

The Model

Our module's purpose is to provide message template management features. But what type of template would the user want to work with? An Email template? SMS? Or perhaps a template that can be processed by MailChimp?

We can't possibly know upfront. So what we will do is provide the essentials while allowing users and developers to extend the system. Good thing we are working with Orchard, because it has just the mechanism we need: Content Parts!

That's right, we'll create a content part called MessageTemplatePart, which can be used to build any template type a user wants. By default, we will include a migration that sets up an EmailTemplate content type, as such a type is probably the most commonly used one.

 

Models/MessageTemplatePart.cs: 

using Orchard.ContentManagement;
using Orchard.ContentManagement.Aspects;
using Orchard.ContentManagement.Records;
using Orchard.ContentManagement.Utilities;
using Orchard.Data.Conventions;
 
namespace Skywalker.Messaging.Models {
 /// <summary>
 /// Represents the essentials of a message template.
 /// Can be attached to any content type that needs to function as a message template.
 /// </summary>
 public class MessageTemplatePart : ContentPart<MessageTemplatePartRecord>ITitleAspect {
  internal LazyField<MessageTemplatePart> LayoutField = new LazyField<MessageTemplatePart>();
 
  /// <summary>
  /// The title of this template. Primarily used to easily identify this template in the dashboard.
  /// </summary>
  public string Title {
   get { return Record.Title; }
   set { Record.Title = value; }
  }
 
  /// <summary>
  /// The subject to use when sending a message using this template. Supports tokens.
  /// </summary>
  public string Subject {
   get { return Record.Subject; }
   set { Record.Subject = value; }
  }
 
  /// <summary>
  /// The actual template text to be processed.
  /// </summary>
  public string Text {
   get { return Record.Text; }
   set { Record.Text = value; }
  }
 
  /// <summary>
  /// Indicates whether this template is to be used as a Layout.
  /// </summary>
  public bool IsLayout {
   get { return Record.IsLayout; }
   set { Record.IsLayout = value; }
  }
 
  /// <summary>
  /// Layout is optional.
  /// If it references another MessageTemplatePart,
  /// it will be used as the "Master" layout when parsing the template.
  /// </summary>
  public MessageTemplatePart Layout {
   get { return LayoutField.Value; }
   set { LayoutField.Value = value; }
  }
 }
 
 /// <summary>
 /// The storage record class for the <see cref="MessageTemplatePart"/>
 /// </summary>
 public class MessageTemplatePartRecord : ContentPartRecord {
  public virtual string Title { getset; }
  public virtual string Subject { getset; }
 
  [StringLengthMax]
  public virtual string Text { getset; }
  public virtual int? LayoutId { getset; }
  public virtual bool IsLayout { getset; }
 }
}

 

 

As you will have noticed, we are including a property that can reference another MessageTemplatePart. The reason for this is so that we can support Master template scenarios.

Ususally, when you send out emails, you will want them to have a fixed header and footer, and simply vary the content. If we didn't support the notion of a Template Layout, we would have to duplicate the header and footer in all templates, which as you can imagine would quickly become a maintenance nightmare if you have a lot of templates.


The Migration

Our migration will be nice and simple: it will create a table for our MessageTemplatePartRecord class and define the MessageTemplatePart.

It will also define a content type called EmailTemplate that takes advantage of the MessageTemplatePart.

Migrations.cs:

using Orchard.ContentManagement.MetaData;
using Orchard.Core.Contents.Extensions;
using Orchard.Data.Migration;
using Orchard.Environment.Extensions;
 
namespace Skywalker.Messaging {
    [OrchardFeature("Skywalker.Messaging")]
    public class Migrations : DataMigrationImpl {
         public int Create() {
 
             // Create a table for the MessageTemplatePartRecord class
             SchemaBuilder.CreateTable("MessageTemplatePartRecord"table => table
                 .ContentPartRecord()
                 .Column<string>("Title"c => c.WithLength(256))
                 .Column<string>("Subject"c => c.WithLength(256))
                 .Column<string>("Text"c => c.Unlimited())
                 .Column<int>("LayoutId"c => c.Nullable())
                 .Column<bool>("IsLayout"c => c.NotNull()));
 
             // Define the MessageTemplatePart
             ContentDefinitionManager.AlterPartDefinition("MessageTemplatePart"part => part.Attachable());
 
             // Define an EmailTemplate content type. Other types can be defined by the user using the dashboard and by developers to implement advanced scenarios.
             ContentDefinitionManager.AlterTypeDefinition("EmailTemplate"type => type
                 .WithPart("CommonPart")
                 .WithPart("MessageTemplatePart")
                 .DisplayedAs("Email Template")
                 .Draftable()
                 .Creatable());
 
             return 1;
         }
    }
}

 

 

The Driver

To provide an editor UI to our users, we will implement a driver for the MessageTemplatePart. This driver will only implement the 2 Editor methods and the Importing and Exporting methods.

The Driver will take a dependency on a service called IMessageTemplateService, makes use of a class called MessageTemplateViewModel and some string extension method called TrimSafe. We'll include all that code next, so don't worry if your code doesn't compile after including the driver code.

Let's have a look at the code:

Drivers/MessageTemplatePartDriver.cs:

using System.Linq;
using System.Xml;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.ContentManagement.Handlers;
using Orchard.Environment.Extensions;
using Skywalker.Messaging.Helpers;
using Skywalker.Messaging.Models;
using Skywalker.Messaging.Services;
using Skywalker.Messaging.ViewModels;
 
namespace Skywalker.Messaging.Drivers {
    [OrchardFeature("Skywalker.Messaging")]
    public class MessageTemplatePartDriver : ContentPartDriver<MessageTemplatePart> {
        private readonly IContentManager _contentManager;
        private readonly IMessageTemplateService _messageTemplateService;
 
        public MessageTemplatePartDriver(IContentManager contentManagerIMessageTemplateService messageTemplateService) {
            _contentManager = contentManager;
            _messageTemplateService = messageTemplateService;
        }
 
        /// <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 "MessageTemplate"; }
        }
 
        protected override DriverResult Editor(MessageTemplatePart partdynamic shapeHelper) {
            return Editor(partnullshapeHelper);
        }
 
        protected override DriverResult Editor(MessageTemplatePart partIUpdateModel updaterdynamic shapeHelper) {
            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()
            };
 
            if (updater != null) {
 
                // We are in "postback" mode, so update our part
                if (updater.TryUpdateModel(viewModelPrefixnullnull)) {
                    part.Title = viewModel.Title.TrimSafe();
                    part.Subject = viewModel.Subject.TrimSafe();
                    part.Text = viewModel.Text;
                    part.Layout = viewModel.LayoutId != null ? _contentManager.Get<MessageTemplatePart>(viewModel.LayoutId.Value) : null;
                    part.IsLayout = viewModel.IsLayout;
                }
            }
            else {
 
                // We are in render mode (not postback), so initialize our view model.
                viewModel.Title = part.Title;
                viewModel.Subject = part.Subject;
                viewModel.Text = part.Text;
                viewModel.LayoutId = part.Record.LayoutId;
                viewModel.IsLayout = part.IsLayout;
            }
 
            // Return the EditorTemplate shape, configured with prope values.
            return ContentShape("Parts_MessageTemplate_Edit", () => shapeHelper.EditorTemplate(TemplateName"Parts/MessageTemplate"ModelviewModelPrefixPrefix));
        }
 
        protected override void Exporting(MessageTemplatePart partExportContentContext context) {
            context.Element(part.PartDefinition.Name).SetAttributeValue("Title"part.Title);
            context.Element(part.PartDefinition.Name).SetAttributeValue("Subject"part.Subject);
            context.Element(part.PartDefinition.Name).SetAttributeValue("Text"part.Text);
            context.Element(part.PartDefinition.Name).SetAttributeValue("IsLayout"part.IsLayout);
 
            // Here it gets interesting: "Layout" references another content item, and we can't simply serialize its Id because that may change across databases.
            // So instead, we leverage the content manager's GetItemMetadat method to ask for the content's Identity.
            // The resulting identity is built by various attached parts to the content item.
            // For example, the AutoroutePartHandler will contribute the DisplayAlias.
            // The IdentifierPartHandler will contribute a GUID.
            // This will of course only happen if the content item has these parts attached.
            // In our case, we don't require the content item to have either one attached, so we will have our own content handler contributing an identity.
            // We'll use this identity value in the Importing method to locate the referenced content item.
            if (part.Layout != null)
                context.Element(part.PartDefinition.Name).SetAttributeValue("Layout"context.ContentManager.GetItemMetadata(part.Layout).Identity.ToString());
        }
 
        protected override void Importing(MessageTemplatePart partImportContentContext context) {
            context.ImportAttribute(part.PartDefinition.Name"Title"x => part.Title = x);
            context.ImportAttribute(part.PartDefinition.Name"Subject"x => part.Subject = x);
            context.ImportAttribute(part.PartDefinition.Name"Text"x => part.Text = x);
            context.ImportAttribute(part.PartDefinition.Name"IsLayout"x => part.IsLayout = XmlConvert.ToBoolean(x));
 
            // If "Layout" is specified, it will be the identoity of the referenced content item.
            // The context argument has a ethod called "GetItemFromSession" which will return the content item corresponding
            // to the identity value we used in the Exporting method.
            context.ImportAttribute(part.PartDefinition.Name"Layout"x => {
                var layout = context.GetItemFromSession(x);
 
                if (layout != null && layout.Is<MessageTemplatePart>()) {
                    part.Layout = layout.As<MessageTemplatePart>();
                }
            });
        }
    }
}

 

 

That looks like a lot of code, but the implementation is simple: we implement both Editor methods and the Importing and Exporting methods.

In the Editor method, we are instantiating a view model of type MessageTemplateViewModel. The reason that we are using an additional class as the view model instead of using the MessageTemplatePart directly as a view model is simple: we want to include additional data with the view model (a list of available Layouts). We could have implemented this differently, as is described here: http://www.skywalkersoftwaredevelopment.net/orchard-development/content-part-editors

That would have worked out just as fine, but I it's interesting to see different approaches.

As described elaborately in the comments, we are taking advantage of content identities and the GetItemFromSession method to export and import related content. The challenge with exporting and importing related content is: how to identify content. We can;t simply use their primary key values, as those will surely change from database to database.

So instead, Orchard provides a mechanism to build an Identity. This is done via ContentHandlers. We'll see how to implement this later on. For now it's enough to know that we can get the Identity of any content item using the GetItemMetadata method of the content manager.

Before we move on to the content handler, let's have a look at the missing pieces: MessageTemplateViewModel, IMessageTemplateService and TrimSafe.

 

MessageTemplateViewModel

MessageTemplateViewModel is a simple class that looks like this:

ViewModels/MessageTemplateViewModel.cs:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Skywalker.Messaging.Models;
 
namespace Skywalker.Messaging.ViewModels {
    public class MessageTemplateViewModel {
        [RequiredStringLength(256)]
        public string Title { getset; }
 
        [RequiredStringLength(256)]
        public string Subject { getset; }
        public string Text { getset; }
 
        [UIHint("TemplateLayoutPicker")]
        public int? LayoutId { getset; }
        public bool IsLayout { getset; }
        public IList<MessageTemplatePart> Layouts { getset; }
    }
}

 

Pretty basic stuff that you would see in many a MVC application: a bunch of properties annotated with DataAnnotations. We're using the UIHintAttribute to specify a specific editor template for the LayoutId property. We'll see how that looks later on.

 

IMessageTemplateService

IMessageTemplateService contains our "business logic" if you will, although be it rather simplistic: it enables us to easily retrieve templates and layouts (which are exactly the same as templates, but with its IsLayout property set to true). It also contains a method to parse a template into the final output. This takes Layouts and view engine specific constructs into account. The engine to be used will be configurable by the user. Out of the box we will provide 2 parser engines: SimpleTextParserEngine and RazorParserEngine.

 

Services/IMessageTemplateService.cs:

using System.Collections.Generic;
using Orchard;
using Skywalker.Messaging.Models;
 
namespace Skywalker.Messaging.Services {
    public interface IMessageTemplateService : IDependency {
        IEnumerable<MessageTemplatePart> GetLayouts();
        IEnumerable<MessageTemplatePart> GetTemplates();
        IEnumerable<MessageTemplatePart> GetTemplatesWithLayout(int layoutId);
        MessageTemplatePart GetTemplate(int id);
        string ParseTemplate(MessageTemplatePart templateParseTemplateContext context);
        IEnumerable<IParserEngine> GetParsers();
        IParserEngine GetParser(string id);
        IParserEngine SelectParser(MessageTemplatePart template);
    }
}

 

 

This service has 3 primary related responsibilities: Querying the available templates, querying the available parsers, and actually parsing a template (which will automatically select the correct parser based on the template and its settings).

As we all know, an important software design principle is the Single Responsibility Principle (SRP). This basically means that all classes should ideally have no more than one responsibility. This is an important principle, but it is als important to be practical, and in this case the 3 mentioned responsibilities are in my opinion closely related enough that it would be overkill to separate them out into their own interfaces.

We will implement this interface partially in this part, and complete it in the next part.

 

MessageTemplateService

The initial implementation looks like this:

Services/MessageTemplateService.cs:

using System.Collections.Generic;
using System.Linq;
using Orchard.ContentManagement;
using Orchard.Environment.Extensions;
using Skywalker.Messaging.Models;
 
namespace Skywalker.Messaging.Services {
 [OrchardFeature("Skywalker.Messaging")]
 public class MessageTemplateService : IMessageTemplateService {
  private readonly IContentManager _contentManager;
  private readonly IEnumerable<IParserEngine> _parsers;
 
  public MessageTemplateService(IEnumerable<IParserEngine> parsersIContentManager contentManager) {
   _parsers = parsers;
   _contentManager = contentManager;
  }
 
  public IEnumerable<MessageTemplatePart> GetLayouts() {
   return _contentManager.Query<MessageTemplatePartMessageTemplatePartRecord>().Where(x => x.IsLayout).List();
  }
 
  public IEnumerable<MessageTemplatePart> GetTemplates() {
   return _contentManager.Query<MessageTemplatePartMessageTemplatePartRecord>().Where(x => !x.IsLayout).List();
  }
 
  public IEnumerable<MessageTemplatePart> GetTemplatesWithLayout(int layoutId) {
   return _contentManager.Query<MessageTemplatePartMessageTemplatePartRecord>().Where(x => x.LayoutId == layoutId).List();
  }
 
  public MessageTemplatePart GetTemplate(int id) {
   return _contentManager.Get<MessageTemplatePart>(id);
  }
 
  public IEnumerable<IParserEngine> GetParsers() {
   return _parsers;
  }
 
  public IParserEngine GetParser(string id) {
   return _parsers.SingleOrDefault(x => x.Id == id);
  }
 
  public string ParseTemplate(MessageTemplatePart templateParseTemplateContext context) {
   throw new System.NotImplementedException();
  }
 
  public IParserEngine SelectParser(MessageTemplatePart template) {
   throw new System.NotImplementedException();
  }
 }
}

 

 

The methods ParseTemplate and SelectParser will be implemented in the next part.

 

IMessageTemplateService and its implementation MessageTemplateService reference 2 more types the we will define next: ParseTemplateContext and IParserEngine.

 

IParserEngine

IParserEngine is the contract that all template parsers must implement in order to be usable as a template parser. In the upcoming parts, we will be implementing a SimpleTextParserEngine and a RazorParserEngine.

 

Services/IParserEngine.cs:

using Orchard;
using Skywalker.Messaging.Models;
 
namespace Skywalker.Messaging.Services {
 public interface IParserEngine : IDependency {
  /// <summary>
  /// The unique ID of the parser. The default base implementation will use the FullName value of its type.
  /// </summary>
  string Id { get; }
 
  /// <summary>
  /// The user friendly text of the parser.
  /// The default base implementation will use the Name property of its type, but is typically overridden in derived classes.
  /// </summary>
  string DisplayText { get; }
 
  /// <summary>
  /// The string / expression that will be replaced with child templates.
  /// For example, if a template is configured to be a Layout, it will replace its LayoutBeacon expression with the output of its child template.
  /// </summary>
  /// <example>
  /// @RenderBody() in the case of a Razor engine, [Body] in the case of the SimpleTextParserEngine.
  /// </example>
  string LayoutBeacon { get; }
 
  /// <summary>
  /// Processes the specified template and returns the resulting string.
  /// </summary>
  /// <param name="template">The template to parse</param>
  /// <param name="context">Additional context to the parser engine.</param>
  string ParseTemplate(MessageTemplatePart templateParseTemplateContext context);
 }
}

 

All members should be self explanatory, except maybe the LayoutBeacon. We'll see what this is and how its used later on. It's pretty simple, you'll see.

 

ParseTemplateContext

 

This is a simple class that simply holds all the (potentially) useful stuff a template parser could use. For now, it looks like this:

 

Models/ParseTemplateContext.cs:

namespace Skywalker.Messaging.Models {
 public class ParseTemplateContext {
  public object Model { getset; }
  public object ViewBag { getset; }
 }
}

 

If these members strike you as eerily familiar, you're right: they are typically used in Razor views. Both members are typed as object so they are generic enough to be used in all sorts of parsers.

 

That's it for the Driver for now. Next we will need to implement the MessageTemplatePartHandler, the editor templates and place the shapes with Placement.info.

 

MessageTemplatePartHandler

MessageTemplatePartHandler is your typical ContentHandler responsible for adding a StorageFilter to the Filters collection. We will also use it to setup the LazyField we defined in MessageTemplatePart. To learn more about LazyFields, check out http://www.skywalkersoftwaredevelopment.net/orchard-development/lazyfield-t.

Let's see how it looks:

Handlers/MessageTemplatePartHandler.cs:

using Orchard.ContentManagement.Handlers;
using Orchard.Data;
using Orchard.Environment.Extensions;
using Skywalker.Messaging.Models;
using Skywalker.Messaging.Services;
 
namespace Skywalker.Messaging.Handlers {
    [OrchardFeature("Skywalker.Messaging")]
    public class MessageTemplatePartHandler : ContentHandler {
        private readonly IMessageTemplateService _messageTemplateService;
 
        public MessageTemplatePartHandler(IRepository<MessageTemplatePartRecord> repositoryIMessageTemplateService messageTemplateService) {
            _messageTemplateService = messageTemplateService;
            Filters.Add(StorageFilter.For(repository));
            OnActivated<MessageTemplatePart>(PropertyHandlers);
        }
 
        private void PropertyHandlers(ActivatedContentContext contextMessageTemplatePart part) {
            part.LayoutField.Loader(x => part.Record.LayoutId != null ? _messageTemplateService.GetTemplate(part.Record.LayoutId.Value) : null);
            part.LayoutField.Setter(x => { part.Record.LayoutId = x != null ? x.Id : default(int?); return x; });
        }
    }
}

 

Were basically doing 2 things here:

  1. We inject an IRepository of MessasgeTemplatePartRecord and construct a StorageFilter for it which we add to Filters. A filter in this context is a sort of reusable piece of ContentHandler. Orchard comes with 7 implementations of them out of the box, one of them being the StorageFilter, which get invoked when content needs to be loaded and saved.
  2. We add a pointer to a function called PropertyHandlers to a method called OnActivated of MessageTemplatePart. The OnActivated<TPart> method is invoked whenever a content item is being instantiated whose type has TPart attached (MessageTemplatePart in our case). This is the perfect place for us to do any part specific initialization, such as setting up the LayoutField lazy field. The implementation is straighforward: the loader is setup to get the template using the injected IMessageTemplateService, and the setter is setup to update the LayoutId of the part's record.

 

MessageTemplate Editor Template

In this part we will use a very simple control to render the template editor: it's called a TextArea. In the next part, we will turn this into a much better looking editor using CodeMirror and even implement a Preview pane.

For now we will add the following markup to MessageTemplate.cshtml:

 

Views/EditorTemplates/Parts/MessageTemplate.cshtml:

@model Skywalker.Messaging.ViewModels.MessageTemplateViewModel
@{
    Style.Include("Common.css""Common.min.css");
    Style.Include("TemplateEditor.css""TemplateEditor.min.css");
}
<fieldset class="message-template-editor">
    <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 tokenized" })
        @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="editor-field">
        @Html.LabelFor(m => m.TextT("Template"))
        @Html.TextAreaFor(m => m.Textnew { @class = "template-editor"})
        @Html.ValidationMessageFor(m => m.Text)
        <span class="hint">@T("The template of the message")</span>
    </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>

 

Nothing fancy going on here, except maybe for this line:

@Html.EditorFor(m => m.LayoutIdnew { Model.Layouts })

 

We're using the Html.EditorFor method to render an editor template for the LayoutId property, and passing in a list of Layouts (which was build by our driver). If you recall, the MessageTemplateViewModel class decorated the LayoutId property with a UIHintAttribute. The EditorFor HTML helper will use the specified name as the template to render an editor for this property. We'll create this template after we created the stylesheets: Common and TemplateEditor. If you have the Web Essentials 2012 extension installed, you'll be able to use LESS:

 

Styles/Common.less:

.editor-field {
    margin-bottom1em;    
}

 

Styles/TemplateEditor.less:

.template-editor {
    height400px;
}

 

 

If you're unfamiliar with LESS and the Web Essentials 2012 extension: LESS is basically a higher level language on top of CSS, which allows nicer syntax, variables, mixins, nesting and much more. Right now we aren't using any of that, but we will use some LESS constructs in later parts.

The Web Essentials 2012 extension provides us with syntax coloring, intellisense and generates both a a css and a minified css file for us, which is what we will be actually including with the output.

As you will have noticed, we are using the overloaded version of Style.Include that takes two arguments: an "debug" version of a stylesheet and a "release" version of it. That means that when we set debug="false" in web.config, our application will be compiled in release mode, which will cause Orchard to automatically include the minified stylesheet. Nice!

 

TemplateLayoutPicker

We decorated the LayoutId property of the MessageTemplateViewModel with a UIHintAttribute:

[UIHint("TemplateLayoutPicker")]
public int? LayoutId { getset; }

 

This enables us to customize the editor template being used for that specific property. In our case, we want to render a list of available Layout Templates. We already prepared that list from the driver, so all we have to do now is implement the TemplateLayoutPicker editor template. Just to be clear, this is not an Orchard specific editor template; it's just a regular MVC editor template, being leveraged by the Html.EditorFor helper method in MessageTemplate.cshtml.

Views/EditorTemplates/TemplateLayoutPicker.cshtml:

@using System.Linq
@using Skywalker.Messaging.Models
@{
    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 });
}
@Html.DropDownList(""options"")

 

The template copies the Layouts passed via the ViewBag into a local variable and projects them to a list of SelectListItem objects which in turn are used by the Html.DropDownList helper method. All basic MVC stuff.

 

Placement

Finally, we need to create a Placement.info file to specify where our editor shape is to be rendered.

 

Placement.info:

<Placement>
  <Place Parts_MessageTemplate_Edit="Content:0" />
</Placement>

 

That's it for coding! Let's see what we've got so far.

 

Enabling the module

 

Creating a Master Template

Enabling the module will create a new content type called Email Template.

Let's create a Master Template (by checking the "This is a Layout" option) and a template that references this master:

 

Creating an Email Template that references a Master Layout

 

Summary

In this part, all we really did is setup a migration that creates a table, defines a part and a type, implemented a driver, a handler, a service and some templates. Basic stuff, for sure, but this provides us with a solid groundwork for the next parts.

In the next part, we will actually implement the parsing logic and a preview pane and enhance the code editor with CodeMirror!

 

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

 

Read Part 2

2 comments

  • ... Posted by giannis Posted 11/07/2014 07:42 AM

    another great article . Thanks sipke

  • ... Posted by Tony Posted 07/30/2015 08:52 AM

    Great Post! Thank you for share. ps skywalker can not visit.

Leave a comment