I have always missed a way of supplying arbitrary input parameters to EPiServer scheduled jobs through the Admin Mode scheduled job interface. As Stefan Forsberg laughingly pointed out to me when I told him about my POC for this functionality (something in the lines of “Oh, I see you have found yourself a hammer.”), I seem to have found a way of using control adapters for just a bit of everything lately. The source code including a sample job is found at the bottom of this post.
The interface of the EPiServer scheduled parameter job
As shown in the image below, the code adds a fieldset to the Settings tab of the scheduled job containing arbitrary input controls. All except the Save values and the Reset values buttons are defined by the developer in advance. Naturally, the standard EPiServer scheduling controls continue to function in the expected manner.
After clicking the Save values button, a message is presented to the administrator letting them know if everything was saved successfully. The same goes of course for the Reset values button.
If we were to manually run the scheduled job before resetting the parameters above, or have the EPiServer scheduler do it, the Sample parameter job would display all of the inputs in the Message column of the History tab, showing how it can be done.
Defining input parameters for an EPiServer scheduled job from codebehind
As I mentioned earlier, this solution relies on a control adapter for it to work. By changing the EPiServer.UI.Admin.DatabaseJob it is possible to add rendering as well as paramerter persisting functionality to the EPiServer job. This is done by adding an adapter node to a browser file in your project’s App_Browsers directory, followed by inheriting an adapter class; in this case PageAdapter. See this post if you are curious about how it works.
AdapterMappings.browser
<browsers> <browser refID="Default"> <controlAdapters> <adapter controlType="EPiServer.UI.Admin.DatabaseJob" adapterType="EPiServer.. ..DatabaseJobAdapter" /> </controlAdapters> </browser> </browsers>
The DatabaseJobAdapter and the scheduled job itself are linked together using an extension of the EPiServer ScheduledPlugInAttribute together with persisting values through the DynamicDataStore. In other words, by using the new (11) attribute’s DefinitionsClass (14) and DefinitionsAssembly (15) properties it is possible to target a class containing a skeleton setup of the input controls.
SampleParameterJob.cs
[ScheduledPlugInWithParameters( DisplayName = "Sample parameter job", Description = "Sample job with parameters", DefinitionsClass = "EPiServer .CodeSample.ScheduledJobs.ParameterJob.DefinitionSample", DefinitionsAssembly = "EPiServer.Templates.AlloyTech" )] public class SampleParameterJob : ScheduledJob { public static string Execute() {
The targeted definitions class must implement the IParameterDefinitions interface in order to be used. This will make sure that all of the job parameter requirements are fulfilled. The GetParameterControls (8) method is what should be returning the data transfer objects for each input control; hard coded or generated. ParameterControlDTO is just a wrapper to help transferring a Control together with an optional label and a tooltip description.
IParameterDefinitions.cs
public interface IParameterDefinitions { IEnumerable<ParameterControlDTO> GetParameterControls(); void SetValue(Control control, object value); object GetValue(Control control); }
The GetValue (10) and SetValue (9) methods are needed to let the adapter know how it should go about persisting and loading its values. Since many controls handle their values in different ways, the developer needs to supply specifications on the controls that they are using; more on this in the “What do I have to do to get input fields in my own scheduled job then?” section further down.
When clicking on a scheduled job in the left hand Admin Mode submenu, a pluginId query parameter is included in the request. This is what tells EPiServer which job it should be looking at. By using this to get hold of a PlugInDescriptor (37) it is possible to determine whether or not the scheduled job should be getting input parameters; if the _attribute variable is null, the job is not using the ScheduledPlugInWithParametersAttribute (38).
DatabaseJobAdapter.cs
var descriptor = PlugInDescriptor.Load(int.Parse(PluginId)); _attribute = descriptor.GetAttribute(typeof (ScheduledPlugInWithParametersAttribute)) as ScheduledPlugInWithParametersAttribute;
Once we have gotten hold of the scheduled job’s attribute, it is just a question of using its DefinitionsAssembly and DefinitionsClass properties to instantiate the skeleton class (51-52).
var assembly = Assembly.Load(Attribute.DefinitionsAssembly); _parameterDefinitions = assembly.CreateInstance(Attribute.DefinitionsClass) as IParameterDefinitions; if (_parameterDefinitions == null) { throw new Exception("Your DefinitionsClass must implement the IParameterDefinitions interface."); }
The DatabaseJobAdapter uses OnInit to loop through the ParameterControlDTO objects that it receives from the GetParameterControls method; rendering EPiServer Admin Mode like HTML for each one as it goes. Should there exist a persisted value for any of the controls, it will be applied instead of the default one coming from the skeleton class. This is where the previously mentioned GetValue and SetValue methods come into play.
The input parameter values are stored using EPiServer’s DynamicDataStore with the scheduled job’s pluginId as an identifier. When the Save values button is pushed in the interface, the click event handler uses a store extension together with the GetValue method to save everything to the database. The same goes for loading the persisted values when rendering the controls, or actually running the scheduled job.
DynamicDataStoreExtensions.cs
public static void PersistValuesFor(this DynamicDataStore store, string pluginId, IEnumerable<Control> controls, Func<Control, object> controlvalue) { store.Save( new ScheduledJobParameters { PluginId = pluginId, PersistedValues = controls .ToDictionary(c => c.ID, controlvalue) }); } public static Dictionary<string, object> LoadPersistedValuesFor(this DynamicDataStore store, string pluginId) { var parameters = store.LoadAll<ScheduledJobParameters>() .SingleOrDefault(p => p.PluginId == pluginId); return parameters != null ? parameters.PersistedValues : new Dictionary<string, object>(); }
The Reset values button works in a similar fashion. It removes all occurences of data with the current pluginId from the DynamicDataStore.
“What do I have to do to get input fields in my own scheduled job then?”
In short, you will have to see to two things; the definitions class, and your scheduled job. First, create a class and make it implement the IParameterDefinitions interface. The DefinitionSample.cs file included in the source code archive is doing just this (12).
DefinitionSample.cs
public class DefinitionSample : IParameterDefinitions { public IEnumerable<ParameterControlDTO> GetParameterControls() { return new List<ParameterControlDTO> { AddACheckBoxSample(), AddATextBoxSample(), AddAnInputPageReferenceSample(), AddACalendarSample(), AddADropDownListSample() }; }
The GetParameterControls is where you generate a List of whatever is up your alley when it comes to input controls. The sample definitions are adding a few example controls to show how it may be done. For instance, granting the web administrator the ability to select a page from the EPiServer PageTree (20) may be highly appreciated.
private static ParameterControlDTO AddAnInputPageReferenceSample() { return new ParameterControlDTO { LabelText = "InputPageReference Sample", Description = "Sample of an EPiServer Page Selector control; InputPageReference.", Control = new InputPageReference { ID = "InputPageReferenceSample" } }; }
The sample InputPageReference control does not really do much, it just states that there will be one with a certain ID (105). It is up to you to add useful controls to the job, and they no not necessarily need to be hard coded. Adding a DropDownList for instance, you would probably want to populate it with ListItem objects dynamically. If the LabelText property is omitted, different HTML will be rendered for the control in the scheduled job interface; see for instance the image of the CheckBoxSample control at the beginning of this post.
What the SetValue and GetValue methods will contain depends on what kind of parameters you added to your control definitions. If you added an InputPageReference control and a CheckBox you would have to match the control to the proper type and handle the value accordingly.
public void SetValue(Control control, object value) { if (control is CheckBox) { ((CheckBox) control).Checked = (bool) value; }
.. else if (control is InputPageReference) { ((InputPageReference) control).PageLink = (PageReference) value; }
The same, somewhat ugly, mapping is necessary when retrieving a value.
public object GetValue(Control control) { if (control is CheckBox) { return ((CheckBox) control).Checked; }
.. if (control is InputPageReference) { return ((InputPageReference) control).PageLink; }
When all that work is done, it is time to create the scheduled job itself. Make sure that you inherit from EPiServer’s ScheduledJob class (17), but rather than using the normal ScheduledPlugIn attribute, use the ScheduledPlugInWithParameters one (11). Point the DefinitionsClass (14) and DefinitionsAssembly (15) properties to where your definitions class is located and create the usual static Execute method (19).
SampleParameterJob.cs
[ScheduledPlugInWithParameters( DisplayName = "Sample parameter job", Description = "Sample job with parameters", DefinitionsClass = "EPiServer .CodeSample.ScheduledJobs.ParameterJob.DefinitionSample", DefinitionsAssembly = "EPiServer.Templates.AlloyTech" )] public class SampleParameterJob : ScheduledJob { public static string Execute() { var descriptor = PlugInDescriptor.Load("EPiServer.CodeSample .ScheduledJobs.ParameterJob.SampleParameterJob", "EPiServer.Templates.AlloyTech"); var store = typeof (ScheduledJobParameters).GetStore(); var parameters = store.LoadPersistedValuesFor( descriptor.ID.ToString(CultureInfo.InvariantCulture));
In the Execute method (19), a PlugInDescriptor (21) may be used to retrieve the proper parameter values from the DynamicDataStore (23) via the pluginId; I have not really found a better way of getting hold of this id, but it really ought to be possible. Will have to read the documentation one of these days. As the values are located in a Dictionary with your control IDs as keys, it should be an easy task for you to retrieve them and cast them to their proper types.
var cbChecked = parameters.ContainsKey("CheckBoxSample") && (bool) parameters["CheckBoxSample"] ? "Aye!" : "Nay..";
var sampleReference = parameters.ContainsKey("InputPageReferenceSample") ? (PageReference)parameters["InputPageReferenceSample"] : PageReference.EmptyReference; var samplePageName = sampleReference != null && sampleReference != PageReference.EmptyReference ? DataFactory.Instance.GetPage(sampleReference).PageName : string.Empty;
var result = string.Empty; result += string.Format("CheckBoxSample checked: <b>{0}</b><br />", cbChecked);
result += string.Format("InputPageReferenceSample page name: <b>{0}</b> (PageId: <b>{1}</b>)<br />", samplePageName, sampleReference);
You cannot rely on the Dictionary‘s ContainsKey method to be sure that a value is set; if a web administrator clicks the Reset values button and then saves the empty fields hitting Save values, empty strings and null values will be persisted in the DynamicDataStore.
Troubleshooting the EPiServer scheduled parameter job
Input parameter controls do not show up in the EPiServer scheduled job interface
The most common reason for the input parameter controls not to show up in the EPiServer scheduled job interface, even though the scheduled job class has the ScheduledPlugInWithParameters attribute, is that the attribute’s properties are wrong. More exactly, either the DefinitionsClass or the DefinitionsAssembly is not pointing to where it should be.
Above is the definitions class from the example included in the source code when it is used in the EPiServer AlloyTech sample project in a directory called ScheduledJobs. The DefinitionsClass property of the attribute should in this case have the value EPiServer.ScheduledJobs.DefinitionSample.
The assembly file that is created by the EPiServer AlloyTech sample project is called EPiServer.Templates.AlloyTech.dll as seen in the image above; meaning the value of DefinitionsAssembly should be EPiServer.Templates.AlloyTech.
Saved input values are not being used by the EPiServer scheduled job
If the values that you have entered in the input parameter controls are persisted when you click the Save values button, but your EPiServer scheduled job still refuses to access them, the problem is very likely to be in the loading of the PlugInDescriptor. If you do not know the plugin id, you will need to supply the Load method with two parameters; the namespace and scheduled job class, as well as the assembly in which the job resides.
The image above shows the EPiServer scheduled sample parameter job from the example in the source code when it is being used in the EPiServer AlloyTech sample project. As you can see, the class name and namespace part is set to EPiServer.ScheduledJobs.SampleParameterJob and the assembly is EPiServer.Templates.AlloyTech.
Source code
The binary version and the NuGet package do not include the example job; for this you would have to get the source code zip, or have a look at GitHub.
Source code: ScheduledParameterJob_src_1.1.0.0.zip (Latest)
Droppable binary: ScheduledParameterJob_binary_1.1.0.0.zip (Latest)
GitHub link: https://github.com/matkun/Blog/tree/master/ScheduledParameterJob
NuGet package: ScheduledParameterJob at the EPiServer NuGet feed.