Sitecore Custom Workflow Email Action

Workflows are one of the most powerful tools in the Sitecore toolkit. An interesting request we recently had was to build a custom workflow action that will send email to a different mailing group/groups based on the location and the language of the item moved to next step of the workflow. At first it seemed like a complex task but as in many cases РSitecore is pretty extensible. In this blog post I will cover the simplest possible implementation where we will just send the email to a different mailing group or email. The build should be pretty extensible, so feel free to extend based on it. There are three main steps required for the component implementation:

  1. Create set of templates that will allow the users to control the rules.
  2. Create a new save action template that will build on top of the existing email action.
  3. Code it ūüôā

Rule Engine (Kind of :))

For the rule engine we will need 2 templates Рone folder template will just store our rules and one rule template. The rule item should give the administrators a chance to pick a start path(from where the items will count as a members of the section) and a possibility to pick languages in which the approvers should be able to move the the item to the next step of the workflow, and finally a list of emails. To be consistent, we are going to add the templates to /sitecore/templates/System/Workflow

So here is an example of how the rule template should look like:

Rule Template

The next decision we have to make is where to place the rules in the content tree. I usually don`t like adding items to the /sitecore/system node if it is not absolutely necessary, but it seems like a good location to place them. Another good option is to place them under the settings node of the website (if there is one). In this example I will place them under the /sitecore/system node like this:

Workflow Rules

Save Action Template

The EmailActionEx should be similar to the existing sitecore/templates/System/Workflow/Email action. The only addition is adding our custom parameter, so the user will be able to pick rules for the action. The save action should look like this:

Email Action Ex

Keep in mind that the Rules DataSource might vary based on where you decided to place your rules folder.

Code Time ! ūüôā

When coding the custom action we need to make sure that we keep the existing token functionality of the default functionality of the existing save action (like tokens etc). Snippet of the source code can be found below:


using System;
using System.Collections.Generic;
using System.Linq;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using System.Net.Mail;
using Sitecore.Globalization;
using Sitecore.Workflows.Simple;
 
namespace Sitecore.Workflows.Web.Workflows
{
    public class SendEmailExAction
    {
        public void Process(WorkflowPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
 
            ProcessorItem processorItem = args.ProcessorItem;
 
            if (processorItem == null)
            {
                return;
            }
 
            Item innerItem = processorItem.InnerItem;
 
            string fullPath = innerItem.Paths.FullPath;
 
            List<string> recipents =
                GetItems(innerItem, "rules")
                    .Where(x => ShouldSendEmail(args.DataItem, x))
                    .SelectMany(x => x.Fields["Emails"].Value.Split(';'))
                    .ToList();
 
            if (recipents.Any())
            {
 
                string from = GetText(innerItem, "from", args);
                string mailServer = GetText(innerItem, "mail server", args);
                string subject = GetText(innerItem, "subject", args);
                string message = GetText(innerItem, "message", args);
 
                Error.Assert(from.Length > 0, "The 'From' field is not specified in the mail action item: " + fullPath);
                Error.Assert(subject.Length > 0,
                    "The 'Subject' field is not specified in the mail action item: " + fullPath);
                Error.Assert(mailServer.Length > 0,
                    "The 'Mail server' field is not specified in the mail action item: " + fullPath);
 
                MailMessage mailMessage = new MailMessage();
 
                mailMessage.From = new MailAddress(from);
                mailMessage.Subject = subject;
                mailMessage.Body = message;
 
                foreach (string recipent in recipents)
                {
                    mailMessage.To.Add(new MailAddress(recipent));
                }
 
                SmtpClient client = new SmtpClient(mailServer);
 
                try
                {
                    client.Send(mailMessage);
                }
                catch (Exception ex)
                {
                    Log.Error("EmailExAction Threw An Exception", ex, this);
                }
            }
        }
 
        ///

<summary>
        /// Gets the text.
        /// 
        /// </summary>


        /// <param name="commandItem">The command item.</param><param name="field">The field.</param><param name="args">The arguments.</param>
        /// <returns/>
        private string GetText(Item commandItem, string field, WorkflowPipelineArgs args)
        {
            string text = commandItem[field];
            return text.Length > 0 ? ReplaceVariables(text, args) : string.Empty;
        }
 
        ///

<summary>
        /// Replaces the variables.
        /// 
        /// </summary>


        /// <param name="text">The text.</param><param name="args">The arguments.</param>
        /// <returns/>
        private string ReplaceVariables(string text, WorkflowPipelineArgs args)
        {
            text = text.Replace("$itemPath$", args.DataItem.Paths.FullPath);
            text = text.Replace("$itemLanguage$", args.DataItem.Language.ToString());
            text = text.Replace("$itemVersion$", args.DataItem.Version.ToString());
            return text;
        }
 
        private bool ShouldSendEmail(Item dataItem, Item ruleItem)
        {
            return IsItemMatch(dataItem, ruleItem) && IsLanguageMatch(dataItem, ruleItem);
        }
 
        private bool IsItemMatch(Item dataItem, Item ruleItem)
        {
            Item ruleStartItem = GetStartItem(ruleItem);
 
            return ruleStartItem == null || IsAncestor(dataItem, ruleStartItem);
        }
 
        private bool IsLanguageMatch(Item dataItem, Item ruleItem)
        {
            List<Language> languages = GetLanguages(ruleItem).ToList();
 
            return !languages.Any() || languages.Contains(dataItem.Language);
        }
 
        private IEnumerable<Language> GetLanguages(Item ruleItem)
        {
            MultilistField selectedLanguages = ruleItem.Fields["Languages"];
 
            if (selectedLanguages != null && selectedLanguages.TargetIDs.Any())
            {
                return selectedLanguages.GetItems().Select(x => Language.Parse(x.Name));
            }
 
            return Enumerable.Empty<Language>();
        }
 
        ///

<summary>
        /// Gets the items
        /// </summary>


        /// <param name="commandItem"></param>
        /// <param name="field"></param>
        /// <returns></returns>
        private IEnumerable<Item> GetItems(Item commandItem, string field)
        {
            MultilistField rules = commandItem.Fields[field];
 
            if (rules != null && rules.TargetIDs.Any())
            {
                return rules.GetItems();
            }
 
            return Enumerable.Empty<Item>();
        }
 
        private Item GetStartItem(Item ruleItem)
        {
            ReferenceField startPathField = ruleItem.Fields["StartPath"];
 
            if (startPathField != null && startPathField.TargetItem != null)
            {
                return startPathField.TargetItem;
            }
 
            return null;
 
        }
 
        private bool IsAncestor(Item currentItem, Item ancestor)
        {
            if (currentItem.ID == ancestor.ID)
            {
                return true;
            }
 
            if (currentItem.Parent != null)
            {
                return IsAncestor(currentItem.Parent, ancestor);
            }
 
            return false;
        }
    }
}

In the code we need to check if the item matches both rules for language and location. If one or more of the rules are matched – we send the email to the corresponding emails. Otherwise we skip them. If there is no match – no email is sent.

And that is it ! Now we have our custom email action that can send items based on the item location and the item language !

The Sample Project can be found on BitBucket.

For people without TDS the package can be downloaded separately from here: SitecoreWorkflows-1.0.zip

Happy Workflowling !

The Amazing World of RAZL Part 2 – Script Like A Boss

It has been almost a year from my first post about¬†The Amazing World of Razl¬†and the second part¬†finally found its place in the posts pipeline. In this post I am going to dig deeper into one of the most amazing and useful Razl features – Scripting. Also for the brave ones who reach the end of the article there will be a sneak peak of the new Razl version! ūüôā

First, let‚Äôs go through a real life scenario that every single Sitecore developer faced at some point. Many times we find ourselves slammed with bugs, which are caused by differences in content in our QA and Production Instances. So every single time we need to explain that our QA instance is not up to date with the latest Production content and that it is normal to have these issues. After that the frequent scenario is – “Can you please update the content, so we can regression test feature X”. Following that, we need to go to our Production CM, export a content package, and install it on our QA instances. The¬†huge problem, with this, is that it happens pretty frequently. Especially on big projects with many content dependencies (i.e. I want to test the search boost I requested but I get only¬†2 search results in total). This is the case in which the Razl Scripting mode shines.

Razl scripts are actually a fairly simple xml files which can be executed from the command line. They have 2 main components – connections and operations.

Connections

Connections are actually a way to describe a Sitecore Instance that Razl needs to connect to. The basic XML for Razl looks like this:




<connection name="" readOnly ="" install="" preset="">



<url></url>



<accessGuid></accessGuid>



<database></database>



<path></path>



</connection>



So let’s explore what the different parameters and attributes do:

  • name¬†– This is the main identifier that is going to get used by the operations to determine the Instance¬†they are going to execute on.
  • readOnly¬†– The possible values are true and false. Specifies if the connection is read only, i.e. if we try to execute a write command on a read only connection – it will not get executed. It is a good practice in your scripts to set your connection to read only.
  • install– The possible values are again true or false. Specifies if the script should install the Razl Service¬†if it is not already installed. If set to true – the path parameter¬†must be specified.
  • url–¬†Used to define the url of the Sitecore Instance we want to connect to.
  • accessGuid– Defines the access Guid which is used by Razl¬†to access the Sitecore Instances. If Razl is already installed on the Sitecore Instance – the Guid can be obtained by reading it`s value from the web.config defined in [Webroot]\_CMP.
  • database– The database which is going to be used by the connection. Nothing fancy here.
  • path–¬†The UNC path to the Sitecore instances. It is used only when you want to install the Razl¬†Service¬†on the server. My recommendation is to preinstall the Razl¬†Service on all servers you want to script on, so you won`t have to expose your servers file systems.
  • preset– If you already have the connection in the RAZL desktop application and you are too lazy to write some xml – you can use the preset property to pass the name as it is in your RAZL application. I usually advice against that, because you cannot be sure which presets are actually installed on the server and in most cases you won`t be the only person using the script.

There is no upper limit on how many connections you can define and naturally, you should have at least 2 connections defined in the script Рbecause you are going to use it on at least 2 Instances (or 2 Databases of a single instance).

Operations

Operations are the way of script telling RAZL what needs to be executed. The basic operation XML looks like this:




<operation name="" source="" target="">

<parameter name="param">

<value></value>

...

</parameter>

</operation>



So let’s take a look at what the different attributes do:

  • source– The name of the connection you want to use for sourcing Sitecore Instance/Database Combo (Copy¬†From :))
  • target– The name of the connection you want to use for targeted Sitecore Instance/Database Combo (Copy To¬†:))
  • name– Name is actually used for the operation types – CopyItem, CopyAll and CopyHistory. I will cover what each of the 3 operation types do later in this post as we need to get familiar with the parameters first ūüôā

Every operation can have one or more parameters. All parameters have a name attribute, every operation type has a list of supported parameter names. Parameters themselves can be single valued and multi-valued.

Here is an example of a

  1. Single valued parameter xml:

<parameter name="">[value]</parameter>

  1. Multi-valued parameter



<parameter name="">

<value>[value]</value>

<value>[value]</value>

.....

</parameter>



As different operation types accept different types of parameters, I will describe them in a separate section.

Operation Types

Copy Item Operation

Base Syntax:




<operation name="CopyItem" source="" target="">



<parameter name="itemId"></parameter>



</operation>



Functionality:

The Copy Item operation copies a single item from the source to the target database. It requires one parameter – itemId which is the ID of the item in the source database. Nothing fancy here.

Copy All Operation

Base Syntax:




<operation name="CopyAll" source="" target="">



<parameter name="itemId"></parameter>



<parameter name="overwrite"></parameter>



</operation>





Functionality:

The Copy All operation copies an item and its descendants from the source to the target database. It requires 2 parameters РitemId which is the ID of the starting item in the source database from which the copying starts and overwrite is a parameter which determines what to do with items which are present in the target database, but are missing in the source database, i.e. if the setting is set to true and there is an item in the target database that is not present in the source Рit will get deleted, if the setting is set to false Рthe item will be skipped.

Copy History Operation

Base Syntax:


<operation name="CopyHistory" source="" target="">



<parameter name="from"></parameter>



<parameter name="to"></parameter>



<parameter name="recycle"></parameter>



<parameter name="include">



<value></value>



</parameter>



<parameter name="exclude">



<value></value>



</parameter>



</operation>



Functionality:

I am sure many of you are familiar that Sitecore comes with a very powerful History Engine. The history engine uses a SQL Table to store all events that happened to an item. The history engine is enabled by default in the recent releases of Sitecore, before that you needed to enable that manually from the web.config.

The Razl copy history operation will read the actions from this table in the source database which occurred in a certain time frame and execute them over the target database. As this operation is more risky:) it accepts several parameters to give you better control over what is going on.

  • The “from” parameter specifies¬†the start time from which Razl should start reading events from the history table. The date needs to be in the following format:¬†yyyy-MM-ddThh:mm:ss. This is the only required parameter of the operation.
  • The “to” parameter specifies the end time when Razl should stop reading events from the history table. The date format is the same like the one for the “from” parameter:¬†yyyy-MM-ddThh:mm:ss. Parameter is optional and if it is not set every event up to now will be executed.
  • The “recycle”¬†is a boolean parameter and it specifies what Razl should do with deleted items. If set to true it will move them to recycle bin and if set to false it will remove them (they will just be deleted not recycled). I strongly advise of always setting this parameter to true as you cannot be sure¬†what the content editors might have done. The parameter is optional with a default value of true.
  • The other two parameters (includeand¬†exclude) are multi-valued and control which items (and their descendants) should be included/excluded when performing events.¬†Both parameters work with item paths and exclude has bigger power over include (in case a path is included in both parameters). If the parameters are left blank – all actions will get executed. If there is a single path¬†in the include – only actions over this item and its descendants will be executed.

I also created a small set of scripts which can be used for a good starting point when you are writing your own. The scripts can be found on BitBucket.

RAZL 2.6

And now – some spoilers! ūüôā

There is a new release of Razl coming soon! ūüôā

With the new release there are several new features, along with huge improvements to the scripting mode! Here are some of the highlights of the new release!

Lightning Mode

Razl 2.6 will introduce a new Lightning Mode for comparison. It increases comparison speed dramatically by using only the revision number when comparing!

Deep Compare

One of the most requested Razl features, deep compare will ship with the new release! Now you will be able to deep compare items without the need to go down the tree !

Scripting Mode Improvements

Razl 2.6 will introduce 5 new operation types:

  1. CopyVersion – For copying a specific version of an item.
  2. DeleteItem – For deleting items.
  3. MoveItem – For moving items in the content tree.
  4. SetFieldValue – For setting field values from a Razl Script.
  5. SetPropertyValue – For setting property values !

Also – a new parameter lightningMode will be introduced for CopyAll and CopyHistory operations to use the new lightning mode for quick comparison of huge trees.

Make sure you stay up to date with the new Razl Features by

Razl Website: http://razl.net/

Hedgehog Development Website: http://www.hhogdev.com/

Our Twitter: @hhogdev

Happy Razling !