I've finally come full circle. I initially intended to write about a pre-existing wizard that I rebuilt before I found out about xaml controls. Ignore the irony about writing a series on using xaml controls and giving the final how-to article using the old controls but I did build the wizard(s) before I discovered the system; so there's that. Also, as I mentioned at the end of the first article, there's a sample tutorial wizard provided by Sitecore, built using the new xaml controls which can be used if you're up to converting it.
Since I began, I've built three more wizards for myself. In the old system. I know, I know. They were to add/remove sites from the system and add/remove extranet security for any given site. This left me with a series of resuable base classes and xml controls. These classes form the backbone that handles gathering data from each field, displaying progress information to the user, and processing the captured information for one reason or another. The intention is to make it easier to customize the forms, messaging and processing for each wizard. In the following example I'm going to implement a sample wizard with two pages demonstrating populating drop trees, drop downs, multi selectors, a checkbox and handling form validity. The final page shows a summary of the information before you process it. Then the process page uses the long running process code written by Alistair Deney's to execute your code in it's own thread so that the page can be updated, giving good feedback to the user. By breaking your task into sections and updating the message for each section it's a pretty slick tool.
For anyone who's unfamiliar with what a wizard is or just not sure what I'm referring to, here's a picture I'm sure you'll be familiar with:
To begin, you'll need to setup a link for an application. The dimensions should be: height-545 and width-500. Next create a "Wizards" folder under the /sitecore modules/shell/ folder and a "SampleWizard" folder beneath that.
Now we need to begin by creating files for the xml control. To start we'll need the xml control that the application points to. Create the file:
/sitecore modules/shell/Wizards/SampleWizard/SampleWizard.xml
Copy the code following code into that file. You should note that the code beside references the WizardCore class. It also uses a WizardFormFirstPage, which is part of the Sitecore system and then two custom pages of type: PageOne and PageTwo. These will be defined next. The next two pages are WizardFormPages also a type provided by Sitecore and they are used for the displaying summary and processing information. The final page is a Sitecore provided WizardFormLastPage that shows when the wizard completes. Here you have the ability to modify some of the messaging in the application. Take note you should update the namespaces and assembly references beginning with "SampleLib" in all the following code with your own class library namespace.
<?xml version="1.0" encoding="utf-8" ?> <control xmlns:def="Definition" xmlns:content="http://www.sitecore.net/content"> <SampleWizard> <WizardForm CodeBeside="SampleLib.Wizards.SampleWizard.WizardCore,SampleLib"> <Stylesheet Src="/sitecore modules/shell/Wizards/SampleWizard/css/wizard.css"/> <WizardFormFirstPage ID="FirstPage" Icon="Applications/48x48/magic-wand.png"> <Border Class="scWizardWelcomeTitle"> <Literal Text="Control Wizard"/> </Border> <Literal Text="This wizard is an example of a control based Wizard."/> </WizardFormFirstPage> <PageOne ID="FirstCustomPage" PageName="First Page" Header="First Page Header" Text="Some instructions." Icon="Software/32x32/text_code.png"/> <PageTwo ID="SecondCustomPage" PageName="Second Page" Header="Second Page Header." Text="Some instructions." Icon="Applications/32x32/gears.png"/> <WizardFormPage ID="SummaryPage" Header="Summary" Text="Please confirm your choices before continuing." Icon="Applications/48x48/magic-wand.png"> <Scrollbox Border="none" Background="transparent"> <Groupbox ID="ChoicesPanel" Header="Your Configuration Choices"> <Border Padding="4" > <Literal ID="Choices" Text="You have selected the following settings:"/> </Border> </Groupbox> </Scrollbox> </WizardFormPage> <WizardFormPage ID="ProcessPage" Header="Building Site" Text="Please wait while the site is being created and configured" Icon="People/32x32/Box_Software.png"> <WizardFormIndent> <Edit ID="HandleId" Hidden="True"/> <GridPanel ID="ProcessDetails" CellPadding="10"> <Groupbox Header="Step 1 Description"> <Border> <Literal ID="step1Message" Text=" "/> </Border> <Border> <Literal ID="step1Status"/> </Border> </Groupbox> <Groupbox Header="Step 2 Description"> <Border> <Literal ID="step2Message" Text=" "/> </Border> <Border> <Literal ID="step2Status"/> </Border> </Groupbox> </GridPanel> </WizardFormIndent> </WizardFormPage> <WizardFormLastPage ID="LastPage" Icon="Applications/48x48/magic-wand.png"> <Scrollbox Border="none" Background="transparent"> <Border Padding="4"> <Literal ID="FinalMessage" Text="The wizard has completed."/> </Border> </Scrollbox> </WizardFormLastPage> </WizardForm> </SampleWizard> </control>
I'll show the two custom pages shortly, but here's a screenshot of the "SummaryPage", "ProcessPage" and "LastPage" included in this control:
Next we'll need some css for the frame and form. It's referenced at the top of the previously defined xml control. You'll want to create:
/sitecore modules/shell/Wizards/SampleWizard/css/wizard.css
fieldset { margin: 10px 10px; } fieldset td { vertical-align:top; } fieldset tr { margin-bottom:7px; display:block; } fieldset label { width:140px; display:block; text-align:right; margin-right:10px; line-height:20px; } fieldset input { width:200px; } fieldset select { width:305px; } fieldset input.checkbox { width:auto; } table#Settings > tbody > tr, table#Language > tbody > tr, table#Security > tbody > tr { margin-bottom:10px;} .scWizardHeader { height:64px; } span.asterisk { color:#ff0000; display:inline-block; margin-right:3px; } span.value { color:#ff0000; } .scWizardText { margin-left:10px !important; } .scComboboxEdit { width: 280px; } fieldset .scCombobox tr { margin-bottom: 0px; } .ErrorMessage { padding:4px; display:block; line-height:17px; } #ProcessDetails { text-align: center; width: 425px; } #ProcessDetails td { padding:5px; } #ProcessDetails div { display:inline-block; margin-left:20px; vertical-align:middle; width:162px; }
Then we'll need the xml controls for each individual page referenced on the main xml control. Each page will reference it's own class file.
On the first page I'm going to demonstrate a TreePicker and a ComboBox. Both are types drop downs but each is unique. Since the TreePicker is displaying a part of the content tree, you need to set a DataContext item for it. You also must create a DataContext item in the xml for this to work. The rest of the settings for the DataContext will be set in the class. The ComboBox will be populated with descendents of the Home item in the content tree in the class file. Create the file:
/sitecore modules/shell/Wizards/SampleWizard/Pages/PageOne.xml
<?xml version="1.0" encoding="utf-8" ?> <control xmlns:def="Definition" xmlns:content="http://www.sitecore.net/content"> <PageOne def:inherits="SampleLib.Wizards.SampleWizard.Pages.PageOne,SampleLib"> <GridPanel Width="100%" Style="display:none"> <WizardPageHeader GridPanel.Class="scWizardHeader" Header="$Header" Text="$Text" Icon="$Icon"/> <WizardPageDivider/> <DataContext ID="ExampleDC"/> <Groupbox Header="Tree Picker Example"> <GridPanel Columns="2"> <Label For="TreeExample"><span class="asterisk">*</span>Tree Picker:</Label> <TreePicker ID="TreeExample" DataContext="ExampleDC" ToolTip="This shows how to setup the TreePicker."/> </GridPanel> </Groupbox> <Groupbox Header="Combobox Example"> <GridPanel Columns="2"> <Label For="ComboboxExample"><span class="asterisk">*</span>Select an item:</Label> <Combobox ID="ComboboxExample" ToolTip="This shows how to setup a Combobox." /> </GridPanel> </Groupbox> </GridPanel> </PageOne> </control>
Here's a screenshot of what the first page looks like:
The second page will also show a multiselect list. I'll populate this will language items from Sitecore in the class file. The second part of the form has a checkbox and an error message that identifies how to move forward through the form. The checkbox will be required to be checked to process the form. File:
/sitecore modules/shell/Wizards/SampleWizard/Pages/PageTwo.xml
<?xml version="1.0" encoding="utf-8" ?> <control xmlns:def="Definition" xmlns:content="http://www.sitecore.net/content"> <PageTwo def:inherits="SampleLib.Wizards.SampleWizard.Pages.PageTwo,SampleLib"> <GridPanel Width="100%" Style="display:none"> <WizardPageHeader GridPanel.Class="scWizardHeader" Header="$Header" Text="$Text" Icon="$Icon"/> <WizardPageDivider/> <Groupbox Header="Listbox Example"> <GridPanel Columns="2"> <Label For="ListboxExample"><span class="asterisk">*</span>Choose Item: </Label> <Listbox ID="ListboxExample" Multiple="false" size="10" ToolTip="This is an example of a Listbox."/> </GridPanel> </Groupbox> <Groupbox Header="Checkbox Example"> <GridPanel Columns="2"> <Literal GridPanel.ColSpan="2" ID="PageTwoErrorMessage" Class="ErrorMessage" Visible="false" Style="color:red" /> <Label For="CheckboxExample">Check or not: </Label> <Checkbox ID="CheckboxExample" Class="checkbox" ToolTip="This is an example of a Checkbox."/> </GridPanel> </Groupbox> </GridPanel> </PageTwo> </control>
Here's a screenshot of the second page:
There are also header chunks that are broken out into two more files. They live just under the Wizards folder because they are resuable to all wizards:
/sitecore modules/shell/Wizards/WizardPageDivider.xml
<?xml version="1.0" encoding="utf-8" ?> <control xmlns:def="Definition" xmlns:content="http://www.sitecore.net/content"> <WizardPageDivider> <GridPanel Width="100%"> <Space GridPanel.Class="scBottomEdge"/> <Space GridPanel.Class="scTopEdge"/> </GridPanel> </WizardPageDivider> </control>
/sitecore modules/shell/Wizards/WizardPageHeader.xml
<?xml version="1.0" encoding="utf-8" ?> <control xmlns:def="Definition" xmlns:content="http://www.sitecore.net/content"> <WizardPageHeader> <GridPanel Columns="2"> <Border GridPanel.Width="100%" GridPanel.VAlign="top"> <Border Class="scWizardTitle"> <Literal Text="$Header"/> </Border> <Border Class="scWizardText"> <Literal Text="$Text"/> </Border> </Border> <ThemedImage Src="$Icon" Width="32" Height="32" Margin="0px 8px 0px 0px"/> </GridPanel> </WizardPageHeader> </control>
That's all the xml control files that make up the wizard structure. You're folder should look like this:
We now need to have the backing classes that run the show but first we should setup a reference to the library we'll be using in an include config file:
/App_Config/includes/Sample.config
<?xml version="1.0"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <ui> <references> <reference comment="SampleLib">/bin/SampleLib.dll</reference> </references> </ui> </sitecore> </configuration>
Next we'll start creating the reusable base classes. To match the sitecore modules folders, you should create a "Wizards" folder and a "SampleWizard" folder beneath it. The AbstractWizardCore is the base class of the application itself. Here is where the movement between pages is managed, the messaging and the thread that processes the final job/task or whatever it is you're doing. You'll want to reference System.Configuration, System.Web, Sitecore.Kernel and Sitecore.Client from your library.
/SampleLib/Wizards/AbstractWizardCore.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore; using Sitecore.Data.Managers; using Sitecore.Jobs; using Sitecore.Shell.Framework; using Sitecore.Web.UI.HtmlControls; using Sitecore.Web.UI.Pages; using Sitecore.Web.UI.Sheer; using HtmlLiteral = Sitecore.Web.UI.HtmlControls.Literal; namespace SampleLib.Wizards { public abstract class AbstractWizardCore : WizardForm { #region Pages public static readonly string ProcessPage = "ProcessPage"; public static readonly string SummaryPage = "SummaryPage"; public static readonly string LastPage = "LastPage"; #endregion Pages #region Controls protected HtmlLiteral Choices; protected HtmlLiteral FinalMessage; //process build id protected Edit HandleId; #endregion Controls #region Settings protected virtual int RefreshTime { get { return 200; } } protected abstract int TotalSteps { get; } protected abstract string ExecuteBtnText { get; } protected abstract string JobName { get; } #endregion Settings #region Control Groupings protected abstract List<HtmlLiteral> MessageFields { get; } protected abstract List<HtmlLiteral> StatusImages { get; } protected Dictionary<StatusType, ImageSet> StatusTypes { get { return new Dictionary<StatusType, ImageSet>() { { StatusType.progress, new ImageSet(){ Src="Images/Progress.gif", Height=17, Width=94 } }, { StatusType.failed, new ImageSet(){ Src="Applications/32x32/delete.png", Height=32, Width=32 } }, { StatusType.passed, new ImageSet(){ Src="Applications/32x32/check.png", Height=32, Width=32 } }, { StatusType.queued, new ImageSet(){ Src="People/32x32/stopwatch.png", Height=32, Width=32 } } }; } } public enum StatusType { progress, failed, passed, queued }; protected class ImageSet { public string Src; public int Height; public int Width; } #endregion Control Groupings #region Properties protected Job BuildJob { get { Handle handle = Handle.Parse(HandleId.Value); return JobManager.GetJob(handle); } } protected IEnumerable<BasePage> SiteBuilderPages { get { // for each page id, find the control var q = from string val in this.Pages select Context.ClientPage.FindControl(val); // return only sitebuilder pages, cast to sitebuilder pages. return q.OfType<BasePage>().Cast<BasePage>(); } } protected BasePage CurrentSiteBuilderPage { get { var ret = Context.ClientPage.FindControl(this.Active) as BasePage; return ret; } } #endregion Properties #region Page Changing protected virtual bool HasCustomPageChangingEvent(string page, string newpage) { return false; } protected override void OnNext(object sender, EventArgs formEventArgs) { if (null != CurrentSiteBuilderPage) { if (CurrentSiteBuilderPage.IsValid) { base.OnNext(sender, formEventArgs); } } else { base.OnNext(sender, formEventArgs); } } protected override bool ActivePageChanging(string page, ref string newpage) { NextButton.Header = "Next"; if(HasCustomPageChangingEvent(page, newpage)){ return true; } else if (newpage == SummaryPage) { NextButton.Header = ExecuteBtnText; // invokes an aggegate function on each sitebuilder page using a new string builder // as the aggregate object to collect the output. StringBuilder sb = SiteBuilderPages.Aggregate(new StringBuilder(), (acc, aPage) => { acc.AppendFormat(@"<h4>{0}</h4>", aPage.PageName); acc.Append(@"<ul>"); foreach (string val in aPage.DataSummary) { acc.AppendFormat("<li>{0}</li>", val); } acc.Append(@"</ul>"); return acc; }); Choices.Text = sb.ToString(); } else if (newpage == ProcessPage) { // performs an aggregation function on each sitebuilder page using a new dictionary as the aggregate object. // Collects the data from all pages and builds the requested site. Dictionary<string, object> data = SiteBuilderPages.Aggregate(new Dictionary<string, object>(), (d, aPage) => d.Merge(aPage.DataDictionary, false)); //disable the buttons and start the long running process NextButton.Visible = false; BackButton.Visible = false; for (int i = 0; i < TotalSteps; i++) { SetStatus((i+1), StatusType.queued, " "); } Job job = JobManager.Start(new JobOptions( JobName, "Wizard Tools", Sitecore.Context.Site.Name, this, "ProcessBuild", new object[] { data })); job.Status.Total = TotalSteps; HandleId.Value = job.Handle.ToString(); SheerResponse.Timer("CheckBuildStatus", RefreshTime); } return true; } #endregion Page Changing #region Building protected abstract AbstractLongRunningJob GetJobObject(Job j); protected void ProcessBuild(Dictionary<string, object> data) { AbstractLongRunningJob blrj = GetJobObject(BuildJob); blrj.Execute(data); } protected void CheckBuildStatus() { try { //get message info int last = BuildJob.Status.Messages.Count - 1; string message = (last > -1) ? BuildJob.Status.Messages[last] : "no messages"; //set status message int step = (int)BuildJob.Status.Processed; if (step > 0 && step <= BuildJob.Status.Total) { //set last step as finished as long as there is a last step if (step > 1) { SetStatus(step - 1, StatusType.passed, "Completed."); } //set current step as in progress SetStatus(step, StatusType.progress, message); } if (!BuildJob.IsDone) { SheerResponse.Timer("CheckBuildStatus", RefreshTime); } else { //on finish the build job adds an additional message so grab the 2nd to last message if it's the last one. message = (last > 0) ? BuildJob.Status.Messages[last - 1] : BuildJob.Status.Messages[last]; BuildComplete((BuildJob.Status.Failed) ? StatusType.failed : StatusType.passed, (BuildJob.Status.Failed) ? "Failed" : "Passed", (message.Length > 0) ? message : "The Site Builder Wizard has completed."); } } catch (Exception ex) { BuildComplete(StatusType.failed, "Check Build Status threw an exception", ex.ToString()); } } protected void BuildComplete(StatusType t, string statusText, string message) { //set last status int step = (int)BuildJob.Status.Processed; SetStatus(step, t, "Completed."); //set the last message and button states ImageSet p = StatusTypes[t]; FinalMessage.Text = string.Format("Build Completed. {0}<br/><br/>Status: {1}<br/><br/>Message:<br/><br/>{2}", ThemeManager.GetImage(p.Src, p.Width, p.Height), statusText, message); //finished. go to the next page this.Next(); } protected void SetStatus(int step, StatusType t, string message) { int pos = (step > 0) ? step - 1 : 0; HtmlLiteral i = StatusImages[pos]; HtmlLiteral m = MessageFields[pos]; ImageSet p = StatusTypes[t]; i.Text = ThemeManager.GetImage(p.Src, p.Width, p.Height); m.Text = message; } #endregion Building #region Cancel Wizard protected override void OnCancel(object sender, EventArgs formEventArgs) { if (this.Active == LastPage) { Windows.Close(); } else { Context.ClientPage.Start(this, "Confirmation"); } } public new void Confirmation(ClientPipelineArgs args) { if (null == args.Result) { Context.ClientPage.ClientResponse.Confirm("Are you sure you want to close the wizard?"); args.Suspend(true); } else if (args.Result == "yes") { Windows.Close(); } } #endregion Cancel Wizard } }
Each page also uses a BasePage class that provides access to the databases, does validation checks, provides summary and data information to its parent wizard core.
/SampleLib/Wizards/BasePage.cs
using System; using System.Collections.Generic; using System.Linq; using Sitecore.Data; using Sitecore.Web.UI.Pages; namespace SampleLib.Wizards { public class BasePage : WizardDialogBaseXmlControl { private Database _mdb; public Database MasterDB { get { if (_mdb == null) _mdb = Sitecore.Configuration.Factory.GetDatabase("master"); return _mdb; } } private Database _wdb; public Database WebDB { get { if (_wdb == null) _wdb = Sitecore.Configuration.Factory.GetDatabase("web"); return _wdb; } } public string PageName { get; set; } public virtual bool IsValid { get { return true; } } public virtual IEnumerable<string> DataSummary { get { return from val in DataDictionary select FormatSummary(val.Key, val.Value.ToString()); } } protected virtual string FormatSummary(string key, string value){ return string.Format(@"{0}: <span class='value'>{1}</span>", key, value); } public virtual IEnumerable<KeyValuePair<string, object>> DataDictionary { get { yield break; } } protected override void OnLoad(EventArgs e) { if (!Sitecore.Context.ClientPage.IsEvent) { InitializeControl(); } } protected virtual void InitializeControl() { } } }
The AbstractLongRunningJob is the base class for the final processing functionality you'll be doing. It provides database access, user messaging and a transactional cleanup capability should something go wrong.
/SampleLib/Wizards/AbstractLongRunningJob.cs
using System; using System.Collections.Generic; using System.IO; using System.Text; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Jobs; using Sitecore.Security.Accounts; using Sitecore.SecurityModel; namespace SampleLib.Wizards { public abstract class AbstractLongRunningJob { protected List<object> CleanupList; protected Database MasterDB; protected Database WebDB; protected Job BuildJob; protected Dictionary<string, object> InputData; public AbstractLongRunningJob(Job job) { CleanupList = new List<object>(); BuildJob = job; MasterDB = Sitecore.Configuration.Factory.GetDatabase("master"); WebDB = Sitecore.Configuration.Factory.GetDatabase("web"); } #region Messaging protected int LangCur; protected int LangTotal; protected int ItemCur; protected int ItemTotal; protected void SetStatus(int processed) { SetStatus(processed, string.Empty); } protected void SetStatus(string message) { SetStatus(-1, message); } protected void SetStatus(int processed, string message) { if(processed > -1) BuildJob.Status.Processed = processed; if(!string.IsNullOrEmpty(message)) BuildJob.Status.Messages.Add(message); } #endregion Messaging #region Execute public void Execute(Dictionary<string, object> data) { BuildJob = Sitecore.Context.Job; SetStatus(0, "Starting Process."); InputData = data; CleanupList.Clear(); try { using (new SecurityDisabler()) { CoreExecute(data); } BuildJob.Status.Messages.Add("Finished Successfully"); } catch (Exception ex) { StringBuilder sb = new StringBuilder(ex.ToString()); CleanupOnFail(ref sb); BuildJob.Status.Failed = true; BuildJob.Status.Messages.Add(string.Format("The wizard was unable to complete because of the following error(s): <br/>{0}", sb.ToString())); } } public abstract void CoreExecute(Dictionary<string, object> data); #endregion #region Transactional Methods protected void CleanupOnFail(ref StringBuilder message) { using (new SecurityDisabler()) { foreach (var val in CleanupList) { try { if (val is Item) { (val as Item).Delete(); } else if (val is DirectoryInfo) { (val as DirectoryInfo).Delete(true); } else if (val is Role) { System.Web.Security.Roles.DeleteRole((val as Role).Name); } } catch (System.Exception ex) { message.AppendLine(); message.AppendFormat("Failed to cleanup [{0}] because of --> {1}", val.ToString(), ex.Message); } } } } #endregion Transactional Methods } }
There are also a few extension methods that are used in other classes.
/SampleLib/Wizards/WizardExtensions.cs
using System.Collections.Generic; using System.Linq; namespace SampleLib.Wizards { public static class WizardExtensions { public static T Get<T>(this Dictionary<string, object> data, string parameter) { object ret = null; if (data.TryGetValue(parameter, out ret) && ret is T) return (T)ret; else return default(T); } } public static class DictionaryExtensions { public static Dictionary<K, V> Merge<K, V>(this Dictionary<K, V> target, IEnumerable<KeyValuePair<K, V>> source, bool overwrite) { source.Aggregate(target, (acc, kvp) => { if (!acc.ContainsKey(kvp.Key)) { acc.Add(kvp.Key, kvp.Value); } else if (overwrite) { acc[kvp.Key] = kvp.Value; } return acc; }); return target; } } }
Now we get to the heart of the sample wizard. The WizardCore class is the implementation of the AbstractWizardCore which is what the application runs on. When you create this class you're forced to provide implementation information such as the number of steps you want to break your task into the text for the button, references to front end messaging controls and an implementation of an AbstractLongRunningJob class. The number provided for the TotalSteps should coincide with the number of pairs of message and status HtmlLiteral controls as well as their references in the MessageFields and StatusImages properties. The refer to the controls on the WizardCore.xml files so if you add or remove any, you should update that file as well. You can fit four comfortably, possibly five.
/SampleLib/Wizards/SampleWizard/WizardCore.cs
using System.Collections.Generic; using Sitecore.Jobs; using HtmlLiteral = Sitecore.Web.UI.HtmlControls.Literal; namespace SampleLib.Wizards.SampleWizard { public class WizardCore : AbstractWizardCore { #region Pages public static readonly string PageOne = "FirstCustomPage"; public static readonly string PageTwo = "SecondCustomPage"; #endregion Pages #region Controls //process steps protected HtmlLiteral step1Message; protected HtmlLiteral step1Status; protected HtmlLiteral step2Message; protected HtmlLiteral step2Status; #endregion Controls #region Settings protected override int TotalSteps { get { return 2; } } protected override string ExecuteBtnText { get { return "Start Job"; } } protected override string JobName { get { return "Control Wizard Example"; } } #endregion Settings #region Control Groupings protected override List<HtmlLiteral> MessageFields { get { return new List<HtmlLiteral>() { step1Message, step2Message }; } } protected override List<HtmlLiteral> StatusImages { get { return new List<HtmlLiteral>() { step1Status, step2Status }; } } #endregion Control Groupings #region Page Changing protected override bool HasCustomPageChangingEvent(string page, string newpage) { if (!newpage.Equals(PageOne)) return false; return true; } #endregion Page Changing #region Building protected override AbstractLongRunningJob GetJobObject(Job j) { return new SampleJob(j); } #endregion Building } }
The implementation of the AbstractLongRunningJob we're using, is the SampleJob and will pull references from the data stored previously by each individual page. You can then do work on that data, update the status and change the step you're working. In this case I'm just going to be counting to ten for each step to show you how to use the messaging. Otherwise no real work is actually done in this example.
/SampleLib/Wizards/SampleWizard/SampleJob.cs
using System.Collections.Generic; using Sitecore.Data.Items; using Sitecore.Globalization; using Sitecore.Jobs; namespace SampleLib.Wizards.SampleWizard { public class SampleJob : AbstractLongRunningJob { public SampleJob(Job job) : base(job) { } #region Start Build public override void CoreExecute(Dictionary<string,object> data) { Item TreeItem = InputData.Get<Item>(Constants.Keys.TreeItem); Item ComboItem = InputData.Get<Item>(Constants.Keys.ComboItem); Language ListItem = InputData.Get<Language>(Constants.Keys.ListItem); bool CheckItem = InputData.Get<bool>(Constants.Keys.CheckItem); //status SetStatus(1, "Step 1."); Method(); //status SetStatus(2, "Step 2."); Method(); } #endregion Start Build #region Build Chunks protected void Method() { for (int i = 0; i < 10; i++) { SetStatus(string.Format("substep {0}", i.ToString())); System.Threading.Thread.Sleep(500); } } #endregion Build Chunks } }
There is also a class file to store strings used throughout the wizard.
/SampleLib/Wizards/SampleWizard/Constants.cs
namespace SampleLib.Wizards.SampleWizard { public static class Constants { public static class Keys { public static readonly string TreeItem = "TreeItem"; public static readonly string ComboItem = "ComboItem"; public static readonly string ListItem = "ListItem"; public static readonly string CheckItem = "CheckItem"; } public static class Paths { public static readonly string Home = "/sitecore/content/Home"; } public static class ItemIDs { public static readonly string Home = "{110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}"; } } }
The last two pieces of the puzzle are the implementations of the individual pages. For each, setup references to the front end controls. Then build out the DataSummary and DataDictionary properties that provide the data to the larger wizard application and long running job. Both pages will use the InitializeControl method to setup the fields.
The DataContext on PageOne requires some special configuration to get it working. PageTwo overrides the IsValid property to do form validation and control page movement.
/SampleLib/Wizards/SampleWizard/Pages/PageOne.cs
using System.Collections.Generic; using System.Linq; using Sitecore.Data.Items; using Sitecore.Web.UI.HtmlControls; namespace SampleLib.Wizards.SampleWizard.Pages { public class PageOne : BasePage { #region Controls protected DataContext ExampleDC; protected TreePicker TreeExample; protected Combobox ComboboxExample; #endregion Controls #region Properties public override IEnumerable<string> DataSummary { get { return new List<string> { FormatSummary(Constants.Keys.TreeItem, MasterDB.GetItem(TreeExample.Value).Name), FormatSummary(Constants.Keys.ComboItem, MasterDB.GetItem(ComboboxExample.Value).Name) }; } } public override IEnumerable<KeyValuePair<string, object>> DataDictionary { get { yield return new KeyValuePair<string, object>(Constants.Keys.TreeItem, MasterDB.GetItem(TreeExample.Value)); yield return new KeyValuePair<string, object>(Constants.Keys.ComboItem, MasterDB.GetItem(ComboboxExample.Value)); } } #endregion Properties #region Page Load protected override void InitializeControl() { //setup the datacontext for the treepicker ExampleDC.GetFromQueryString(); ExampleDC.Root = Constants.Paths.Home; ExampleDC.Folder = Constants.ItemIDs.Home; //setup drop downs Item pFolder = MasterDB.GetItem(Constants.Paths.Home); if (pFolder == null) return; IEnumerable<Item> pages = from val in pFolder.Axes.GetDescendants() orderby val.Name select val; foreach (Item val in pages) { ListItem li1 = new ListItem() { ID = Control.GetUniqueID("I"), Header = val.DisplayName, Value = val.ID.ToString(), Selected = false }; ListItem li2 = new ListItem() { ID = Control.GetUniqueID("I"), Header = val.DisplayName, Value = val.ID.ToString(), Selected = false }; Sitecore.Context.ClientPage.AddControl(TreeExample, li1); Sitecore.Context.ClientPage.AddControl(ComboboxExample, li2); } } #endregion Page Load } }
/SampleLib/Wizards/SampleWizard/Pages/PageTwo.cs
using System.Collections.Generic; using System.Linq; using Sitecore.Globalization; using Sitecore.Web.UI.HtmlControls; namespace SampleLib.Wizards.SampleWizard.Pages { public class PageTwo : BasePage { #region Page protected Literal PageTwoErrorMessage; protected Listbox ListboxExample; protected Checkbox CheckboxExample; #endregion Page; #region Properties public override bool IsValid { get { bool valid = CheckboxExample.Checked; if (!valid) { PageTwoErrorMessage.Visible = true; PageTwoErrorMessage.Text = "You should check the checkbox to proceed"; Sitecore.Context.ClientPage.ClientResponse.SetOuterHtml(PageTwoErrorMessage.ID, PageTwoErrorMessage); } return valid; } } public override IEnumerable<string> DataSummary { get { yield return FormatSummary(Constants.Keys.ListItem, ListboxExample.Value); yield return FormatSummary(Constants.Keys.CheckItem, CheckboxExample.Checked.ToString()); } } public override IEnumerable<KeyValuePair<string, object>> DataDictionary { get { Language selectedLang = (from val in MasterDB.Languages where val.Name.Equals(ListboxExample.Value) select val).First(); yield return new KeyValuePair<string, object>(Constants.Keys.ListItem, selectedLang); yield return new KeyValuePair<string, object>(Constants.Keys.CheckItem, CheckboxExample.Checked); } } #endregion Properties #region Page Load protected override void InitializeControl() { IEnumerable<Language> langs = from val in MasterDB.Languages orderby val.Name select val; foreach (Language l in langs) { ListItem li = new ListItem() { ID = Control.GetUniqueID("I"), Header = l.CultureInfo.DisplayName, Value = l.Name, Selected = (l.Name == Sitecore.Context.Language.Name) }; Sitecore.Context.ClientPage.AddControl(ListboxExample, li); } } #endregion Page Load } }
You're class library should look like this:
Now with all the xml controls and backing class files setup, build the project and open the application. Here's the complete series of images that show the progress through the form to completion.
Alright that about does it. Now get out there and automate your little hearts out. Let me know how it goes.