LibrarySites.Banner

Run Scheduled Agents Interactively in the Sitecore ASP.NET CMS

This blog post provides prototype code that you can use to add a command to the Administration group in the Control Panel in the browser-based desktop of the Sitecore ASP.NET web content management system (CMS) to invoke scheduled agents interactively. I developed this solution using Sitecore CMS 6.5.0 rev. 111230, and it should work in almost any version, but I did not test extensively on any version.

Introduction

In Sitecore, an agent is a type of process that you can schedule, typically to perform a background administrative function that could run for a long period of time, such as periodic publishing or file system cleanup. The blog post All About Sitecore Scheduling Agents and Tasks provides additional information about agents. I would occasionally find it convenient to initiate an agent interactively rather than waiting for the next recurrence defined for that agent in the Web.config file, for example when developing and testing an agent. I decided to extend the Control Panel available in the Sitecore browser-based desktop with a command that implements this feature. This current post might seem like a bit of a whirlwind for readers not experienced with various aspects of the system.

This blog post provides sample code, configuration, and information that you can use to achieve the following:

  • Implement a simple command (launch a wizard)
  • Extend an existing CMS user interface (add a command to a group of commands in the Control Panel)
  • Implement a wizard, which introduces Sitecore XML user interface design concepts and APIs (Application Programming Interfaces)
  • Retrieve configuration information from the /configuration/sitecore section of the Web.config file
  • Invoke a job
  • Invoke an agent

I honestly had little or no knowledge of half of the items in this list but was able to get this prototype working in a matter of hours. It generally takes significantly less time to develop something than it takes to explain how to develop that thing, and this blog post is certainly no exception. Note that command templates initiate commands, meaning that you can follow this general process to implement a command template that initiates a wizard to help the user create one or more items. For more about command templates, see The Sitecore Data Definition Reference and The Sitecore Data Definition Reference and The Sitecore Data Definition Cookbook.

Extending the Administration Group in the Control Panel

Sitecore uses much of the same technology to build its CMS user interfaces that you use to manage published web sites. To add a command to the Administration group in the Control Panel, follow this process in the Sitecore desktop:

  1. Click the database icon in the lower right corner, and then select core from the menu that appears. This sets the content database (the database with which CMS user interfaces interact) to the Core database, which is the database that controls the CMS user interfaces. The desktop refreshes, causing any open applications to disappear.
  2. Click the Sitecore button, and then click the Content Editor. This launches the Content Editor application within the desktop to edit items in the Core database that controls the CMS user interfaces.
  3. Navigate to the /sitecore/content/Applications/Control Panel/Administration item that defines the Administration group in the Control Panel.
  4. Insert an item using the Sitecore Client/Tasks/Task option data template that to add a command to the Administration group in the Control Panel. This data template appears in insert options as Task option, or you can duplicate one of the existing command definition items at this level and update its properties afterwards. For more information about insert options and instructions to use them, see The Sitecore Data Definition Reference and The Sitecore Data Definition Cookbook.
  5. Enter sharedsource:runagent for the value of the Click field in the Data section. When you click this command in the Administration group of the Control Panel, Sitecore initiates this command code, which you will later map to the class that implements that command. If you choose to use a different value for this field, remember to use that same value later when you add the corresponding /configuration/sitecore/commands/command element that defines this command in the Web.config file.
  6. Enter Run Agent for the value of the Header field in the Data section. This value appears in the Administration group in the Control Panel to initiate this command. Internationalization of the UI is not within the scope of this blog post.
  7. Optionally, you can sort the command definition item relative to the other commands in the Administration group of the control panel.
  8. Click the database icon in the lower right corner, and then select master from the menu that appears. This sets the content database to the Master database, which contains the work in progress and final versions of data for your managed web sites. The desktop refreshes, causing the Content Editor and any other applications to disappear.

Defining the Command

You need to map the command code entered previously to the class that implements that command. The /configuration/sitecore/commands/sc.include element in the /web.config file includes the /App_Config/Commands.config file, which maps the default commands to the default implementations, as if it appeared at that location in the /web.config file. For more information about Web.config include files, see the blog post All About web.config Include Files with the Sitecore ASP.NET CMS.

To map the sharedsource:runagent command code to the class that implements this command, you can do any of the following:

  • Add a /configuration/sitecore/commands/command element to the /web.config file.
  • Update the /App_Config/Commands.config file to include that <command> element.
  • Add an element to the /web.config file to explicitly include a config file that you create to contain that <command> element.
  • Create a Web.config include file in the /App_Config/Include subdirectory containing that <command> element for Sitecore to include automatically.

To keep the configuration simple and separate from the default, I suggest that you create the /App_Config/Include/Commands.config file containing the following:

<configuration>
  <sitecore>
    <commands>
      <command name="sharedsource:runagent"
        type="Sitecore.Sharedsource.Shell.Framework.Commands.System.RunAgent,assembly"/>
    </commands>
  </sitecore>
</configuration>

The name attribute of the /configuration/sitecore/commands/command element in this example should match the value you entered in the Click field of the command definition item you created previously. The value of the type attribute is a type signature that includes the namespace and class name that implements the command, a comma, and the assembly that contains that class. Note that you do not have to defined any namespaces (such as using xmlns:patch or xmlns:x) in this configuration file because the location of this element relative to other elements is irrelevant. You can download this Web.config include file with the code available at the end of this blog post. You can add additional commands to this file as you develop them.

Implementing the Command to Launch the Wizard

Implement the command that launches a wizard that lets the user select an agent to invoke and displays status information about that running process, in this example the Sitecore.Sharedsource.Shell.Framework.Commands.System.RunAgent class. While I should have investigated shared source projects and read the documentation pertaining to wizards, to get started with the code, I simply copied the Sitecore.Shell.Framework.Commands.System.AddLanguage class associated with the system:addlanguage command code by the by the /App_Config/Commands.config file, as I knew that command launches a wizard. I used Red Gate’s .NET Reflector to disassemble this existing command. One day I hope to blog about disassembly with Sitecore, which I often to find more beneficial than accessing the actual source code of the product. Note that under no circumstances does any Sitecore license agreement allow you to disassemble Sitecore in order to develop a competing product.

Implementing the Wizard User Interface

A wizard is a simple user interface dialog that helps a CMS user to accomplish a task, which consists of a sequence of pages that walk the user through a process. The standardized approach leads to a consistent user interface, which is perfect for me with sorely lacking markup, CSS, user interface, and user experience skills.

A wizard consists of two primary components:

  • An XML file that defines the wizard user interface, somewhat similar to a web form (.aspx file)
  • A code-beside file, which contains logic to manage that user interface, referenced by the /control/*/WizardForm element in the XML file, where the asterisk (*) character represents the unique identifier of your XML control

You can download the RunAgent.xml file and a code-beside that I used with the code available at the end of this blog post.

Sitecore manages a collection of presentation controls built from such XML files. It is important that the /control/* element in your XML file match the URL that you specify in the class that opens the wizard. For example, my command (Sitecore.Sharedsource.Shell.Framework.Commands.System.RunAgent) contains this line of code:

string url = new Sitecore.Text.UrlString(
  Sitecore.UIUtil.GetUri("control:RunAgent")).ToString();

Therefore, the XML file for this control should contain a /control/RunAgent element to define this wizard.

You can store your XML file in any subdirectory specified by the folder attribute of any /configuration/sitecore/controlSources/source element in the Web.config file for which the value of the mode attribute is on. One easy place to put this file is the /sitecore/shell/override subdirectory or any subdirectory of that subdirectory. The purpose of that subdirectory is to override default Sitecore user interface components, so it is not exactly appropriate for this purpose. A better solution involves another Web.config include file. I suggest adding the /App_Config/Include/ControlSources.config file containing the following:

<configuration
  <sitecore
    <controlSources
      <source mode="on" deep="true"
        namespace="Sitecore.Sharedsource.Web.UI.XmlControls"
        folder="/sitecore/shell/sharedsource" /> 
    </controlSources
  </sitecore
</configuration>

You can download this Web.config include file with the code available at the end of this blog post. The namespace attribute of the element instructs Sitecore to compile XML controls from this source into the specified namespace. The deep attribute of that element instructs Sitecore to iterate the files and subdirectories in the subdirectory specified by the folder attribute and add all contained XML controls to the list under management.

You may want to consider whether you to implement a single Web.config include file to contain this content as well as the content created previously in the /App_Config/Include/Commands.config file. Because you may use other shared source XML controls, I would not name such a file anything specific to this solution, such as RunAgent.config.

Next, create the /sitecore/shell/sharedsource/Applications/Administration/RunAgent subdirectory containing the RunAgent.xml file. Technically, you could create RunAgent.xml directly in /sitecore/shell/Sharedsource/Applications/Administration or any subdirectory of the /sitecore/shell/Sharedsource subdirectory. It is best to separate all files for each component into separate subdirectories, as you may find that some components require CSS, JavaScript, or other files that you might not to prefer to mix with files from other applications in the same subdirectory. I generally develop each new Sitecore component by copying something that exists. In this case, I looked at the following:

  • /Sitecore/Shell/Applications/Globalization/AddLanguage/AddLanguage.xml: Wizard to register a language populates a drop-down similar to the one from which the user will select an agent to invoke
  • /Application/Dialogs/Publish/Publish.xml: Wizard to publish displays information about a potentially long-running background process

The CodeBeside attribute of the /control/*/WizardForm element in such an XML file specifies the type signature of the code-beside file for the wizard. Each child element of that element defines a page of the wizard. When the user invokes the wizard, the pages appear to the user in the order that they appear in the XML file. In some ways, these XML elements work much like ASP.NET web forms (.aspx files) - they contain elements that map to dynamic controls that the wizard processing framework converts to markup at runtime and literal controls that the framework writes directly to the output stream. 

The first page in a wizard typically uses the <WizardFormFirstPage> element to provide a static overview of what the user can accomplish with the wizard, followed by some number of sibling <WizardFormPage> elements. With a little help, the wizard framework automatically displays Back, Next, Forward, Cancel, and other buttons where appropriate, which you can enable and disable as needed.

Agent Wizard Page 1

The first <WizardFormPage> element in this example contains a <Combobox> element (which functions like a drop-down list) with a value of SelectedAgent for the ID attribute. The code-beside contains a Sitecore.Web.UI.HtmlControls.Combobox named SelectedAgent and populates that element with the list of agents from which the user must select. Another <Combobox> enables the user to select a thread priority for the agent.

Agent Wizard Page 2

The second <WizardFormPage> element in this example contains a <Literal> element with a value of Status for the ID attribute. The code-beside contains a Sitecore.Web.UI.HtmlControls.Literal named Status and populates that literal with status information about the running agent.

Agent Wizard Page 3 

Most wizards end with a <WizardFormLastPage> element to indicate completion of the task conducted by the wizard. This example contains another element named Result and populates that literal to indicate whether the agent succeeded or failed.

Agent Wizard Page 4

Implementing the Wizard Code-Beside

In writing the code-beside, which inherits from the Sitecore.Web.UI.Pages.WizardForm class, one of the challenges I faced was to determine how to pass information from one page to the next, as properties set by one page were not available to the next page. Specifically, I needed to store a handle for the job used to invoke the agent so that I could display results on the . I eventually found that I could store those values base.ServerProperties, which is a System.Web.UI.StateBag that functions as a keyed collection. I assume I could alternatively have used a hidden variable in the HTML, but this solution was very easy, and what I saw in existing code. Interestingly, properties of controls in each page appear to persist between pages, and you can reference any such control from any of the pages in the wizard.

In the code-beside, I implemented the OnLoad() method to populate the drop-down list of available agents. The wizard framework calls this method each time it loads a page, such as when the wizard opens or when the user clicks Next or Back.

The wizard also calls the ActivePageChanged() method when the user moves from one page to another. Each <WizardFormFirstPage>, <WizardFormPage>, and <WizardFormLastPage> element has an ID attribute that the wizard framework passes to the ActivePageChanged() method to indicate the current location of the user in the wizard. In this example, if the user selects an agent in the first <WizardFormFirstPage>, they reach the second <WizardFormFirstPage> with a value of Running for the ID attribute. In that case, the ActivePageChanged() method invokes the agent. To determine the agent to run, the ActivePageChanged() method:

  • Retrieves the value of the SelectedAgent control from the Sitecore.Context.ClientPage.ClientRequest.Form collection
  • Disables the Next, Back and Cancel buttons (maybe someone else can implement cancellation for a running agent)
  • Retrieves the definition of the selected agent from the Web.config file using the Sitecore.Configuration.Factory.GetConfigNode() method
  • Creates an object of the type specified by that configuration element in the Web.config file
  • Determines the object of that type to invoke from the method attribute of that element in the Web.config file
  • Instantiates a Sitecore.Job.JobOptions object to contain properties of the Sitecore.Jobs.Job object that will invoke that method on the object that represents the agent (more about jobs later in this post)
  • Invokes the Sitecore.Jobs.Job and stores the handle of that job
  • Calls the Sitecore.Web.UI.Sheer.SheerResponse.Timer() method to instruct the wizard framework to call the CheckStatus() method in a few milliseconds to refresh the wizard with status information about the job

The CheckStatus() method is not part of the wizard framework, but something specific to wizards that invoke tasks that can be long-running, as agents can. The implementation in this example retrieves the job identified by the handle stored by the ActivePageChanged() method. If that job is no longer running, the ActivePageChanged() method sets the Result literal with a message that indicates job success or failure. If the job generated messages, it adds the last message generated to that status message. In some cases, agents may generate a small enough number of messages that you would want to present all of them, for example using an Sitecore.Web.UI.HtmlControls.Memo object that supports scrollbars (as used by the publishing wizard) instead of using a simple Sitecore.Web.UI.HtmlControls.Literal as I used. The CheckStatus() method then sets the Active property of the base class to LastPage, which causes the wizard to progress to the page with that ID, as processing has completed. When the wizard reaches the last page, it automatically re-enables the Back button and Cancel buttons (the latter now appears as Finish), and there is no Next button. If the user clicks the Back button, the wizard invokes the same agent again. If the job has not ended, the ActivePageChanged() method simply updates the Status literal with information about the running job and calls Sitecore.Web.UI.Sheer.SheerResponse.Timer() again to instruct the wizard framework to call the CheckStatus() again in a few milliseconds.

Agent Names

In the drop-down list of available agents, instead of showing the names of the classes and methods specified for each /configuration/sitecore/scheduling/agent element in the Web.config file, this wizard shows the value of the name attribute of those elements. This is useful for a few reasons:

  • The values of the name attributes can be user-friendly, where class and method names might not be
  • You can use the same class and method in multiple agents, typically with different configurations, so without cluttering the drop-down list with configuration information, that information alone could appear ambiguous
  • If you do not specify the name attribute for a /configuration/sitecore/scheduling/agent element, it does not appear in the drop-down list of available agents in the wizard

If the values of the name attributes of the /configuration/sitecore/scheduling/agent elements in the Web.config file are not unique, the wizard throws an exception (it also throws an exception of no /configuration/sitecore/scheduling/agent elements define the name attribute). Remember that the names Agent Smith and Agent Johnson are already taken (just kidding). The materials available for download at the end of this blog post contain a Web.config include file that adds the name attribute to most of the agents defined in the default /web.config file.

Updating Agents to Populate Job Information

Sitecore uses jobs to invoke agents. You can read more about jobs in the blog post All About Jobs in the Sitecore ASP.NET CMS. Because agents can run for any durration, it seemed appropriate to use jobs to run agents from the wizard UI, refresh the wizard while monitoring the progress of the job, and then transition to the final page of the wizard after job completion. This turned out to be remarkably easy; the code creates a job, stores its handle in base.ServerProperties, and polls the status of that job periodically.

If you maintain source code for agents that you intend to invoke with this wizard, you can update that code to provide status information to the wizard. If the Sitecore.Context.Job static property is not null, you can add a string to the Status.Messages collection of that object. If your agent processes some number of somethings, you can set the Status.Processed property of that object. For example:

if (Sitecore.Context.Job != null)
{
  Sitecore.Context.Job.Status.Messages.Add("message from agent");
  Sitecore.Context.Job.Status.Processed++;
}

Configuring Agents

This wizard invokes agents as the context user. If you need an agent to run as a specific user, you can use the configuration factory to specify that user to the agent. For more information about the configuration factory, see the blog post The Sitecore ASP.NET CMS Configuration Factory. Add a property such as the following to the agent:

public string UserName { get; set; }

Then wrap the existing code in the main method of the agent in a Sitecore.Security.Accounts.UserSwitcher:

Sitecore.Security.Accounts.User.User user = Sitecore.Context.User;
  
if (!String.IsNullOrEmpty(this.UserName))
{
  if (!Sitecore.Security.Accounts.User.Exists(this.UserName))
  {
    throw new Exception("User " + this.UserName + " does not exist.");
  }
  
  user = Sitecore.Security.Accounts.User.FromName(this.UserName, false);
}
  
Sitecore.Diagnostics.Assert.IsNotNull(user, "user");
  
using (new Sitecore.Security.Accounts.UserSwitcher(user))
{
  // move existing agent method body here
}

Then define the User property in the /configuration/sitecore/scheduling/agent element in the Web.config file:

<agent ...>
  <user>sitecore\admin</user>
  ...

 

If you need to prevent multiple concurrent instances of an agent, such as if Sitecore runs an agent while you use the wizard, note that the scheduling engine uses the name attribute of the /configuration/sitecore/scheduling/agent element as the name of the job if it exists, which is what the wizard always uses. The job manager automatically prevents multiple concurrent jobs of a single name. If the name attribute does not exist, the scheduling engine uses the namespace and class name specified by the type attribute of the /configuraiton/sitecore/scheduling/agent element, without the method name or any parameter values. 

Conclusion

Some of the benefits of using agents include:

  • Agents are very easy to write, and using the configuration factory, quite flexible.
  • You define agent configuration, or multiple configurations, only once, and invoke an existing configuration rather than reselecting configuration options through a user interface each time you invoke one.

One of the drawbacks of agents is that you cannot easily define parameters at runtime; you define them in the Web.config file.

I think the solution described in this blog post makes it easier to invoke agents, which should encourage their use. One of the nice things about this wizard is that you can even run agents that are disabled (the interval attribute for the /configuration/sitecore/scheduling/agent element in the Web.config file is 00:00:00). Other than the issue of the context in which the agent runs, it seems advantageous to define code and configuration that you can invoke both on a schedule and interactively. You can download this zip file that contains the following files that I used to implement this prototype:

  • /Website/App_Config/Include/AgentNames.config - Web.config include file that adds values for the name attribute to the default /configuration/sitecore/scheduling/agent elements in the /web.config file so that they appear in the wizard UI
  • /Website/App_Config/Include/Commands.config: Web.config include file that maps the sharedsource:runagent command code to the Sitecore.Sharedsource.Shell.Framework.Commands.System.RunAgent class used to implement the command that launches the wizard
  • /Website/App_Config/Include/ControlSources.config: Web.config include file that configures Sitecore to process XML controls in the /sitecore/shell/sharedsource subdirectory and its subdirectories
  • /Website/sitecore/shell/SharedSource/Applications/RunAgent/RunAgent.xml: Wizard user interface definition
  • /Website/Shell/Applications/Administration/RunAgent/RunAgentForm.cs: Code-beside for the wizard
  • /Website/Shell/Framework/Commands/System/RunAgent.cs: Command that invokes the wizard
  • /Data/serialization/core/sitecore/content/Applications/Control Panel/Administration/Run Scheduled Task.item: Serialization file for the command definition in the Administration group in the Control Panel (for more about serialization, see The Sitecore Serialization Guide)

Remaining areas for opportunity include at least the following:

  • Real-world testing
  • A base class for agents could handle common properties set by the configuration factory (for example, the database to process, the root item in that database from which to begin processing, the user to impersonate, and the thread priority).
  • Maybe the Cancel button in the wizard could somehow cancel the running agent
  • With a very long list of agents, the drop-down list to select an agent might not provide the greatest usability

While this prototype leaves plenty of opportunities for enhancement, it seems to function properly and could be a useful shared source module. Please add comments to this post with any questions, suggestions for improvement, issues that you face using the code, or any other relevant information.