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.
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:
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.
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:
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:
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:
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.
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.
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:
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:
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:
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.
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.
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.
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.
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.
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:
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.
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:
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.
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:
"message from agent"
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:
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;
" does not exist."
user = Sitecore.Security.Accounts.User.FromName(
// move existing agent method body here
Then define the User property in the /configuration/sitecore/scheduling/agent element in the Web.config file:
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.
Some of the benefits of using agents include:
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:
Remaining areas for opportunity include at least the following:
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.
How to use a treelist in a wizard: sdn.sitecore.net/.../ShowPost.aspx
This works great when using the main admin account. However whenever I try to use a non-admin account I see the "Run Agent" option, but upon running it I just get a blank popup. Is there any specific access permission that I need to provide for this to work for other users (even ones marked as administrators)?
@Ernesto: I don't know of anything specific. I would first try wrapping the entire thing in a SecurityDisabler. If that works, then you might be able to add a SecurityDisabler or find another way to skip security for the logic used, or probably more correctly, identify the items that logic that accesses and open their permissions or add users to roles. Alternatively you could impersonate a specific user.
Some great examples. Thanks, John.
Does this trick still works on sitecore 8.2 Update 2?