Alenka's profileSitecore ExperienceBlogLists Tools Help

Sitecore Experience

Alenka

Occupation
Location
Interests
June 06

Mandatory workflow comments

In the previous post I described how you can enhance your workflows with dialogs which appear after a command is executed. This time I’ll explain how you can make workflow comments mandatory. While we can’t ensure our users will actually enter a meaningful comment, we can certainly force them to enter one. This can be useful for ‘reject’ commands where a user should specify the reason for rejection.

Instead of modifying the default Sitecore’s template for a workflow command, let’s start by creating a custom template which inherits from the default one. The template will inherit all fields from the base template, so our job is to add a checkbox field with which we can specify that the comment is mandatory.

enhanced command template

So now you’re ready to create your workflow. If you need to add a command which requires a mandatory comment to your state, simply use the enhanced command template we just created and tick the ‘Mandatory’ checkbox.

enhanced workflow command

Of course, next step is to customize how workflow commands work. If you have a closer look at /App_Config/Commands.config file, you’ll find this line:

<command name="item:workflow" type="Sitecore.Shell.Framework.Commands.Workflow,Sitecore.Kernel" />

So what we need to do is implement a custom class, which does everything that the default class does and additionally to that, checks if the comment should be mandatory. Here’s a hint, use Reflector and an add-in called File Disassembler to create a source copy of the above class.

In the Run method, find a line of code, that look something like that (P.S. it’s not a big class, so it’s easy to find):

    if ((!isPostBack && ui) && !suppresscomment)
    {
        SheerResponse.Input("Enter a comment:", "");
        args.WaitForPostBack();
    }

and replace it by something like that:

    if ((!isPostBack && ui) && !suppresscomment)
    {
        string commandID = args.Parameters["commandid"];
        Item workflowCommand = Client.ContentDatabase.Items[ID.Parse(commandID)];
        //check if comment is mandatory
        CheckboxField mandatoryField = workflowCommand.Fields["mandatory"];
        if (mandatoryField != null && mandatoryField.Checked)
        {
            SheerResponse.Input("Enter a comment:", 
string.Empty,
@"\w+",
"Comment is mandatory",
int.MaxValue); } else { SheerResponse.Input("Enter a comment:", ""); } args.WaitForPostBack(); }


If a user doesn’t provide comment, he’ll get notified by alert:

comment is mandatory alert Once the user clicks OK button on the alert, he’ll be returned to a prompt to enter comment.

The above described approach will work when a workflow command is executed from Content Editor application. However, commands in the Workbox application work a bit differently because there are more options available. User can execute commands on individual items, on selected items or all items in a certain state. Therefore you’ll have to have a look into WorkboxForm and customise it with a similar approach.

Enjoy and check back regularly to read more tips from  the Sitecore kitchen.

May 26

Workflow commands with dialogs

You can implement virtually any workflow requirement with Sitecore’s workflow engine. It’s simple, easy to understand, easy to set up and comes with a set of common out-of-the box actions such as send email, auto-publish, perform validation and so on.

If these don’t suffice and you need to implement an even more sophisticated workflow, you can of course do so. For example, a pretty common requirement is to set certain field values as part of the workflow command. I have content management fields in mind. I would like to demonstrate how you can display intuitive dialog to your users and allow them to set a field value. For this demo, let’s assume we want to pop up a dialog that prompts a user to choose a review date when approving an item.

What you need to do is implement a custom workflow action and add it to the desired command on which you want the dialog box to appear. I want Set review date dialog to appear when user approves content item.

image

On the action item, specify C# class which implements the logic.

image

The class needs to implement Process method which is called when workflow command is executed. This method should contain logic that will pop up a modal dialog.

public class SetReviewDateCommand : DialogForm
{
public void Process(WorkflowPipelineArgs args)
{
//open a dialog that prompts user to enter a custom Review Date.
UrlString url = new UrlString(UIUtil.GetUri("control:SetReviewDateDialog"));
url.Add("ItemID", args.DataItem.ID.ToString());

Sitecore.Context.ClientPage.ClientResponse.ShowModalDialog(url.ToString(), "370px", "170px");
}
}

 

Okay, so now we need to create SetReviewDateDialog. Easiest way of doing it is to open Developer Center and create new XML Control.

image

By default the new XML control will be placed under /layouts folder, so you might want to move it into the same folder where your class resides. It’s a good idea to place XML controls under one of the folder listed under the following setting in web.config:

    <controlSources>
<
source mode="on" namespace="Sitecore.Web.UI.XmlControls" folder="/sitecore/shell/override" deep="true" />
<
source mode="on" namespace="Sitecore.Web.UI.XmlControls" folder="/layouts" deep="false" />
<
source mode="on" namespace="Sitecore.Web.UI.XmlControls" folder="/sitecore/shell/controls" deep="true" />
<
source mode="on" namespace="Sitecore.Web.UI.XmlControls" folder="/sitecore/shell/applications" deep="true" />
<
source mode="on" namespace="Sitecore.Web.UI.XmlControls" folder="/sitecore modules" deep="true" />
<
source mode="on" namespace="Sitecore.Web.UI.HtmlControls" assembly="Sitecore.Kernel" />
<
source mode="on" namespace="Sitecore.Web.UI.WebControls" assembly="Sitecore.Kernel" />
<
source mode="on" namespace="Sitecore.Shell.Web.UI.WebControls" assembly="Sitecore.Kernel" prefix="shell" />
<
source mode="on" namespace="Sitecore.Shell.Applications.ContentEditor" assembly="Sitecore.Kernel" prefix="content" />
<
source mode="on" namespace="Sitecore.Shell.Web.Applications.ContentEditor" assembly="Sitecore.Kernel" prefix="shell" />
<
source mode="on" namespace="Sitecore.WebControls" assembly="Sitecore.Kernel" />
<
source mode="on" namespace="System.Web.UI.WebControls" assembly="System.Web" prefix="asp" />
<
source mode="on" namespace="System.Web.UI.HtmlControls" assembly="System.Web" prefix="html" />
<
source mode="on" namespace="Sitecore.Web.UI.Portal" assembly="Sitecore.Kernel" />
<
source mode="on" namespace="ComponentArt.Web.UI" assembly="ComponentArt.Web.UI" prefix="ca" />
</
controlSources>

If the XML control resides in a folder which is not listed in the above setting, you will need to add a control source setting.

Next step is to define our control. To create a dialog with a date time picker we’d do something like that:

<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
<
SetReviewDateDialog>
<
FormDialog Icon="Applications/24x24/folder_document.png"
Header="Set Review Date Dialog"
Text="Set the review date."
OKButton="OK"
runat="server">

<
CodeBeside Type="Kaalen.WorkflowsSetReviewDateCommand,Kaalen.Workflows"/>
<
Border>
<
Label Header="Review date: " For="dtpReviewDate" Style="font-weight: bold;" />
<
DateTimePicker ID="dtpReviewDate" runat="server" ShowTime="false" />
</
Border>
</
FormDialog>
</
SetReviewDateDialog>
</
control>

 

Okay, so now all we need to do is save the data from the dialog, when user clicks OK button:

protected override void OnOK(object sender, EventArgs args)
{
base.OnOK(sender, args);

ID id = ID.Parse(Context.Request.QueryString["ItemID"]);
Item item = Sitecore.Context.ContentDatabase.GetItem(id);
item.Editing.BeginEdit();
item["review date"] = dtpReviewDate.Value;
item.Editing.EndEdit();
}

 

Sample C# project:


May 21

Making your business users really happy

Sitecore’s media library has some very nice features. For example for image files it attempts to automatically import file metadata. Our editors were impressed by it and have quickly tried the same trick with PDF and Word documents.

Hmm… why doesn’t this work they asked. It would be really nice if it did.

A bit of investigation how this feature works for images and some help from Sitecore’s excellent support team gave me an inspiration to implement a prototype for file documents as well. First on my list was tackling PDF documents. Rather than re-inventing the wheel I decided to use an open source .NET library which allows developers to work with PDF documents - iTextSharp.

Then I simply had to implement a class which inherits from Sitecore.Resources.Media.Media and override the following two methods:

   1:  protected override void UpdateMetaData(MediaStream mediaStream)
   2:  {
   3:      base.UpdateMetaData(mediaStream);
   4:      if (!mediaStream.AllowMemoryLoading)
   5:      {
   6:          Tracer.Error("Could not update PDF meta data as the PDF is larger than the maximum size allowed for memory processing. Media item: {0}", mediaStream.MediaItem.Path);
   7:      }
   8:      else
   9:      {
  10:          PdfReader reader = null;
  11:          try
  12:          {
  13:              reader = new PdfReader(mediaStream.Stream);
  14:              if (reader == null)
  15:              {
  16:                  return;
  17:              }
  18:              Item innerItem = this.MediaData.MediaItem.InnerItem;
  19:              using (new EditContext(innerItem, SecurityCheck.Disable))
  20:              {
  21:                  innerItem["title"] = reader.Info["Title"].ToString();
  22:                  innerItem["description"] = reader.Info["Description"].ToString();
  23:                  innerItem["keywords"] = reader.Info["Keywords"].ToString();
  24:                  foreach (object key in reader.Info.Keys)
  25:                  {
  26:                      Log.Info(
  27:                          string.Format("PDF file metadata: {0} = {1}", 
  28:                                 key,
  29:                                 reader.Info[key]),
  30:                          this);
  31:                  }
  32:              }
  33:          }
  34:          catch (Exception ex)
  35:          {
  36:              Log.Warn("Exceptions occured while reading uploaded PDF file metadata.", ex, this);
  37:          }
  38:      }
  39:  }

 

Next step is of course configuring Sitecore to use this class when uploading PDFs. We could either do that directly in web.config but there is a much better way. Since Sitecore 6 we have configuration include files and they are a very convenient way of altering or enhancing Sitecore’s default configuration and providing you with a peace of mind when it comes to future upgrades. Something like that…

   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
   3:    <sitecore>
   4:      <mediaLibrary>
   5:        <mediaType name="PDF file" extensions="pdf">
   6:          <prototypes>
   7:            <media type="Kaalen.Extras.PdfMedia, MRInternet" />
   8:          </prototypes>
   9:        </mediaType>
  10:      </mediaLibrary>
  11:    </sitecore>
  12:  </configuration>

Sample PdfMedia class implementation: PdfMedia.cs

If you’re simply looking for a quick solution, include the above file into your solution, put the configuration include file into /App_Config/Includes folder, download the iTextSharp library and place the dll file into /bin directory. Then compile and enjoy… until the users ask you to implement something similar for Word documents :-)

May 17

The pain of pasting Word documents

Content migration on our project has started and we ran into a nasty issue with pasting Word documents into Rich Text Editor. Sure enough, there are special buttons in RTE which partially clean pasted text but in my opinion they’re not nearly good enough. Namely, these buttons are Paste from Word and Paste from Word cleaning fonts and styles. I’m happy that headings are transformed into HTML equivalents, e.g. H1, H2 etc. But bulleted and numbered lists are a major issue for our project.

I have decided to come up with a decent solution for these problems and have started investigating. HTML Tidy was the first thing that popped into my mind. I quickly downloaded the necessary assemblies and I had to be a bit picky in doing that. At home I’m working on a 64-bit system and it seems that registering COM components in that case is a bit trickier. To avoid the unnecessary mangling I used my best friend Google to find a ready-to-use .NET wrapper. To my disappointment, HTML Tidy does a good job at cleaning Word documents but again it does not really transform it properly.

I continued by trying a few other tools and even online services and none was up to my standards.

So I’m back to square zero and I decided to tackle javascripts for RTE. RTE used in Sitecore is Telerik’s RADEditor by the way. This is where I am fully aware that the lack of this particular functionality isn’t Sitecore’s fault but I think Sitecore could do a bit more to push Telerik to improve in this area. Or maybe even consider replacing RADEditor with a different control. I can whinge on Telerik’s forums but even if my cries have an effect, solution is months away for sure.

So the plan is to write additional Javascript functions to get the desired result when it comes to bulleted and numbered lists. I already shiver at the thought of writing nasty regular expressions but heck, if that’s the way it has to be, I’ll do it. In the mean time… hint hint… I have actually found a little bug in Telerik’s javascript.

I’ll publish my work once I’m done as I’m 100% sure lots of others will find it extremely useful.

May 14

Mysterious duplicated languages

A while ago I implemented a custom data provider for Sitecore. We wanted our content editors to be able to choose a content owner from a dropdown or other similar control. We already had standard ASP.NET membership provider tables stored in the core database so I only had to get them displayed in content tree. After that was achieved, I could simply set the tree node which held the user items as a source on the desired template field.

It was quite easy to implement the provider and configure that. I only implemented a read-only provider since I didn’t want content editors to be able to modify user information. Later on in the project we plan to improve that data provider and add a custom field control which will enable content editors to search for users by their name, department etc.

Since my provider seemed to work well I haven’t paid much attention to it afterwards, until we started showing off our work to business analysts and content migration team. They quickly noticed we had a “duplicate” languages problem in the content editor. Hmm… I was scratching my head and trying to figure out where those duplicate languages came from. I have also played a bit with the languages and added a custom one, then deleted it and so on. Initially I thought my language problem was somehow caused by that.

I used reflector.net to explore what could possibly be happening and I quickly found out that DataProvider implementations return a collection of 2 default languages (en and en-US) if language provider isn’t disabled in the implementation of the provider.

So if you’re planning on implementing a custom data provider you can learn a bit from my lesson. Read the documentation and instructions on SDN and explore  the DataProvider class thoroughly to avoid such mysteries.

 

Custom HTML

hit counter for blogger