Brian McKeiver's Blog

Anonymous Blog Post Notifications using Advanced Workflow in Kentico CMS–Part 1


Introduction

Kentico CMS has many different ways to handle content notifications in the form of email alerts. You can use the built in notification gateways, various subscription web parts, and/or dynamic newsletters. All of these components offer  great solutions to the various scenarios where you want to keep your users up to date on what is going on with the content on your website. Notice the key word I used in that last sentence, users. I am re-enforcing that point because all of the above mentioned components require that a subscriber be a full user account, password and all, in your Kentico website.

Kentico EMS helps build websites

Having a user who has a full account on the website works perfectly when it comes to an intranet or membership based web site, but doesn’t always work for someone who is just running a blog or basic company website. Sometimes the whole goal of a personal blog or brochure site is to actually capture an anonymous visitor’s email address without that visitor having to go through the hassle of creating a full fledged account. The less steps the better is usually the rule of thumb. So having a visitor perform a full registration may not be a good thing and could been seen as a barrier to entry.

In this two part mini blog post series I am going to show you how to leverage a few tools that Kentico CMS gives you, out of the box, to create a system to alert anonymous subscribers when a new blog post is created. The solution involves using the built in Newsletter module, Advanced Workflow, and a custom Workflow Action to tie it all together. One other small note, Kentico CMS Ultimate or Kentico EMS are required for all of this to work.

 

Background

For brevity I am going to assume you know how to use standard workflow in Kentico CMS and have a basic understanding of the Newsletter module in Kentico too. If you need to brush up your memory on either of those I recommend hitting up the respective documentation first and then coming back here. Another note, in this blog post I am using the scenario of sending out a content notification for a new blog post, but you could easily apply this to any other document type in the system.

 

The Setup

I am going to discuss each component of the full solution next. You don’t have to do everything exactly the same as I have laid it all out here, but I though this would be a helpful walkthrough as opposed to just putting out a download package with no real instructions.

 

Advanced Workflow

The main component of this solution is the workflow engine of Kentico CMS.  The whole idea here is that when a content editor publishes a blog post we let our subscribers know via email. The workflow below shows how we can accomplish this. When you create an advanced workflow you get three main steps, Edit, Published, Archived. These three starting steps represent which state your document is in when workflow is involved. But the power comes in when you drag and drop you own steps into the workflow. The most typical example of this is adding an approval step that just sends an email to an approver for review before being published.

Before I talk about the  steps of this workflow let me make one small note about one customization I have made to the document type to support my needs. I have added one field to the Blog Post document type in my Kentico site, named NotificationSent. That field is just a boolean field to hold the status of if we have sent notifications to our subscribers or not. The last thing that we want to do is to spam our subscribers every time we make a change to the document.

The first step of the Advanced Workflow is to check a condition. If the Blog Post’s NotificaitonSent field is true, then nothing special happens and the publish action just publishes the latest version of the document to the live web site. If NotificationSent is false, the workflow goes on to the next step where we actually generate a Newsletter Issue and send that issue to all of the subscribers via an email.  Finally the workflow sets the documents NotificationSent property to true to ensure we don’t send another one. All of this is out of the box functionality in Kentico CMS except for the one “Send Blog Post As Newsletter Issue” custom action I have created.

As far as the scope of the workflow goes, I have simply selected all nodes under my main Blog node in the content tree where the document type is CMS.BlogPost.

 

Kentico CMS Advanced Workflow

 

Notice that there are different colored steps in this workflow example. The brown step is the first non traditional step. Brown colored steps are Condition workflow steps. They allow you to specify a condition right through the UI of Kentico, no code needed.  The next two screen shots show how I have specified a condition on the document that is being passed through this workflow. By default a conditional workflow step has true and false cases, but you can actually add more cases if you want. It is pretty powerful. Clicking on the small pencil icon brings up the Workflow step properties dialog to configure the step.

The Condition Settings group is the most import property group here. The Condition property itself allows you to specify any macro condition that you want. You just have to click the pencil icon to edit the step, and the macro editor will appear. It’s very cool. You can see below that I have just specified the condition as “Document.NotificationSent != true”.

 

Kentico CMS Workflow Step Properties

 

Orange steps are action steps. There are quite a few built in ones, but you can also add your own. This ability really expands the posibilities of workflow in Kentico CMS. When you add your own custom step you can actually specify parameters. These parameters are normal Form Controls in Kentico CMS, and you build them just like fields in a document type or on-line form. I have created two parameters for our example. The first parameter allows the user to specify which Newsletter we are using for the workflow that we want the Issues to be generated in. The second parameter allows the user to pick which email template holds the structure and layout of the content of each issue.

 

Kentico CMS Workflow Step Properties Params

 

Custom Workflow Action

How did I build this custom workflow action ? Well let me show you. On the Actions sub tab of CMSSiteManager –> Development –> Workflows you can see all the built in action steps, and add your own. Clicking on the New Action button brings up the form shown below. You give the action a display name, code name, description, etc. etc., just like any object in Kentico CMS. The key is the Action configuration at the bottom of the form. This is where you tie the UI Action to your custom code. You can have a custom class in App_Code or use an external assembly. I have created a class in App_Code named SendBlogPostAsNewsletterIssueAction and linked it up like so:

 

Kentico CMS Custom Workflow Action

 

Custom Workflow Action Parameters

After defining the custom action, you can optionally specify parameters for the action step. This is how I accomplished allowing the user to choose which Newsletter we are using, and which email template the Newsletter Issue should get its structure and layout from. Again this is just like building a field on a document type.

 

Kentico CMS Custom Workflow Action Property

 

Kentico CMS Custom Workflow Action Property

 

 

Custom Code for the Workflow Action

Now the systems knows that we have a custom workflow action, but we still need to write our custom code to handle what we want to do. Like I mentioned above, I created a custom class file in the App_Code directory named SendBlogPostAsNewsletterIssueAction.cs. Technically, I have two classes in this file, but the first class, WorkflowActionModuleLoader, is just the module loader class that handles wiring up the custom class. This is a required class to have to register any custom code in the core engine of Kentico CMS, but it is nothing special.

A few things to note  about the code. Because we created two parameters on our custom workflow action we need register the same two parameters in our class file. The two public params, NewsLetterCodeName and NotificationEmailTemplateCodeName use the same method to get the value of what the user has selected from the UI. That method relies on GetResolvedParameter. This is a built in function that does the job for you as long as you use the right code name.

From there the main entry point of the code is the Execute method.  This is where you start. Also because we are inheriting from DocumentWorkflowAction, we have an automatic reference to the document, or node in this case that is being run through the workflow. We can reference it from the Node object. As you can see in the code, I look at that Node object, and create a an IssueInfo object to represent an issue of the Newsletter for the specific blog post that we are working with. I build the Issue content by using a MacroResolver, and then with one line of code, send out that Issue to all of the subscribers of the Newsletter. This is important because the system will asynchronously queue up 300 emails in the email queue if you have 300 subscribers and not slow down the publishing of the document while waiting for individual emails to be sent.

Below is the full source code to that file.

 

using System.Xml;
using CMS.DocumentEngine;
using CMS.GlobalHelper;
using CMS.WorkflowEngine;
using CMS.SettingsProvider;
using CMS.Newsletter;
using CMS.CMSHelper;
using CMS.EmailEngine;

/// 
/// Loader Module Class
/// 
[WorkflowActionModuleLoader]
public partial class CMSModuleLoader
{
    private class WorkflowActionModuleLoader : CMSLoaderAttribute
    {
        public override void Init()
        {
            ClassHelper.OnGetCustomClass += ClassHelper_OnGetCustomClass;
        }

        private void ClassHelper_OnGetCustomClass(object sender, ClassEventArgs e)
        {
            if (e.Object == null)
            {
                switch (e.ClassName)
                {
                    case "SendBlogPostAsNewsletterIssueAction":
                        e.Object = new SendBlogPostAsNewsletterIssueAction();
                        break;
                }
            }
        }
    }
}


/// 
/// Send Blog Post as a Newsletter Issue Advanced Workflow Action 
/// 
public class SendBlogPostAsNewsletterIssueAction : DocumentWorkflowAction
{
    private string newsLetterCodeName = null;
    private string notificationEmailTemplateCodeName = null;

    /// 
    /// CodeName of the On-line Marketing Newsletter  in CMSDesk -> On-line Marketing -> Newsletters
    /// 
    public string NewsLetterCodeName
    {
        get
        {
            if (newsLetterCodeName == null)
            {
                newsLetterCodeName = GetResolvedParameter<string>("NewsLetterCodeName", "");
            }

            return newsLetterCodeName;
        }
    }

    /// 
    /// CodeName of the Email Template in CMSSiteManager -> Administration -> Email Templates
    /// 
    public string NotificationEmailTemplateCodeName
    {
        get
        {
            if (notificationEmailTemplateCodeName == null)
            {
                notificationEmailTemplateCodeName = GetResolvedParameter<string>("NotificationEmailTemplateCodeName", "");
            }

            return notificationEmailTemplateCodeName;
        }
    }

    /// 
    /// Main starting point of the custom Workflow Action
    /// 
    public override void Execute()
    {
        if (Node != null)
        {
            // Create a new Issue on that Newsletter
            int issueID = CreateStaticNewsletterIssue();

            //Send it to all Subscribers 
            if (issueID > 0)
            { 
                // this acutally adds it to the queue and doesnt wait to send out to multiple email addresses
                // don't forget to enable: Send issues via e-mail queue on this Newsletter
                IssueInfoProvider.SendIssue(issueID);
            }

            using (CMSActionContext ctx = new CMSActionContext())
            {
                ctx.DisableAll();
                Node.Update();
            }
        }
    }

    /// 
    /// Creates a new Static Newsletter Issue from the NewsletterCodeName paramater
    /// 
    /// Id of newly created Issue
    public int CreateStaticNewsletterIssue()
    {
        // Create a new static issue 
        IssueInfo i = new IssueInfo();

        // Gets the newsletter to attach to that also has our subscribers
        NewsletterInfo newsletter = NewsletterInfoProvider.GetNewsletterInfo(NewsLetterCodeName, Node.NodeSiteID);
        
        if (newsletter != null)
        {
            // Get which post we are sending to our "Newsletter" subscribers
            string title = ValidationHelper.GetString(Node.GetValue("BlogPostTitle"), string.Empty);

            // Get the Newsletter Template for Notifications
            i.IssueSubject = string.Format("New Blog Post Notification - {0}", title);
            i.IssueNewsletterID = newsletter.NewsletterID;
            i.IssueSiteID = Node.NodeSiteID;
            i.IssueText = string.Format(@"{0}", GetCurrentEmailTemplateTextAsXml());
            i.IssueUnsubscribed = 0;
            i.IssueSentEmails = 0;
            i.IssueTemplateID = newsletter.NewsletterTemplateID;
            i.IssueShowInNewsletterArchive = false;

            // Saves the static issue
            IssueInfoProvider.SetIssueInfo(i);
        }

        return i.IssueID;
    }

    private string GetCurrentEmailTemplateTextAsXml()
    {
        string finalTemplateText = string.Empty;

        //Go get the corresponding Email Template from CMSSiteManager -> Administration -> Email Templates
        CMS.EmailEngine.EmailTemplateInfo template = EmailTemplateProvider.GetEmailTemplate(NotificationEmailTemplateCodeName, Node.NodeSiteID);
        if (template != null)
        {
                // Set resolver to the node that represents the blog post
                ContextResolver resolver = CMSContext.CurrentResolver;
                resolver.SetNamedSourceData("BlogPost", Node);
                resolver.EncodeResolvedValues = true;

                // Resolve the macros in the body of the email template
                finalTemplateText = resolver.ResolveMacros(template.TemplateText);
        }

        //escape it properly as xml because this goes in the content of the IssueText
        return HtmlToSafeXml(finalTemplateText);
    }

    private string HtmlToSafeXml(string Html)
    {
        //Let the XmlDocument do the dirty work
        XmlDocument doc = new XmlDocument();
        XmlElement node = doc.CreateElement("root");
        node.InnerText = Html;

        //for some crazy reason the CKEditor wouldnt properly escape / unescape the xml until I added the extra white space and took out the &
        //TODO: See if there is a better way to handle this ?
        return node.InnerXml.Replace(">", ">   ").Replace("&gt;", ">").Replace("&lt;", "<");
    }

}

 

 

To Be Continued

Keeping reading part two of Anonymous Blog Post Notifications using Advanced Workflow in Kentico CMS to get the rest of the story.