Quantcast
Channel: Project Lifecycle
Viewing all 178 articles
Browse latest View live

Sheer UI: 2 - Hello World

$
0
0

With the previous article explaining a bit about what Sheer UI is, setting up a hello world will make much more sense. For this, I'm going to setup the XML control and supporting class file and then give examples of calling them from Applications and Item Editor Tabs

Code Behind

Let's start by creating the class file that is referenced by the XML control. I'll provide an example for both the "control" and "xamlControls" types below.

"control" example

I created a class named HelloWorld and it will inherit from the Sitecore.Web.UI.Sheer.BaseForm in the Sitecore.Kernel assembly. There are also a few other base classes you can inherit from that live in the Sitecore.Web.UI.Pages namespace depending on what you are trying to do. Here's the class code I'm going to be using for this example:

using System;
using System.Web;
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Sheer;

namespace MyLibrary {
     public class HelloWorld : BaseForm {

          protected Literal ltlOut;

          protected override void OnLoad(EventArgs e) {
               Assert.ArgumentNotNull((object)e, "e");
               base.OnLoad(e);
              
               ltlOut.Text = string.Format("Querystring: {0}", HttpContext.Current.Request.Url.Query);
           }
     }
}

"xamlControls" example

I created a class named XamlHelloWorld and it will inherit from the Sitecore.Web.UI.XamlSharp.Xaml.XamlMainControl in the Sitecore.Kernel assembly.

using System;
using System.Web;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.XamlSharp.Xaml;

namespace MyLibrary {
     public class XamlHelloWorld : XamlMainControl {
         
          protected Literal ltlOut;

          protected override void OnLoad(EventArgs e) {
               Assert.ArgumentNotNull((object)e, "e");
               base.OnLoad(e);

               ltlOut.Text = string.Format("Querystring: {0}", HttpContext.Current.Request.Url.Query);
           }
     }
}

To be able to reference the controls on the front end, you'll need to add a reference to them here with the ID name matching the ID attribute value on the front end. Sitecore handles populating the control when the class is constructed. In the event a control reference is null, it can be difficult to determine why. Try adding a runat="server" attribute on the XML control or make sure to check the control name id name and casing as well as properly closing the control. Also make sure to set the security on the class reference to at least protected. Setting it to private would not work. In this example, I'm displaying the querystring parameters, which can provide context for you depending on where you launch the control from. For example: Applications only provide the XML control name whereas Item Editor Tabs provide the item ID, language, version, database and other useful context information.

XML File

The next step is to create an xml file. For this example and for yourself, I recommend creating a folder under /sitecore modules/shell.

"control" example

I'm going to name this control "HelloWorld" and create the following file here: /sitecore modules/shell/helloworld/HelloWorld.xml. The contents of that file will look like this:

<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
     <HelloWorld>
          <FormPage>
               <CodeBeside Type="MyLibrary.HelloWorld,MyAssembly"/>
               <Literal ID="ltlOut" runat="server"/>
          </FormPage>
     </HelloWorld>
</control>

"xamlControls" example:

I'm going to name this control "XamlHelloWorld" and create the following file: /sitecore modules/shell/helloworld/XamlHelloWorld.xaml.xml. The contents of that file will look like this:

<xamlControls
  xmlns:x="http://www.sitecore.net/xaml"
  xmlns:ajax="http://www.sitecore.net/ajax"
  xmlns:rest="http://www.sitecore.net/rest"
  xmlns:r="http://www.sitecore.net/renderings"
  xmlns:xmlcontrol="http://www.sitecore.net/xmlcontrols"
  xmlns:p="http://schemas.sitecore.net/Visual-Studio-Intellisense"
  xmlns:asp="http://www.sitecore.net/microsoft/webcontrols"
  xmlns:html="http://www.sitecore.net/microsoft/htmlcontrols"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
     <XamlHelloWorld x:inherits="MyNamespace.XamlHelloWorld,MyAssembly">
          <Sitecore.Controls.HtmlPage runat="server">
               <Literal ID="ltlOut" runat="server" />
          </Sitecore.Controls.HtmlPage>
     </XamlHelloWorld>
</xamlControls>

 

Applications

When you want to setup a Sheer UI Application, you need five basic parts: the code behind file (with the functionality), the xml file (with xml markup referencing the code behind), the layout (referencing the xml file), the application (referencing the layout) and the shortcut (referencing the application). Since we already defined the first two, we'll just need the last three.

Layout Item

Now that we have a control file, we need create a layout reference in Sitecore to it. To do this log into Sitecore's Desktop application and change to the Core database. Open up the content editor and browse to core:/sitecore/layout/Layouts and, to help with organization, insert a plain old folder named "Modules". From that modules folder, right-click and "insert from template" and select the core:/sitecore/templates/System/Layout/Xml layout and name it "HelloWorld".

core layouts

"control" example

On the "HelloWorld" layout item fill out the "Control" field with "HelloWorld". The "Control" field value is what is used by Sitecore to identify the name of the control from the xml file, not the file name or class it references.

layout control field

"xamlControls" example

On the "HelloWorld" layout item fill out the "Path" field with "/sitecore/shell/~/xaml/XamlHelloWorld.aspx". The "Path" field value is what is used by Sitecore to identify the url by referencing the name of the control from the xml file, not the file name or class it references.

layout path field

Application Item

Applications live in the Core database under core:/sitecore/content/Applications. I'm going to add a common folder named "Modules", again for organization, for my application to live in. Then right-click and "insert from template" on this folder and select the template core:/sitecore/templates/Sitecore Client/Applications/Application.

core applications

This item will now need to have the "HelloWorld" layout item applied through the presentation details, so select the item, jump to the presentation tab and click on the details button and set the layout to the one just created. In my case the layout file is located at core:/sitecore/layout/Layouts/Modules/HelloWorld.

application presentation details

Now it's worthwhile to select an item icon so that it's more readily identifiable. When you do, copy the path to the icon you chose and paste it into the "Icon" field on the item. Then fill out the "Display Name" and "Tool tip" fields.

application fields

On the item there's an appearance field section. This will allow you select the dimensions and look and feel of the application. There's a few checkbox fields that will change the display and a few drop down fields. The only one worth mentioning is the "Chrome" field. There's several types of "Chrome" or window treatments. Leaving the field empty will default the application to "WindowChrome". For this example I'm going to leave this value blank but here's how each value affected the display of my application

BorderWindowChrome has no top toolbar which means it's non-draggable.

border window chrome

ToolWindowChrome has a partial top toolbar with the window control buttons (minimize, maximize and close) which allows it to be draggable

tool window chrome

ContentEditorChrome has a partial top toolbar with the window control buttons and the application name which allows it to be draggable.

content editor chrome

WindowChrome has a full toolbar across the top with the window control buttons, the application name and icon. It is also draggable.

window chrome

WindowFrame pops open a bare window with no link in the desktop tool bar. It's more like an alert or confirm window.

window frame

BorderlessChrome displays nothing. possibly a daemon/service app.

Shortcut Button

Now that you've got your application setup, the last part is to create a shortcut button in the start menu to launch the application. Shortcuts live in core:/sitecore/content/Documents and settings/All users/Start menu. Before you create this item, you'll want to consider the audience when you choose where to put the button. If you check the security on the Left, Right and Programs folders you'll see that the read is disabled for Client Users. This is to allow each button or group to be allowed only by specific client roles. I don't want to go into modifying any permissions for this example so I'm just going to create the shortcut in the Development Tools folder and inherit the permissions there. To create this shortcut right-click on core:/sitecore/content/Documents and settings/All users/Start menu/Right/Development Tools and insert "Application shortcut" and name it "Hello World". For reference the template is core:/sitecore/templates/Sitecore Client/Applications/Application shortcut.

core shortcuts

Once you've created the item, you'll want to set the icon to match the application. Then select the application item with the Insert Link button on the "Application" field but once you've clicked ok, click into the field and remove the ".aspx" from the path otherwise the application won't display in the navigation. It's annoying. And a bug. This results in the "Application" field value looking like this: /Applications/Modules/HelloWorld. If you're on Sitecore 7, the fields are not storing the path value, only the ID so you'll have to go to view and edit the raw value of the field. Add the attribute url="/Applications/Modules/HelloWorld". Then finish it off the shortcut by populating the "Display name", "Icon" and "Tool tip" fields.

shortcut fields

Now refresh your browser and open the start menu and click into Development Tools and there's the Hello World or Xaml Hello World button.

menu hello world

Now you're ready to run the application. So click away to start the application.

application running

If you're using the "control" example, you'll see the querystring value each time you open the application like this: ?xmlcontrol=HelloWorld but if you're using the "xamlControls" example it will be blank.

Item Editor Tabs

The editor tab is altogether different from an application. As far as I can tell it only supports the newer "xamlControls". An editor has four basic parts: the code behind file, the xml file, the editor item (referencing the xml file) and a template's Standard Values' editor list. The first two we already created, so we just need the last two. If you're not sure what an editor tab is, I'm referring to the tabs used on a folder item which shows a list of its subitems.

editor folder

Editor Item

To show a XAML control in an items editor tab, you'll need to create an editor tab item in the core database at core:/sitecore/content/Applications/Content Editor/Editors. Right-click and insert a common folder. I'm naming mine "Modules" and then right-click, "insert from template" and select the core:/sitecore/templates/Sitecore Client/Content editor/Editor template and name the new item "EditorHelloWorld". 

core editors

Then set the "Header" and "Icon" fields. The "Url" field will not be a file path but instead an odd mix of file path and custom nomenclature: /sitecore modules/shell/~/xaml/HelloWorld.aspx.

editor fields

Template's Standard Values

The template is where the control will be viewed so switch to the master database and go into master:/sitecore/templates/Sample/Sample Item/__Standard Values (or any standard values item). Editors should be applied to the Standard Values item like Presentation Details etc. Then, in the ribbon, click on the "Configure" tab and click the "Editors" button. 

editor button

Inside the dialog window, select the /Modules/EditorHelloWorld item and click ok.

editor add

Now you can view the tab by re-clicking on the standard values item or selecting an instance from the content tree. When you view the editor tab, you'll see the querystring value like this:

?id=%7b9DFBB778-512F-4BA7-8293-C01503572D30%7d&la=en&language=en&vs=1&version=1&database=master&readonly=0&db=master. 

editor run

These are all available context parameters you can use to display information or provide custom editor functionality for the item.

You can also read the article I wrote about setting up a Rich Text Editor Button if you're curious about how to do that as well. The next article will delve deeper into the syntax of the XML controls.


Sheer UI: 3 - Syntax and Controls

$
0
0

The previous articles explained what Sheer UI was and how to do basic integrations into your system but what else can it do? There's a good deal of syntax that is supported and a number of useful controls at your disposal.

Extension Tags

These are custom tags defined in the XamlSharp.config that modify the local page. They're unique only to the "xamlControls". They can provide action elements with logic statements similar to XSL. You also have the ability to reference class libraries to support custom web controls, set variables and attach attribute values to the existing tags. Here's what they look like:

attribute 

Attaches an attribute and value to the parent tag

<x:attribute runat="server" name="type" value="value"/>

include

Includes another control into this control as if they were the same control.

<x:include runat="server" href="Sitecore.Web.Controls.Properties" />

param

Allows you to set parameter values inside a control

<x:param name="Icon" value="Applications/32x32/about.png" />

register

Includes a reference to support controls from non-referenced libraries

<x:register assembly="ComponentArt.Web.UI" namespace="ComponentArt.Web.UI"/>

styleattribute

Adds the css style to a style attribute on a parent tag

<x:styleattribute runat="server" name="text-align" value="{Align}"/>

Style

Sets a style tag with the class and properties

<x:Style x:Key="MyStyle">
      <Setter Property="Foreground" Value="green" />
</x:Style>
<x:style runat="server" key=".MyStyle">
      <Foreground>green</Foreground>
</x:style>

Using

this is similar to the import statement on a page control

<x:using runat="server" namespace="Sitecore.Controls" />

XSL-like control tags

<x:template name="testTemplate">
<x:variable name="Color" value="lime"/>
<x:set-variable name="OutsideScope" value="{Color2}" />
<x:for-each select="/sitecore/system/*">
<x:value-of select="@@name"/>
<x:if test="$Color = 'blue'">

Parameters

Sitecore provides a structure for passing values into an XML control. To do so, you use the attributes on the instance of that control. When the value is retrieved on the control definition, it can only be used inside another attribute. This means if you wanted to pass in some page text, you'd have to use a Literal control to display it. Here's an example demonstrating the control definition retrieving values:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <ChildControl>
          <div class="$ClassName">
               <Literal Text="$TextValue" runat="server"/>
          </div>     
     </ChildControl>
</xamlControls>

And here is how to pass the values in through the instance control's attributes:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <ParentControl>
          <ChildControl ClassName="someClass" TextValue="Some Text"></ChildControl>
     </ParentControl>
</xamlControls> 

Another example, using a different syntax to query for the parameters, passes in the Title and Background values for the Sitecore.Controls.HtmlPage. On the Sitecore.Controls.HtmlPage definition they are referenced using the like so (I've shortened it for the example):

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <Sitecore.Controls.HtmlPage>
          <x:param name="Title" value="Sitecore" />                                
          <x:param name="Background" />
          <html>                          
               <html:Head runat="server">          
                    <html:Title runat="server" Text="{Title}" />
               </html:Head>
               <HtmlBody runat="server">
                   <x:styleattribute runat="server" name="overflow" value="{Overflow}" />
               </HtmlBody>
          </html>
     </Sitecore.Controls.HtmlPage>
</xamlControls>

And here is an example of using these parameters to the instance control:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <Sitecore.Controls.HtmlPage runat="server" Title="Page Title" Background="url(/sitecore/shell/themes/backgrounds/building.jpg)"/>
</xamlControls>

Attribute Evaluators / Expressions

Expressions are similar to the <% %> .NET syntax. The expression within will be evaluated at runtime. In the control, the expression is inserted directly into the code, so any valid C# code can be used. Similar to the parameters though, this can only be used inside the value of an attribute.

Here's the example from the original documentation on how to use this:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <ColoredText>
          <Border Style='color:${StringUtil.GetString($Color, "red")}'>
               <Literal Text="$Text"/>
          </Border>
     <ColoredText>
</xamlControls>

And here is how you would create an instance of this control and pass in the parameters:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <TextSample>
          <ColoredText Color="blue" Text="Blue Text"/>
          <ColoredText Color="yellow" Text="Yellow Text"/>
     </TextSample>
</xamlControls>

Attribute Assigners

There are attributes that can be used on controls that aren't properties of that control. They are supported by Sitecore's compiler to allow you greater flexibility with the controls you're working with. The syntax for using them on a "control" is slightly different than using then on a "xamlControls" item and I'm not sure all supported attributes are the same for both types.

On a "control" you'd setup the namespace and reference it like so:

<control xmlns:def="Definition">   
     <SampleControl>
          <Literal def:AttributeName="value"/>
     </SampleControl>
</control>

With "xamlControls" you'd reference them like:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <SampleControl>
          <Literal x:AttributeName="value" runat="server" />
      </SampleControl>
</xamlControls>

I recommend using the same conventions provided in previous examples for consistency. I don't know all available attributes but here's a few samples:

x:ID

The ID property is used to help manage unique control ID's. Let's say you had a SampleControl that looked like this:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <SampleControl>
          <Literal ID="ltlOut"/>
     </SampleControl>
</xamlControls>

If you were to include this control twice on another control like this:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <InstanceControl>
          <SampleControl/>
          <SampleControl/>
     </InstanceControl>
</xamlControls>

This will cause an exception to be thrown: Multiple controls with the same ID 'ltlOut' were found. FindControl requires that controls have unique IDs.

To fix this you'll need to add the prefix to the ID attribute which will allow the rendered ID to be unique to the control. The result would look like this:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <SampleControl>
          <Literal x:ID="ltlOut"/>
     </SampleControl>
</xamlControls>

x:inherits

The inherits attribute allows you to define which class is used to represent the control. This can be used on controls who already have a class file defined allowing you to override it. The "xamlControls" in the previous example used this like so:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <XamlHelloWorld x:inherits="MyNamespace.XamlHelloWorld,MyAssembly">
</xamlControls>

x:Placeholder

This attribute tells the rendering engine which placeholder control it should be added to as a child control. A common example would be when you use the Sitecore.Controls.HtmlPage. On this file you'll see this placeholder inside the <html:head>

<Placeholder runat="server" key="Stylesheets"/>

and when you use this page you'd reference it like so:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <Sitecore.Controls.HtmlPage runat="server">
          <Stylesheet runat="server" Src="style.css" x:placeholder="Stylesheets" />
     </Sitecore.Controls.HtmlPage>
</xamlControls>

Controls

With the controls provided by Sitecore out of the box there's two general categories of controls: There's structural (like tables, divs and menus) and there's functional (like form inputs).

Border 

This outputs a div around an whatever it is wrapping.

GridPanel 

This will output any child controls inside a table structure. If you set the number of columns it will fill each table division with a control until the next line.

Scrollbox 

This outputs your controls into a div whose height can be specified and sets the overflow to scroll.

Ribbon

Placing a ribbon on your page requires a few extra step. There's, of course, more than one way to do this but here's the way I recommend. I'm registering the <Ribbon> tag by using the <x:register> tag to identify the library it exists in. I'm also including the css file to make sure its displayed correctly. The XML looks like this:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <XamlRibbonExample x:inherits="MyLibrary.RibbonExample,MyAssembly">
          <Sitecore.Controls.HtmlPage runat="server">
               <x:register assembly="Sitecore.Kernel" namespace="Sitecore.Web.UI.WebControls.Ribbons"/>
               <link rel="stylesheet" href="http://www.markstiles.net/sitecore/shell/Themes/Standard/Default/Ribbon.css" /> 
               <Ribbon ID="Ribbon" runat="server" />
          </Sitecore.Controls.HtmlPage>
      <XamlRibbonExample>
<xamlControls>

The class will implement the IHasCommandContext interface to tell Sitecore that there is a CommandContext to use. The CommandContext is used to set the Uri of the item in the core database that represents the ribbon. In this example, I'm just using the same ribbon from the package designer. The class would look like:

using System;
using System.Web;
using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.XamlSharp.Xaml;

namespace testsite.library {
     public class RibbonExample : XamlMainControl, IHasCommandContext {
         
          protected override void OnLoad(EventArgs e) {
               Assert.ArgumentNotNull((object)e, "e");
               base.OnLoad(e);
          }

          CommandContext IHasCommandContext.GetCommandContext() {
               Item itemNotNull = Client.CoreDatabase.GetItem("/sitecore/content/Applications/Tools/Installer/Designer/Ribbon");
               CommandContext commandContext = new CommandContext();
               commandContext.RibbonSourceUri = itemNotNull.Uri;
               return commandContext;
          }
     }
}

There are other notable controls that I won't provide examples for such as the Edit (TextBox), Combobox (DropDownList) but they are powerful controls that allow you to build some really slick applications.

Exceptions

When you're working through development you may experience different exceptions or issues that aren't clear what the problem is. Here's a few I caught while working:

Parameter name: parent Sitecore.Web.UI.Sheer.ClientPage.AddControl Value cannot be null. Parameter name: parent

If you're getting this exception, you might not be setting a value on a control that should be set. I've hit this a few times using the TreePicker when I didn't set the DataContext Folder property. The DataContext usually needs to have the GetFromQueryString() method called, the Root and Folder properties set.

The resource cannot be found 

This can occur if you're trying to view the control and you're not logged in or there is a compiler error with your xml control. Sometimes if you try to call another control, you might be able to get a different exception that is closer to identifying the issue.

There are still a number of other topics which I didn't have time to research and write about such as persistence (short and long term storage), events and messaging and, of course, how to build a wizard. If you plan on trying to build an application using Sheer, peruse the tutorial information I mentioned in the first article and even how look over how Sitecore builds the applications within itself and you should be able to follow the breadcrumbs and put the pieces together.

Sheer UI: 4 - Building a Wizard

$
0
0

Prefacio

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.  

WIZARD!

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:

wizard page 1

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.

Xml Controls

sitecore modules wizards folder

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:

wizard page 5

wizard page 6

wizard page 8

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:

wizard page 2

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:

wizard page 3

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:

sitecore modules wizards finished

Config Settings

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>

Base Classes

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;
		}

	}
}

Sample Wizard

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:

library wizards finished

Run it

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.

wizard page 1

wizard page 2

wizard page 2.1

wizard page 2.2

wizard page 3

wizard page 4

wizard page 5

wizard page 6

wizard page 7

wizard page 8

Alright that about does it. Now get out there and automate your little hearts out. Let me know how it goes.

TestStar - A Web and Unit Testing SDK

$
0
0

DIATRIBE

I began working with web testing years ago with the MS Test framework when I got a version of Visual Studio that allowed it's use. I started small and began building simple ping tests. Over time it developed into a tool would run sets of tests daily and fed the results into a blog that I could read every morning. It tought me a lot but eventually I hit too many limitations. License cost being the most obvious but also I couldn't create a custom interface for it. I had to work within Visual Studio and the UI was too blunt. This forced me to finally bite the bullet and start converting my system to NUnit and Watin.

I originally only expected to build a simple web app to run my web tests but it quickly became something much more comprehensive. I decided to incorporate both web(integration) tests and unit tests. Up until now, unit tests were something I've avoided because they were too time/code intensive. For any given piece of code you'll likely need several tests. On the other hand, you can get quite of a bit of mileage from a single web test. Now web testing is not a replacement for unit tests. Really they're complementary. Unit tests will catch all types of things like logical errors, regression errors etc. but web testing will catch context errors. A unit of code may purposefully return an exception and that will pass a unit tests but as useful as a stack trace is, users of your website won't ever think so. That being said, just because you stop an exception from being thrown doesn't mean there isn't an error. So both are needed to support a reliable system.

SHOUT OUTS

I want to give credit where it is due. I spent a lot of time reading about unit testing and testing Sitecore in particular. I found a helpful kickstart article about unit testing Sitecore specifically by Matt Kenny. Another by Alistair Deneys that provides a tool to run your tests. Another article by Dan Solovay detailing a solution to some context issues. A really insightful Wikipedia article on Code Coverage and a great article about the philosophy of unit tests.

"TESTSTAR"

So I named the tool "TestStar". I'm clearly a fan of Star Wars. In my mind building this tool was an enormous task that wields a great amount of power and it focuses a lot of scrutiny on any given site so.... yeah, TestStar. You can get it on GitHub and I assure you, it is fully operational. I've also put up a video on YouTube to explain it a little more visually. 

At it's core it's a web application with separate projects for unit and web tests. The intention is to make a companion application to a continuous integration server so that you can copy the binaries and run unit tests, then copy the binaries to your web application and run your web tests. The web app will run these tests through the webpages and when you've got a test configuration you like, there's a form to create a batch file with those settings. This can then be tied up to scheduled tasks and be run hands-free. My end use is to test Sitecore and my associated libraries with it, so I'll go into that towards the end.

Alright now that we got that out of the way let's get down to the nitty gritty.

FRAMEWORK

I've packaged the NUnit and Watin libraries in with the project. I realize this creates dependencies and will not automatically update NUnit and Watin but I wanted to have the application be functional out of the box. You should be aware of that though. You can update the version on your own and/or fork it and do whatever you like to it.

Core

The NUnitTesting.Core library has set of tools to retrieve test fixtures, test methods and categories from an assembly. The core of the code handles running tests and delegating events and corresponding interfaces which define how to handle test events. The web testing requires a bit of structure to store domain names, domain prefixes, environments etc. so there's entity classes and retrieval methods. The data for web testing is stored in JSON in local files and is reconstituted through deserialization. There's also some configuration information stored in the config files so there's a utility that will retrieve those values as well.

WebTests

The NUnitTesting.WebTests library is where the web tests should be written. This is also a place where you'll likely want to add a fair amount of customization. In my implementation I created a few other base test classes and entities specific for my systems and Sitecore. I've left a few samples to show what's involved in creating a test and how they are displayed on the web application and I'll also explain it in more detail later in this post.

UnitTests

The NUnitTesting.UnitTests library is where unit tests should be written. I could've put all the tests into a single project but I found that separating them made more sense. You'll need to include references to any library you intend to test but I'm sure that's standard fare. Aside from that, you should use categories on either the test fixture or test method to help group the tests. If you intend to test Sitecore, you should read the Matt Kenny article detailing the setup which will help you debug your tests but when you build the library. The binaries (including the ones your testing) will end up in the NUnitTesting.WebApp project. You will still need to add your config files to the NUnitTesting.WebApp project but you won't need to manipulate the App_Config path since it will run just like Sitecore does.

TestLauncher

The NUnitTesting.TestLauncher is a console application that your batch scripts will call to run your tests. You probably won't have to modify it at all but you can debug it if you need to understand why something isn't working or if you're curious about how the chain of events works. There are separate test handlers for unit and web tests because they require different parameters and may output messages differently.

WebApp

The NUnitTesting.WebApp is where everything converges. There are really only three pages: Unit Test page, Web Test page and Web Test Data Manager page. The data manager page is how you'll start creating the information you'll need to run web tests against. This information is stored in JSON locally under the /data folder. Once you have the information, you're then able to run the web tests you've built against that data. The unit test will run against the binaries you have locally in the project.

This is also the intended root of where all the batch scripts will run from and by extension the console app and web/unit tests. There's a /scripts folder where batch scripts are stored and there's even a sample to show how to use it.

WEB TESTING

To get started with web testing you'll first need to understand a few concepts that the system is built on. These have developed organically over time.

Environments

An environment is largely defined by the domain prefix. In my sphere of influence, many of my sites are duplicated across several environments so that I can test various branches of code independently. I tend to create similar subdomains for each site environment. IE: my local environment has the "http://test." prefix and my integration environment has the "http://integration." prefix. I have around six different environments not all site live support each environment so the site itself stores the site-to-environments relationship information. For unique situations, the environment values, including the domain prefix, can be overridden on the local value stored on the site.

Systems

A system is really a collection of sites. I use it to be able to select multiple sites at one time. It really only is just a name and any given site only belongs to one system. This evolved because I wanted to be able to target a certain test against a group of sites. So instead of having to select every one individually, I can select by the group.

Sites

A site stores information such as root domain, environments, system but also has a key/value store called properties to allow extensibility. I use this to store Sitecore specific information such as site ID and language code. I can then write a subclass of the site class which exposes a property that will retrieve the values from the key/value store.

Tests

Tests are the modular components and should be customized to your specific environment. The project comes with two samples: PingTest and SitemapTest. To build a custom web test, you should extend the BaseWebTest abstract class and decorate the class with the [TestFixture, RequiresSTA] attribute. The BaseWebTest class provides contextual information that allows you to decide what you want the test to do for a given site and environment. The RunTest method is called when the test is run. It is also up to you to update the RequestURL and ResponseStatus properties to tell the event handlers what happened during the test.

UNIT TESTING

Tests

Unit tests are standard NUnit tests and only require that the class has the [TestFixture] attribute and the test method have the [Test] attribute. You should also set a Category on either the [TestFixture] or [Test] attribute so that you can select groups of tests to run from the web app. For examples of what's available to you when setting up an NUnit test class refer to NUnit.org's documentation.

APP SETTINGS

There's a handful of settings that are stored in the web.config and app.config files. This will allow you to configure and customize the system to so that the web pages and batch script creators will use a different library to load and launch tests from and the web test data can be stored in different files. It also helps when debugging the applications to have the values stored locally in the project app.config files. Here's the breakdown of those settings.

EnvironmentsDataFile

This is the file name where the web testing environments will be stored. The file lives under the NUnitTesting.WebApp/data folder.

SystemsDataFile

Similar to the environments file, this is the file that stores the web testing systems.

SitesDataFile

Like the two previous files, this lives in the same folder and stores the web testing site information

DefaultWebTestAssembly

This defines which assembly contains the web tests

DefaultWebTestLauncher

This defines which assembly contains the web test launcher console application.

DefaultUnitTestAssembly

This defines which assembly contains the unit tests

DefaultUnitTestLauncher

This defines which assembly contains the unit test launcher console application.

BATCH SCRIPTING

For me the end game was to have the tests that I designed be run from scheduled tasks and continuous integration build events on my server. To that end, both web and unit testing pages have a form to create a batch script from the test configuration selected on the page. This can help you get up and running more quickly especially if you're not familiar with the syntax of batch scripts. There's some caveats with the web testing batch creator. The systems are essentially a grouping of sites but when the script is created the system value won't be used in the startup params. This is because you could select a system and then deselect a single site and the system will still be selected. So to prevent any issues, it just creates a list of the site id's. If you've got the saavy you can replace the list of id's with a single system id manually though.

The syntax of the batch script is specific to what the console application is expecting for input parameters. The unit and web test runner each has a different set of parameters. Here's the breakdown:

Web Tests: [0 - test type] [1 - test assembly] [2 - test name] [3 - environments] [4 - systems (optional)] [5 - sites (optional)]

The environments, systems and sites are all comma separated lists of the id's of their respective objects. The id's can be determined from the manager page where the id is displayed next to each item in the drop down lists. The id's are ordered in a zero-based index. Also although systems and sites are optional, at least one of them should be set.

example:

@echo off
set TestLauncherPath=C:\PathToNUnitTesting\Website\bin\NUnitTesting.TestLauncher.exe

@echo on
"%TestLauncherPath%" "-w" "NUnitTesting.WebTests" "SitemapTest" "3,4" "0" ""

Unit: [0 - test type] [1 - test assembly] [2 - test categories (optional)] [3 - test methods (optional)]

The test categories and test methods are comma separated lists of strings that match the category name or method name from the unit test classes. Both are optional but at least one should be set.

example:

@echo off
set TestLauncherPath=C:\PathToNUnitTesting\Website\bin\NUnitTesting.TestLauncher.exe

@echo on
"%TestLauncherPath%" "-u" "NUnitTesting.UnitTests" "Some Test Category,Another Test Category" "SomeNullTest"

SITECORE TESTING

It is possible to test a Sitecore system and it's associated class libraries from this system. It was actually it's intended purpose but I built it to work as a standalone utility for any web/unit tests. There are a handful of configurations you will have to make locally which are well explained by Mathew Kenny but I'll reiterate. Fair warning; this will test your skill level in working with application contexts.

WebApp

For the web application to run the unit tests, you'll need to copy in some information from your existing Sitecore site.

1. You'll need to copy the following sections into your web.config file: Sitecore, appSettings, connectionStrings and configSections.

2. You'll also need to copy in the App_Config folder so there's a local reference to all the Sitecore configurations.

3. Next you'll need to add project references to Sitecore libraries and your site libraries.

4. You'll either need to create a data folder and copy in the license file then update your config to that folder, or update the data file path to an existing folder.

TestLauncher, UnitTests

To enable debugging the class library projects you'll have to setup the project similar to a web application.

1. You'll need to create an App.config file and add the following sections from your web.config: Sitecore, appSettings, connectionStrings and configSections.

2. You'll then need to update all references to "/App_Config" in your App.config file to ".\App_Config" because it needs a relative path to these files.

3. You'll also need to copy in the App_Config folder so there's a local reference to all the Sitecore configurations. You should know that these files all get copied into the bin folder and run from there in case you're having trouble determing where it's context is.

4. Next you'll need to add project references to Sitecore libraries and your site libraries from the WebApp project.

5. You'll either need to create a data folder and copy in the license file then update your config to that folder, or update the data file path to an existing folder.

6. If you're working on WebTests in the TestLauncher you'll need to create a data folder and copy the sites, systems and environments files from the WebApp project

It has been suggested that you can create a patch include config to update all the modified settings. This is true except for one; the connection strings path.

Since the TestLauncher requires a config file to help it run these libraries you'll see in the TestLauncher project's bin folder a NUnitTesting.TestLauncher.exe.config file which copies the app.config settings. You'll need this file in the WebApp project for it to run but, of course, there's an exception; the ".\App_Config" path anywhere within the "Sitecore" section in the config file needs to be set to "..\App_Config". This is because the context of the TestLauncher is partially in the scripts folder where the exe is called from and partially in the bin folder where the exe lives. I've kept a separate copy of the App.config and I copy it into the WebApp on the Post Build Event. Here's the code for that:

copy "$(ProjectDir)App.config.WebApp" "$(SolutionDir)Website\bin\NUnitTesting.TestLauncher.exe.config"

Unit Test Sitecore Context

When you're creating unit tests for Sitecore you'll want to be able to set context data such as the site, language, database, device and whether or not the system is unit testing. I've handled this by setting these manually in the Setup method decorated by the [Setup] attribute. For some tests I needed to have a live site content to test against so I setup a test website in my system. Here's the code I'm using to create that context:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Sitecore.Collections;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;
using Sitecore.SecurityModel;
using Sitecore.Sites;
using Sitecore.Web;

namespace NUnitTesting.UnitTests.Tests {
	public class GlobalStatics {

		public static StringDictionary SiteDic = new StringDictionary(){
			{"allowDebug","true"},
			{"browserTitle",""}, 
			{"cacheHtml",""},
			{"cacheMedia",""},
			{"contentLanguage",""},
			{"contentStartItem",""},
			{"database","master"},
			{"defaultDevice",""},
			{"device",""},
			{"disableClientData",""},
			{"disableXmlControls",""},
			{"domain","extranet"},
			{"enableDebugger","true"},
			{"enableLinkedItems",""},
			{"enablePreview","true"},
			{"enableWebEdit","true"},
			{"enableWorkflow",""},
			{"enableAnalytics",""},
			{"filterItems",""},
			{"hostName","unitesting.local"},
			{"htmlCacheClearLatency",""},
			{"language","en"},
			{"loginPage",""},
			{"masterDatabase",""},
			{"mediaCachePath",""},
			{"mode","true"},
			{"name","UnitTesting"},
			{"physicalFolder","/sites/UnitTesting"},
			{"port",""},
			{"requireLogin",""},
			{"rootPath","/Sitecore/content"},
			{"scheme",""},
			{"startItem","/UnitTesting/Home"},
			{"targetHostName",""},
			{"virtualFolder","/"},
			{"xmlControlPage",""}
		};

		public static SiteContext UTSiteContext {
			get {
				SiteInfo si = new SiteInfo(SiteDic);
				return new SiteContext(si);
			}
		}

		public static string DefaultDeviceID = "{FE5D7FDF-89C0-4D99-9AA3-B5FBD009C9F3}";

		public static Database MasterDB {
			get {
				return Sitecore.Configuration.Factory.GetDatabase("master");
			}
		}
		public static DeviceItem DefaultDevice {
			get {
				using (new SecurityDisabler()) {
					return MasterDB.Resources.Devices[DefaultDeviceID];
				}
			}
		}

		public static Language EnglishLanguage {
			get {
				return Language.Parse("en");
			}
		}

		public static void SetupContext(){
			Sitecore.Context.Site = UTSiteContext;
			Sitecore.Context.Language = EnglishLanguage;
			Sitecore.Context.Database = MasterDB;
			Sitecore.Context.Device = DefaultDevice;
			Sitecore.Context.IsUnitTesting = true;
		}
	}
}

and here's how I'm calling it:

[SetUp]
public void SetUp() {
	GlobalStatics.SetupContext();
}

The use of the .IsUnitTesting property is optional and will modify the path where Sitecore looks for the config files. Dan discusses this in more detail.

Subclasses

While working with web tests I found it useful to convert the TestSite object to a more specific subclass. This is because I added key/value properties to each of my Sitecore site entries to identify the site ID and content language. To be able to do this I created a SitecoreSite subclass from TestSite. Here's the definition for that:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NUnitTesting.Core.Entities;

namespace NUnitTesting.WebTests.Entities {
	public class SitecoreSite : TestSite {

		public string LanguageCode {
			get {
				return GetProperty<string>("LanguageCode", string.Empty);
			}
		}
		public string SiteNodeID {
			get {
				return GetProperty<string>("SiteNodeID", string.Empty);
			}
		}

		private T GetProperty<T>(string key, T defaultValue) {
			return (Properties.ContainsKey(key)) ? (T)Properties[key] : defaultValue;
		}

		public string SCBaseURL(TestEnvironment env) {
			return (string.IsNullOrEmpty(LanguageCode))
				? BaseURL(env)
				: string.Format("{0}/{1}", BaseURL(env), LanguageCode);
		}
	}
}

The TestSite class has a ConvertTo method that basically just serialized the class the JSON and then back into the specified class. Here's an example of that from within a web test class:

SitecoreSite scs = ContextSite.ConvertTo<SitecoreSite>(); 

Automated Updating

After the system is setup and running the long term goal is to get the binaries/configs updated regularly. I haven't made it to this point yet, so I don't have anything to provide as a sample but it will require some sort of batch/build script to do it.

Fin

So that's TestStar. The web app / console app that can run both unit and web tests. Development is ongoing but at the current time of this writing it is stable so please download and fork away.

Musings of a Sitecore Developing Man

$
0
0

Prologue

This article is about the theory of template development. A topic, which at its best, is dry and wonkish. My goal is to provoke thought and discussion but mostly to prevent others from having to continuously wander through this hedge maze until they invariably come to the same end. Besides that, there may be a better solution still that someone else has come up with.

Act I

Recently, on a particularly frigid night in Boston, while milling around after the New England Sitecore User Group, I was speaking to Rick Cabral about his ORM Diamond. I mentioned that I thought he was right to conclude in using interfaces to describe datasource data types in sublayout controls. My reasoning was that fields should be grouped into templates by use, then inherited into pages. Each grouping would then lend itself to being defined by an interface which could be implemented on any page class and then many different page types could reuse or interchange code and presentation elements as needed.

Act II

Let me back up a bit and explain where I began when I came to my conclusion. I was thinking a bit about an article I read discussing the page template mistake which discusses better ways to build templates for the page editor. I really wanted to follow up with my own thoughts on page templates because there are a lot of constraints when building a multisite solution including template reuse.

When you're trying to share templates between two or more sites, you're going to hit page template dilemma. Let's say you have three sites in your system and two of those three will share the same presentation but will differ in content and language. The third site, however, will need to have a separate design and as a result will need a different set of presentation details. If you've already created a page template with fields and presentation set, you have two options: inherit from the page template that already exists and get with it all the standard values baggage (insert options, default tokens, and presentation details) or create an entirely new page template and duplicate all the fields. Both prospects are disagreeable.

Inheriting may leave you with fields you don't need and if the presentation of the base template gets updated, the changes may or not make it through since your customization will break the chain of inheritance. What you're left with is needing to reset the presentation details now and again to get the changes to pass through, then re-adding any changes you've made. Even now, I'm left with a foul taste in my mouth from when I had to do this.

On the other side of the coin, is creating a duplicate set of templates and fields which has the drawback of limits with respect to scale. One thing I hate doing is repeating myself. I have a particular distaste for duplicating templates and their fields since, with just over a hundred site in my system, the thought of a creating a hundred sets of templates, just so I can change the presentation, makes me ill.

Act III

This presentation problem has been with me for longer than I care to remember. Even back when it was only two or three sites in a system, there was a need to share template fields and separate the presentation by site. At first I thought about customizing Sitecore to support having multiple standard values and then setting up a custom Standard Values provider that could choose which to use depending on the site settings. Not entirely a bad solution but when you customize built-in template behavior you're asking to get your system wiped out when you upgrade.

I've also inherited a solution that modifies the path to the layouts and sublayouts during page load to look in the physicalFolder path of the context site and check for the file there before defaulting to a global folder. It's really brilliant but still creates a lot of customization to pipelines that a future change to Sitecore could break.

Denouement

To solve this problem in a way that's a bit more future-proof, you have to find a way to work with the system. The realization I then came to, was that you need to think of page templates as non-field, presentation only. This allows you to create more than one copy of the same generic page template but with different presentation settings and without duplicating the fields. The next logical step is to see each group of fields in their own templates that can be inherited by these page types. To clarify I'll show an example. Let's say we want just a home page, regular page and a form page across these three sites I've mentioned previously. Here's how I'd structure the templates:

Example template tree

To start I'm using a global pool of "Page Groups" that define fields for specific uses like meta data, page content and form handling. I've also created an intermediate layer with the "Page Base" and "Form Base" templates that then inherits these groups. Nothing up to this point has any type of presentation associated with it. They only relate fields to a use and groups of fields to a page type.

The presentation details are defined for each specific site on the matching "Page", "Home Page" and "Form Page" templates in those specific site folders. From the earlier example, the first two sites would use the "Default" set of page templates. The third would use the "Site 1" templates. Both "Form Page" templates will inherit from the "Form Base", "Home Page" and "Page" from "Page Base". Now there's no duplication of fields and you can specify the presentation for each site uniquely. From here you can create IFormBase and IPageBase interfaces that will allow you to build reusable web controls and methods in a core library and globally reusable sublayouts. And ultimately that is the goal; reduction, reuse and modularity.

Addendum

After reading through this a second time, I realized I forgot to mention how this affect insert options. This is a good place to add them because the root structure of what you'll be creating in sites, are pages. Or more explicitly, the items with the presentation set on them. You could brush some broad stroke with the insert options on the standard values here. Then in the event an individual site needed them, you could manage insert options on a site level as well. 

Data Importer Updated

$
0
0

For those of you unfamiliar with it, the Data Importer is a Sitecore module on the Sitecore Marketplace. It's a tool that allows you to import data into Sitecore. It was originally built to import sites from existing websites by pulling from databases and into content items. It allows you the flexibility to handle each individual field in a row of data using a number of built-in conversion types but also allows you to extend the system by creating your own providers for data sources and adding your own field conversion types

I just wanted to write a quick post about the latest release. It's now on version 1.4. If/when you install the package make sure you do a merge on the system settings portion so your existing settings aren't wiped out. Also make sure you do an overwrite on the application content (core) or templates (master) because they need to be updated. If you're visual and prefer a video, I've posted one on YouTube explaining the latest changes. Otherwise, there three changes that comprise the update. 

First is that CSV file imports are now supported. There is a new template type and backing class to support this. To run an import on this type, you'd place the full file path in the query field of the import item and each "From What Field Name" field will be a zero-based index number indicating the column position. The files you're importing will have to live within the web application folder to prevent read permissions issues. 

The second change is a UI change. I've removed the application that manages the imports and instead moved that layout to a tab on the import item itself. This means you won't need to be logged into Desktop mode to use it and will hopefully make it a little more intuitive since you will be able to run the import with the item itself selected. 

The final change was from a user request for a feature to allow importing based on a schedule. To do this I've added a console application that can be called from scheduled tasks or elsewhere. I've also added a sample batch script under the \sitecore modules\Data Import\scripts folder to show how you would call the executable and fire off the import. It's a small step but I can see that being a very useful feature going forward. 

Sitecore Extranet Module

$
0
0

Antecedent

Friends, romans, countrymen... et al, the Setting Up a Sitecore Extranet article I wrote in 2011 is one of my most trafficked. It occurred that many of you could benefit from the additional changes that I've made since then, so that those who are setting up your own system by hand could now benefit from a module that manages most of the process for you. For visual reference, I've made a video for a quick intro to the module.

There already exists an extranet module on the Sitecore Marketplace but I'm not the developer of it. Sitecore is. The documentation says it was created in 2007 for Sitecore 5.3. I've never used it so I can't speak to its efficacy. I'm presenting my own solution and you're free to choose which will work best for you. I do plan to request my module be placed on the marketplace but it's not there yet. Currently the Sitecore install package can be downloaded from Github and the full source is available here

The Gist

There are two wizards in this tool that are used to setup and remove extranets from individual sites. This implies it's support for a multisite installation. Depending on whether you're using the Multiple Site Manager or not, it will create attributes for the site nodes in the content tree or in include config files following the pattern Sitecore.Extranet.sites.<SiteName>.config. These attributes will allow you to specify the security role for the extranet, the prefix that will be added to usernames (so the same user can have separate accounts on separate sites), the from address emails are sent from and the number of login attempts before being blocked. By default the setup wizard creates them individually for each site but you can change them in the config or content tree depending on where your sites are managed. I only recommend you do so only after you understand all of what's affected (during installation, the operation of it and removal process). 

When adding and removing sites, you may need to know which sites already have an extranet applied. To help you quickly determine this, there's a listing on the "Extranet Settings" tab of the system settings item under /sitecore/System/Modules/Extranet.

Pimp My Ride

There's a set of templates that represent the page types that manage logging in, registering and managing account information. The templates themselves do not have presentation or security setup on them, although the form sublayouts are provided. They are, however, setup on the "example" branch set of templates provided. I designed it this way because each site may require their own presentation and security settings. The security is only applied to the login page and has a generic role set on it which is replaced during creation by the wizard with the new role created for that site.

The intention is that you would create your own branch (you could duplicate the example) and configure your own presentation settings. The security settings should be copied since they are a token value that is replaced with the specific site role during creation. This will allow you have different configurations for different types of sites. When you use the setup wizard, you will select which branch you want to use for the site you're adding it to. One item to note is that there is an account navigation sublayout that you should apply it to every page on your site. Currently it was added to the login page presentation settings just so that you'd know it's there but the best way to display it is to add it to a global location on a page or master page file. The account navigation is where the logout and edit account buttons are located so it's needed for a fully cyclical system (login and logout). It will automatically hide itself when a user is logged out so it won't be obtrusive to the existing site.   

When working with the sublayouts, you have the ability to create your own front end. You can decide how best to handle this. You could just alter the files provided or create new sublayout items/files and attach them to a new branch template. The default files are stored under /sitecore modules/web/extranet/. 
Much of the functionality can be modified also. On the base classes for the sublayouts many methods are marked virtual so that, if needed, you can also override and customize that locally too.  

Throughout the sublayouts used by the extranet, there's all types of field labels, error messages and other text that can be translated. 

The settings section contains a dictionary of these text items. With this you're able to populate versions of the items into the language each site will be using. If this doesn't meet your needs or you already have a dictionary that you manage, you can override the form text provider in the include Sitecore.Extranet.config and manage the form text in your own way.

Wrap Up

Although this does work with Sitecore 7.1 it's still built using Sheer UI. I don't have a firm enough grip on SPEAK to have created a version using it so it'll be a while before that happens, although I'm sure I'll end up with a series about how to do it. 

Live long and prosper.

Linq to Sitecore

$
0
0

I wanted to write up an article that elaborates on a presentation I gave on some but not all of the new features that will be upcoming in Sitecore 7. Mostly I'm going to focus on the updates around Lucene search but I'll also touch on some of the other features as well. This will be a longer article so be warned.

I'd also like to preface this article by saying that one of the driving forces behind much of the change in Sitecore 7 is an increasing need to support large data sets. And don't dismiss this release thinking that there isn't a lot of value if you're system isn't that large. You don't need to be very big to benefit from a faster search utility or the new features it includes since it doesn't really take much more than a few thousand items to really find Lucene beneficial.

What's Changing

The new system is highly geared toward improved search and as a shining example of that is how the Item Buckets are now baked into the content editor. Buckets are a great way to manage large sets of content considering the suggested limit of around 100 child items per item in Sitecore. But let's also consider that aside from massive quantities of data, not all content needs to be stored structurally in a tree. There's plenty of use cases where you just need to store information and will deal with selecting and displaying it based on the value of the fields and not on how they're stored. to assist with this, you'll see that there are new editor tabs attached to what seems like all items in the tree, that allow you to search for content in the context of the item selected. They've also included new field types that allow users to search for items which is great because item lists can grow long fast inside those tiny [treelist, droplink etc.] windows.  

Hot Swappable Indexes

One of the major changes to Lucene is that you can now configure it to build the index in a separate folder so that it doesn't interfere with the existing index. If you've ever found yourself with no results or a filelock exception due to Lucene deleting the index before rebuilding, you'll be interested in this. It's not setup by default but you can configure it by changing the configuration setting for the Lucene Provider. Here's how it will look:

<indexes hint="list:AddIndex">
	<index id="content_index" type="Sitecore.ContentSearch.LuceneProvider.SwitchOnRebuildLuceneIndex, Sitecore.ContentSearch.LuceneProvider"> 

The way this works is that the two folders for the index will be named like so: index_folder, index_folder_sec. The "_sec" stands for secondary. The provider will use the most up to date folder and will rebuild into the other. It should be noted that after you make the change to use the SwitchOnRebuildLuceneIndex, you will need to perform a rebuild twice on the index as per the provided documentation.

Interchangeable Indexes

The next set of updates are regarding the indexing source itself. Lucene can be now replaced with an implementation of SOLR that can be setup and configured remotely and queried in it's vastness very quickly. I still have questions about if the Lucene indexer can be configured to run on a separate machine in a similar vein but I don't currently know. This of course will require some training time to understand how to set that up but if you're building something that needs that kind of scale I'd guess you'll just be glad it's possible.

The new API

There's also a new search API "Sitecore.ContentSearch" that's unofficially being called "Linq to Sitecore" and includes an "ORM" feature that allows you to cast your search results to your existing class-model structures (assuming you have one) such as Custom Item Generator or Glass Mapper. Actually the casting is similar to that of the Glass Mapper in how you decorate fields to identify which fields should be popuplated from corresponding fields in the search result. You may now have questions, as I do, about where Alex Shyba's amazing AdvancedDatabaseSearch/Sitecore.SearchContrib stands. I think this may end up replacing it but again I don't know. It seems like this new search has in many ways learned from what he did and taken it a bit further with it's syntax. I did find this bit in the documentation explaining that Sitecore does want to begin phasing out the older search methods:

"The Sitecore.Data.Indexing API was deprecated in Sitecore CMS 6.5 and in 7.0 it has been completely removed. The Sitecore.Search API works with Sitecore CMS 6 and 7, but is not recommended for new development in version 7.0. Developers should use the Sitecore.ContentSearch API when using Sitecore Search or Lucene search indexes in Sitecore 7.0."

Configuration and Code Samples

Okay so now that I've covered a bit about what's happening I'll get the "how" side of things. I wanted to see how much effort was involved in using the new search features to see if I would be completely relying on it for all my data querying needs or just supplementing the existing methods. I started by setting up a local instance and creating some templates, content items and a sublayout. The next piece is to change the search configuration file located at /App_Config/Includes/Sitecore.ContentSearch.Lucene.DefaultIndexConfiguration to tell it to store my field values in the index by changing storageType="NO" to storageType="YES":

<fieldType fieldTypeName="single-line text" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String"   settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" />

With that set you'll then need to run a rebuild of the index which is now available in many ways but I used the one available through the Control Panel in the Content Editor. Then I found an example of how to query from the "Developer's Guide To Item Buckets and Search" document provided to me by Sitecore. I've altered it a bit to show how to make the query more generic for use in returning results of any given type and to also see what it's limits were:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Security;
using Sitecore.Data;
using Sitecore.Data.Items;

namespace TestsiteSC7.Web.layouts {
	public partial class PeoplePage : System.Web.UI.Page {

		#region Utility Methods

		protected StringBuilder log = new StringBuilder();
		protected void Log(string title, string message) {
			log.AppendFormat("{0}:{1}<br/>", title, message);
		}

		#endregion Utility Methods

		#region Page Events

		protected void Page_Load(object sender, EventArgs e) {

			//show indexes
			//ShowIndexes();

			//demo hydration
			//GetPeople();

			//show all fields
			GetResult();

			//write log
			ltlOut.Text = log.ToString();
		}

		#endregion Page Events

		#region Search Methods

		protected void ShowIndexes() {
			Dictionary<string, ISearchIndex> indexes = ContentSearchManager.SearchConfiguration.Indexes;
			Log("Number of Indexes", indexes.Count.ToString());
			foreach (KeyValuePair<string, ISearchIndex> p in indexes)
				Log("Index", p.Key);
		}

		protected void GetPeople() {
			List<Person> people = PerformSearch<Person>();
			Log("Number of People", people.Count().ToString());
			foreach (Person i in people) {
				Log("name", i.Name);
				Log("address", i.Address);
				Log("occupation", i.Job);
				Log("_fullpath", i.Path);
				Log("parsedlanguage", i.ParsedLanguage);
				Log("_template", i.template);
				Log("_name", i.OtherName);
				Log("", "");
			}
		}

		protected void GetResult() {
			//filters down to a single result
			IEnumerable<MySearchResultItem> results = PerformSearch<MySearchResultItem>()
				.Where(a => a.Name.Equals("Jim"));
			Log("Number of results", results.Count().ToString());
			foreach (MySearchResultItem m in results) {
				Log(m.Name, string.Empty);
				foreach (KeyValuePair<string, string> f in m.fields)
					Log(f.Key, f.Value);
			}
		}

		private List<T> PerformSearch<T>() where T : AbstractResult, new() {
			//the fields are managed in:
			//			/App_Config/includes/Sitecore.ContentSearch.Lucene.DefaultIndexConfiguration
			//this index is managed in:
			//			/App_Config/includes/Sitecore.ContentSearch.Lucene.Index.Master.config
			//this settings for indexing in:
			//			/App_Config/includes/Sitecore.ContentSearch.config
			var index = ContentSearchManager.GetIndex("sitecore_master_index");
			using (var context = index.CreateSearchContext(SearchSecurityOptions.EnableSecurityCheck)) {
				var queryable = context.GetQueryable<T>()
					.Where(a => a.template.Equals(AbstractResult.TemplateID));

				//work with items inside the context
				T instance = new T();
				//display count
				instance.HandleResults(queryable);

				//need to send to list before the using outside the context
				return queryable.ToList();
			}
			//exception thrown when working with IQueryable object outside of context
			//Accessing IndexSearchContext after Commit() called
		}

		#endregion Search Methods
	}

	public interface IResult {
		string template { get; set; }
		string Name { get; set; }
	}

	public abstract class AbstractResult : IResult {

		public string Name { get; set; }
		[IndexField("_template")]
		public string template { get; set; }

		public static readonly string TemplateID = "fc110b3df82c4b0eabc580a4f185ea1d";

		public abstract void HandleResults(IQueryable<AbstractResult> results);
	}

	public class Person : AbstractResult {

		#region properties

		[IndexField("_name")]
		public string OtherName { get; set; }

		public string ParsedLanguage { get; set; }

		[IndexField("_fullpath")]
		public string Path { get; set; }

		public string Address { get; set; }

		[IndexField("occupation")]
		public string Job { get; set; }

		#endregion properties

		public override void HandleResults(IQueryable<AbstractResult> results) {
			IEnumerable<Person> people = (IEnumerable<Person>)results;
			HttpContext.Current.Response.Write("People count:" + people.Count().ToString());
		}
	}

	public class MySearchResultItem : AbstractResult {

		#region properties

		// Fields
		public readonly Dictionary<string, string> fields = new Dictionary<string, string>();
		// Will match the myid field in the index
		public Guid MyId { get; set; }
		public int MyNumber { get; set; }
		public float MyFloatingPointNumber { get; set; }
		public double MyOtherFloatingPointNumber { get; set; }
		public DateTime MyDate { get; set; }
		public ShortID MyShortID { get; set; }
		public ID SitecoreID { get; set; }

		// Will be set with key and value for each field in the index document
		public string this[string key] {
			get {
				return this.fields[key.ToLowerInvariant()];
			}
			set {
				this.fields[key.ToLowerInvariant()] = value;
			}
		}

		#endregion properties

		public override void HandleResults(IQueryable<AbstractResult> results) {
			IEnumerable<MySearchResultItem> people = (IEnumerable<MySearchResultItem>)results;
			HttpContext.Current.Response.Write("Results count:" + people.Count().ToString());
		}
	}
}

What to Expect

I will state for the record that it doesn't take much effort to get results. In fact you may get more than you bargained for. By default you'll get all results in the index, which include everything and the kitchen sink. I ended up with standard values items as results even after filtering by template id. The key thing to note here is that Lucene is returning results and Sitecore is then using the field values to fill properties on the class you provide. It will fill your object with data whether or not it makes sense to. I guess this is no different than other ORMs but keep in mind that you will be best served by filtering out the results thoughtfully. What I will recommend is that you create several small indexes that are focused just on the template/content types you're trying to retrieve otherwise you may have other data comingling into your results. Making smaller indexes will also keep the amount of memory and CPU usage to a minimum when rebuilding indexes and will limit the scope of what is affected.

The Code Walkthrough

Now I'll delve a bit into the code itself. I've created an interface IResult, an abstract class AbstractResult and two classes: Person and MySearchResultItem (MySearchResultItem is actually pulled from the documentation). The use of the interface was to require that all results have certain fields. The abstract class was to give enough substance to a generic result class to allow the search method to call a method with the result set.

Sitecore is going to use the search result values to populate your classes after a search is run and you'll notice that on the Person class there are fields decorated to identify which values should be put into what property. You also won't have to add the decoration if the class property matches the name of a field in a result. I haven't yet tested how field names with spaces will be handled but I imagine that the spaces will just be removed or replaced.  Here's some different examples:

//setting Sitecore provided fields
[IndexField("_fullpath")]
public string Path { get; set; }

//fills itself by matching names
public string Address { get; set; }

//populating a template field into an unrelated property name
[IndexField("occupation")]
public string Job { get; set; }

It will help to become acquainted with the fields you have available to you. The MySearchResultItem has an example of property called fields that will get populated by Sitecore with all the field names/values. You can use this as a guide to see what's available to you. By default, it contains some Sitecore information like template id and item path among others:

public readonly Dictionary<string, string> fields = new Dictionary<string, string>();
// Will be set with key and value for each field in the index document
public string this[string key] {
	get {
		return this.fields[key.ToLowerInvariant()];
	}
	set {
		this.fields[key.ToLowerInvariant()] = value;
	}
}

When I got to actually operating the search, my goal was to push as much generic search functionality into a single method and allow the calls for each type to be extremely simple. Here's how the search is initiated in my examples.

List<Person> people = PerformSearch<Person>();
IEnumerable<MySearchResultItem> results = PerformSearch<MySearchResultItem>();

The PerformSearch is simple and reusable. It also takes a Generic type that is of type AbstractResult and has an empty constructor. Here's the signature for that method:

private List<T> PerformSearch<T>() where T : AbstractResult, new() {}

It starts by getting the index and setting up a using context. In this case the security is enabled so Sitecore will filter results with permissions in mind:

var index = SearchManager.GetIndex("sitecore_master_index");
using (var context = index.CreateSearchContext(SearchSecurityOptions.EnableSecurityCheck)) {

I'm using the built in master database index and I've filtered by the template id provided by the abstract class. Which does work well. The search and filter largely occur on a single line, which is also really quite impressive.

var queryable = context.GetQueryable<T>().Where(a => a.template.Equals(AbstractResult.TemplateID));

The GetQueryable<T> is the part where Sitecore will fill your class type with the result values and the .Where() is the Linq to Sitecore which does all the work in the Lucene/SOLR index. You will be able to use the alternate syntax as well:

var queryable = from T a in context.GetQueryable<T>
		where a.template.Equals(AbstractResult.TemplateID)
		select a;

The beauty here is that you're offloading a lot of the filtering directly to Lucene instead of having to sort/filter/trim a humungous list of items in the .NET then cache it. You should know that not all queryable methods are implemented but what is available certainly isn't a short list. The supported methods are: Where, Select, Take, Skip, Page, OrderBy, OrderByDescending, ThenBy, ThenByDescending, First, FirstOrDefault, ElementAt, Last, LastOrDefault, Single, SingleOrDefault, Any and Count. It's definitely got some teeth.

I also wanted to point out that you're really only able to manipulate the search result in it's current form while you're inside the using context. If you try to return an IEnumerable result and then try to loop through the results you'll get an exception like:

Accessing IndexSearchContext after Commit() called

This is likely because of how IEnumerable behaves in that the items are only queried when the GetEnumerator is called and in this case the objects that are used to crunch the data have been disposed of through the IDisposable using context. You do have two options to deal with this. You'll either want to pass in a handler method or have one available on your Type(T) class, as I did or you can convert the result set to a List<T>. This will force the data to be crunched while the context is still available. Here's the example of the handler being called on the AbstractResult class which is implemented separately in the subclasses.

T instance = new T();
instance.HandleResults(queryable);

I also return the result set as a List and do work on the results in the calling method.

return queryable.ToList();

My results are then iterated through and have some of the field data printed to a log for display:

Log("Number of People", people.Count().ToString());
foreach (Person i in people) {
	Log("name", i.Name);
	Log("address", i.Address);
	Log("occupation", i.Job);
	Log("_fullpath", i.Path);
	Log("parsedlanguage", i.ParsedLanguage);
	Log("_template", i.template);
	Log("_name", i.OtherName);
	Log("", "");
}

Hopefully this new search, with it's simplified API and multi-foldered index will be big enough improvements to gain the trust of the development community and get rapid adoption. I think you'll be able to rely on it whether you've got a small or large system, as it's quite fast and powerfull.

One More Thing

There is also a nice update to Sitecore regarding the caching properties on sublayouts. You will be able to configure the cache to clear when the index is updated. This may need some customization for multi-site instances since you (and I) may not want all html cache removed when you publish a single item.

Synopsis

Sitecore has now brought Lucene out of the shadows and integrated it into the system as a much more visible and reliable utility to build your solutions on. The new API is definitely a breath of fresh air and will hopefully, lower the barrier for entry for developers to configure and use it and allow us to build much more efficient applications.


Site Scraper

$
0
0

I just finished cleaning up a utility I built a few years ago and pushed it up to GitHub. I often found myself needing to have an html export of some sites so that design vendors could update the css/js, or some sites were being moved to different systems in another part of the company. I've cleaned up the design so that it's less distracting and updated how it works so that you get what you want in the end: a clickable website in a folder.

Here's a screenshot:

site scraper

There's two parts that I built: the Xenu list processor and the link processor. I use the Xenu site crawler to first get a list of all the css, js, pdfs, images and page urls that the site has on it. Then I export that list to a tab separated file. With the first form, I remove all the extra information from the Xenu link list. The form also creates a new file that can be loaded into the drop-down list of the second form. There's also a few options you have to work with such as include/exclude filtering and separating the links for files, images and pages into separate files. Sometimes it's easier to process long lists of files separately. I found it useful so it's in there.

The second form will take the list generated by the first form, or really any list of links you provide it and will then request for the content of that page and save it to a file. The files generated will be foldered to match the existing site and will replace all the internal site links to relative paths so that when you open the files in a browser you can click to other pages. You do of course have options so that you can opt to not update the urls to relative paths, you can save the pages down as a different file type, you can set what the default page name will be and you can append the querystring to the filename so you can capture separate states of an individual page if you need to.

I've found it incredibly useful and hopefully other people will also.

Field Suite Images Field

$
0
0

I've been using the FieldSuite, which was built by Sitecore MVP, superhero and all around good guy, Tim Braga, for about 8 months now and have come to really like a lot of its featues but there was one field that I hadn't used: The "Field Suite Images Field". When I thought I'd finally look into it, the documentation had a few details about customizing the control but not about using the field which gave me the opportunity to do a quick write-up. So first I tinkered with it until I figured out how to get it working and then figured it would be a good idea to write up a quick post about it.

The basic premise is an extension of the built-in Sitecore image field which allows you to select a single image. With this Field Suite Images Field you can select multiple images, albeit,  one at a time.

First you'll want to have a media library folder with some images in it. I've also created a nested folder to show that it support it.

media library structure

When you add the field to a template, you'll want to set the field source to a media library folder where you'll want it to pull images from.

field definition

Once you've setup the template, next you'll create an item in the content tree and you'll see that the Images Field has a row of buttons to help manage the value.

field empty

If you click on the "Link Item" button this allows you to use a drop tree field to select an image.

field select item

After you select an image you click save and you'll return to the field value with the image set.

field save

field single selected

If you continue to "Link" more images you'll see them appear in a horizontal list with their publish status (another Field Suite feature) below.

field multiple selected

From here you can select any individual item in the field and edit, go to the item, remove the item or reorder the item in the list.

field image select

When it comes time to access the field value, it's stored as a pipe delimited list of Sitecore ID's so it should be fairly easy to work with.

field raw value

Sitecore Custom User Properties

$
0
0

In a recent Sitecore build I had to store the pages that a user saved as a "Favorite" throughout the site. The site was an intranet so everyone had a user within Sitecore, which helped. The next question was where to store the information. I wanted it to be stored along with local user data but wasn't sure if I was going to need to extend the user object and create extra fields or if I could leverage the existing structure. I ended up looking through a lot of the user properties and scouring the cookbooks and eventually I found what I was looking for in the security api cookbook: Custom Properties.

You can access and set custom properties on a Sitecore user. It stores these custom properties in a NameValueCollection so you can get and set the values with a key. With that settled, I knew I'd only need to be storing the item ID's for each page. I ended up creating a wrapper class to manage the key for the favorites and to help manage the string value as a comma-delimited list.

Here's the code I used:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Security.Accounts;

namespace MyLibrary {

	public static class UserExtensions {

		public static readonly string custPropKey = "user-favorites";
		public static readonly string custPropDelim = ",";

		#region Add Favorite

		public static void AddFavorite(this User u, ID pageID) {
			AddFavorite(u, pageID.ToString());
		}

		public static void AddFavorite(this User u, Item page) {
			AddFavorite(u, page.ID);
		}

		public static void AddFavorite(this User u, string pageID) {
			IEnumerable<string> f = GetFavorites(u);
			List<string> l = f.ToList();
			//skip if already added
			if (l.Contains(pageID))
				return; 
			l.Add(pageID);
			SetFavorites(u, l);
		}

		#endregion Add Favorite

		#region Remove Favorite

		public static void RemoveFavorite(this User u, ID pageID) {
			RemoveFavorite(u, pageID.ToString());
		}

		public static void RemoveFavorite(this User u, Item page) {
			RemoveFavorite(u, page.ID);
		}

		public static void RemoveFavorite(this User u, string pageID) {
			IEnumerable<string> f = GetFavorites(u);
			List<string> l = f.ToList();
			if (!l.Contains(pageID))
				return; 
			l.Remove(pageID);
			SetFavorites(u, l);
		}

		#endregion Remove Favorite

		#region Set Favorites

		public static void SetFavorites(this User u, List<string> pageIDs) {
			StringBuilder sb = new StringBuilder();
			foreach (string s in pageIDs) {
				if (sb.Length > 0)
					sb.Append(custPropDelim);
				sb.Append(s);
			}
			u.Profile.SetCustomProperty(custPropKey, sb.ToString());
			u.Profile.Save();
		}

		#endregion Set Favorites

		#region Get Favorites

		public static IEnumerable<string> GetFavorites(this User u) {
			string s = u.Profile.GetCustomProperty(custPropKey);
			IEnumerable<string> split = s.Split(new string[] { custPropDelim }, StringSplitOptions.RemoveEmptyEntries).ToList();
			return split;
		}

		public static IEnumerable<ID> GetFavoriteIDs(this User u) {
			IEnumerable<string> s = GetFavorites(u);
			return (s.Any()) ?
				from string id in s
				where ID.IsID(id)
				select ID.Parse(id) 
				: Enumerable.Empty<ID>();
		}

		public static IEnumerable<Item> GetFavoriteItems(this User u) {
			IEnumerable<ID> s = GetFavoriteIDs(u);
			List<Item> returnList = new List<Item>();
			if (s.Any()) {
				var items = from ID i in s
							select Sitecore.Context.Database.Items[i];
				foreach(Item i in items){
					if(i != null)
						returnList.Add(i);
				}
			}
			return returnList;
		}

		#endregion Get Favorites

		#region Is Favorite

		public static bool IsFavorite(this User u, string pageID) {
			IEnumerable<string> s = GetFavorites(u);
			List<string> l = s.ToList();
			return l.Contains(pageID);
		}

		public static bool IsFavorite(this User u, ID pageID) {
			return IsFavorite(u, pageID.ToString());
		}

		public static bool IsFavorite(this User u, Item page) {
			return IsFavorite(u, page.ID);
		}

		#endregion Is Favorite
	}
}

Now knowing this and looking back, there's a lot of other useful things you can store for a user to help manage users who access different sites but end up in the same extranet security domain. There's also a lot of registration information that can be stored without having to modify anything. Here's to forward thinking in design.

Creating Sitecore Sublayouts Dynamically

$
0
0

Preface

On a recent project I was looking to replace some existing infrastructure that built a sidebar on a page using a list of content items (non-page items) that were selected by the content editor. The existing system was using a single sublayout which had several templated server controls that defined the display for different types of content. This was a global resource and any individual site that wanted to modify the display would have to copy the file in it's entirety even if it only wanted to modify a single line. It also didn't provide a default display for items that weren't explicitly defined. Only predefined types were displayed so there was some confusion as to why some things wouldn't be displayed.

There's, of course, a lot of ways to do this, especially within Sitecore. My plan was to keep the sidebar sublayout as the parent display object but I wanted to be able to create individual sublayouts for each content type. Then I would add each to the parent sublayout. I didn't want to load the ascx files as user controls because I wanted to be able to control the settings such as datasource, parameters, caching etc. This lead me to adding Sitecore sublayout objects to the page dynamically. It also allowed me to follow certain conventions and build the sublayouts with respect to the datasource instead of the context item.

Now if a single site wanted to override how the contact information was displayed, it would only have to override that single sublayout and it wouldn't affect anything else. It also allowed me to remove a lot of duplication in the system.

Solution

In this case I had business logic that already determined which display objects were used for which content types, I modified it to replace the server control references with paths to the sublayout files and then set that path on the Sitecore sublayouts. Here I also set the datasource on the sublayout to the item ID of the content item, I pass in a parameter to let each sublayout know what it's sort order is and then (I don't but it's possible) set the cache settings as well. Here's the ascx method I use that creates the sublayout and attaches it to the page:

protected void AddFeature(Item i, string filePath) {
	Sitecore.Web.UI.WebControls.Sublayout s = new Sitecore.Web.UI.WebControls.Sublayout();
	s.Path = filePath;
	s.DataSource = i.ID.ToString();
	s.Parameters = string.Format("Order={0}", Order.ToString());
	s.Cacheable = true;
	s.VaryByData = true;
	this.Controls.Add(s);
	Order++;
}

I'm also using a BaseSublayout class to manage the accessor properties for the Datasource and Parameters

public class BaseSublayout : System.Web.UI.UserControl {
	private Item _DataSourceItem; public Item DataSourceItem {
		get {
			if (_DataSourceItem == null)
				_DataSourceItem = Sitecore.Context.Database.GetItem(((Sublayout)Parent).DataSource);
			return _DataSourceItem;
		}
		set { _DataSourceItem = value;  }
	}
	private NameValueCollection _Parameters;
	public NameValueCollection Parameters {
		get {
			if (_Parameters == null)
				Parameters = Sitecore.Web.WebUtil.ParseUrlParameters(((Sublayout)Parent).Parameters);
			return _Parameters;
		}
		set {   _Parameters = value;  }
	}
}

Sitecore Manager and Helper Classes

$
0
0

As a follow up to my Sitecore Util Classes post, I randomly did a search through the Sitecore 6.4.1 kernel for Manager and Helper classes and as it turns out this time I hit a gold mine. Like my previous article I'll only touch on them briefly because there's just too many to go into any real depth but I wanted to at least put it out on the internets to raise a little awareness on these hidden gems in Sitecore. I left out any Interfaces, Abstract classes or classes without any public methods. In total I found 55 manager classes and 15 helper classes.

Manager Classes

Sitecore.Caching.CacheManager
public static void ClearUserProfileCache(string userName)
public static Cache[] GetAllCaches()
public static Cache FindCacheByName(string name)
public static HtmlCache GetHtmlCache(SiteContext site)

Sitecore.Data.Archiving.ArchiveManager
public static Archive GetArchive(string archiveName, Database database)
public static int GetArchiveCount(Database database)
public static List<Archive> GetArchives(Database database)

Sitecore.Data.DataManager
public DataUri[] GetItemsInWorkflowState(WorkflowInfo info)
public WorkflowInfo GetWorkflowInfo(Item item)
public Item[] SelectItems(string query)

Sitecore.Data.Fields.FieldTypeManager
public static CustomField GetField(Field field)
public static FieldType GetFieldType(string name)

Sitecore.Data.Managers.HistoryManager
public static HistoryEntryCollection GetHistory(Database database, DateTime from, DateTime to)
public static void RegisterItemCreated(Item item)
public static void RegisterItemDeleted(Item item, ID oldParentId)

Sitecore.Data.Managers.IndexingManager
public static string GetUpdateJobName(Database database)
public static void UpdateIndex(Database database)

Sitecore.Data.Managers.ItemManager
public static Item AddFromTemplate(string itemName, ID templateId, Item destination)
public static bool DeleteItem(Item item)
public static ChildList GetChildren(Item item)
public static VersionCollection GetVersions(Item item, Language language)

Sitecore.Data.Managers.LanguageManager
public static Language GetLanguage(string name, Database database)
public static bool IsLanguageNameDefined(Database database, string languageName)
public static bool RegisterLanguage(string name)
public static Language DefaultLanguage

Sitecore.Data.Managers.TemplateManager
public static bool ChangeTemplate(Item item, TemplateChangeList changes)
public static Template GetTemplate(ID templateId, Database database)
public static TemplateDictionary GetTemplates(Database database)
public static bool IsFieldPartOfTemplate(ID fieldId, Item item)
public static bool IsTemplate(Item item)

Sitecore.Data.Managers.ThemeManager
public static string GetIconImage(Item item, int width, int height, string align, string margin)
public static string GetImage(string path, int width, int height)
public static string GetLanguageImage(Language language, Database database, int width, int height)

Sitecore.Data.Proxies.ProxyManager
public static Item CopyVirtualTreeRoot(Item item, Item target)
public static Item GetItem(ID itemId, Language language, Sitecore.Data.Version version, Database database)
public static Item GetRealItem(Item item, bool preserveState)
public static bool HasVirtualItems(Item item)

Sitecore.Data.Serialization.Manager
public static void DumpItem(Item item)
public static void DumpRole(string rolename)
public static void DumpTemplates(Database database)
public static Item LoadItem(string path, LoadOptions options)
public static Role LoadRole(string path)

Sitecore.Data.StandardValuesManager
public static string GetStandardValue(Field field)
public static bool IsStandardValuesHolder(Item item)

Sitecore.Data.Validators.ValidatorManager
public static ValidatorCollection BuildValidators(ValidatorsMode mode, Item item)
public static void UpdateValidators(ValidatorCollection validators)
public static void Validate(ValidatorCollection validators, ValidatorOptions options)

Sitecore.Eventing.EventManager
public static void QueueEvent<TEvent>(TEvent @event)
public static void RaiseQueuedEvents()
public static SubscriptionId Subscribe<TEvent>(Action<TEvent> eventHandler)
public static void Unsubscribe(SubscriptionId subscriptionId)

Sitecore.Events.Hooks.HookManager
public static void LoadAll()

Sitecore.Jobs.JobManager
public static Job GetJob(Handle handle)
public static bool IsJobRunning(string jobName)
private static void RemoveJob(Job job)
public static void Start(Job job)

Sitecore.Links.LinkManager
public static string ExpandDynamicLinks(string text)
public static UrlOptions GetDefaultUrlOptions()
public static string GetDynamicUrl(Item item, LinkUrlOptions options)
public static string GetItemUrl(Item item, UrlOptions options)
public static RequestUrl ParseRequestUrl(HttpRequest request)

Sitecore.Presentation.ControlManager
public static IWebControl GetControl(string controlUri, Dictionary<string, string> parameters)

Sitecore.Presentation.PresentationManager
public static ItemPresentation GetPresentation(Item item)

Sitecore.Publishing.DistributedPublishingManager
public static PublishStatus GetStatus(Handle statusHandle)
public static Handle QueuePublish(PublishOptions[] options)
public static void StartPublishing(StartPublishingRemoteEvent @event)
public static void UpdatePublishStatus()

Sitecore.Publishing.PreviewManager
public static string GetShellUser()
public static void ResetShellUser()
public static void SetUserContext()

Sitecore.Publishing.PublishManager
public static void AddToPublishQueue(Item item, ItemUpdateType updateType)
public static bool CleanupPublishQueue(DateTime to, Database database)
public static void ClearPublishQueue(Database database)
public static ItemList GetPublishingTargets(Database database)
public static PublishStatus GetStatus(Handle handle)
public static Handle RebuildDatabase(Database source, Database[] targets)

Sitecore.Resources.Media.MediaManager
public static Sitecore.Resources.Media.Media GetMedia(MediaItem item)
public static string GetMediaUrl(MediaItem item)
public static string GetThumbnailUrl(MediaItem item)
public static bool HasMediaContent(Item item)
public static bool IsMediaRequest(HttpRequest httpRequest)

Sitecore.Resources.Media.MediaPathManager
public static string GetExtension(string file)
public static string GetMediaFilePath(string itemPath, string extension)

Sitecore.Search.SearchManager
public static Index GetIndex(string id)
public static object GetObject(SearchResult hit)

Sitecore.Security.AccessControl.AccessRightManager
public static AccessRight GetAccessRight(string accessRightName)
public static AccessRightCollection GetAccessRights()
public static bool IsApplicable(AccessRight accessRight, ISecurable entity)

Sitecore.Security.AccessControl.AuthorizationManager
public static AccessResult GetAccess(ISecurable entity, Account account, AccessRight accessRight)
public static AccessRuleCollection GetAccessRules(ISecurable entity)
public static bool IsAllowed(ISecurable entity, AccessRight right, Account account)
public static bool IsDenied(ISecurable entity, AccessRight right, Account account)
public static void RemoveAccessRules(ISecurable entity)
public static void SetAccessRules(ISecurable entity, AccessRuleCollection rules)

Sitecore.Security.Accounts.RolesInRolesManager
public static void AddRolesToRole(IEnumerable<Role> memberRoles, Role targetRole)
public static IEnumerable<Role> FindRolesInRole(Role targetRole, string roleNameToMatch, bool includeIndirectMembership)
public static Role GetEveryoneRole()
public static IEnumerable<Account> GetRoleMembers(Role role, bool includeIndirectMembership)
public static bool IsUserInRole(User user, Role targetRole, bool includeIndirectMembership)

Sitecore.Security.Accounts.UserManager
public static int GetUserCount()
public static IFilterable<User> GetUsers()

Sitecore.Security.Authentication.AuthenticationManager
public static User BuildVirtualUser(string userName, bool isAuthenticated)
public static User GetActiveUser()
public static bool Login(string userName, string password, bool persistent)
public static void Logout()
public static void SetActiveUser(User user)

Sitecore.Security.Authentication.HttpAuthenticationManager
public static bool Authenticate(HttpRequest request)
public static bool IsAuthorized(HttpRequest request)

Sitecore.SecurityModel.DomainManager
public static void AddDomain(string domainName)
public static bool DomainExists(string domainName)
public static DomainConfiguration GetConfiguration()
public static Domain GetDomain(string name)
public static Domain GetDomainForUser(string userName)

Sitecore.SecurityModel.License.LicenseManager
public static void BlockSystem(string reason)
public static void ResetLicense()
public static long GetDatabaseSize(Database database)
public static int GetEditorUserCount()

Sitecore.Shell.Applications.ContentEditor.Galleries.GalleryManager
public static void GetGallerySize(string id, ref string width, ref string height)
public static void SetGallerySizes()

Sitecore.Shell.Applications.ContentEditor.Gutters.GutterManager
public static List<ID> GetActiveRendererIDs()
public static List<GutterRenderer> GetRenderers()
public static string Render(List<GutterRenderer> gutterRenderers, Item item)

Sitecore.Shell.Feeds.ClientFeedManager
public static ClientFeed GetFeed(string feedname)
public static SyndicationFeed GetFeedsAreDisabledFeed()

Sitecore.Shell.Framework.Commands.CommandManager
public static Command GetCommand(string name)
public static CommandState QueryState(Command command, CommandContext context)
public static void RegisterCommand(string name, Command command)

Sitecore.Shell.Framework.Commands.OpenSecurityManager
public override void Execute(CommandContext context)
public override CommandState QueryState(CommandContext context)

Sitecore.Shell.Framework.Commands.Shell.DomainManager
public override void Execute(CommandContext context)
public override CommandState QueryState(CommandContext context)

Sitecore.Shell.Framework.Commands.Shell.RoleManager
public override void Execute(CommandContext context)
public override CommandState QueryState(CommandContext context)

Sitecore.Shell.Framework.Commands.Shell.UserManager
public override void Execute(CommandContext context)
public override CommandState QueryState(CommandContext context)

Sitecore.Sites.SiteManager
public static Site GetSite(string siteName)
public static SiteCollection GetSites()

Sitecore.Syndication.FeedManager
public static PublicFeed GetFeed(Item item)
public static string GetFeedUrl(Item feedItem, bool includeUserAuthentication)
public static string Render(SyndicationFeed feed)

Sitecore.Visualization.LayoutManager
public static Rendering GetRendering(string fullName, Database database)

Sitecore.Visualization.RuleManager
public static void Execute(object thisObject)
public static RuleSet GetRuleSet(object thisObject)

Sitecore.Web.Authentication.TicketManager
public static string CreateTicket(string userName, string startUrl)
public static bool Relogin(string ticketId)

Sitecore.Web.UI.HtmlControls.PageScriptManager
public bool AddScripts
public bool AddStylesheets
public List<ScriptFile> ScriptFiles
public List<StylesheetFile> StylesheetFiles

Sitecore.Web.UI.WebControls.AjaxScriptManager
public void Dispatch(string parameters)
public ClientCommand Alert(string text)
public ClientCommand Download(string file)
public ClientCommand SetInnerHtml(string id, string value)

Sitecore.Web.UI.WebControls.ContinuationManager
public static void Register(string key, ISupportsContinuation @object)
public Pipeline Start(Pipeline pipeline, PipelineArgs args)

Sitecore.Web.UI.WebControls.MessageManager
public object SendMessage(Message message)
public object SendMessage(object sender, string message)

Sitecore.Web.UI.XamlSharp.ControlManager
public static Control GetControl(ControlUri uri)
public static string GetControlUrl(ControlName name)

Sitecore.Web.UI.XamlSharp.Xaml.XamlManager
public static AttributeAssigner GetAttributeAssigner(System.Xml.XmlAttribute attribute)
public static Compiler GetCompiler(ControlDefinition definition)
public static ControlCompiler GetControlCompiler(CompilerOptions options)
public static Sitecore.Web.UI.XamlSharp.Xaml.Extensions.Extensions GetExtensions()

Sitecore.Web.UI.XmlControls.ModeManager
public static ModeMatch Match(ArrayList modes, HashList<string, object> requested)

Sitecore.WordOCX.WordOCXUrlManager
public string GetDownloadLink()
public string GetEditLink()
public string GetPreviewLink()
public string GetUploadLink()

Helper Classes

Sitecore.Common.PagingHelper
public List<TOutput> GetRecords(int pageIndex, int pageSize, IEnumerable<TInput> inputList, GetElements<TInput, TOutput> getElements, out int totalRecords)

Sitecore.Configuration.ProviderHelper
public string GetRootAttribute(string attributeName)

Sitecore.Data.DataProviders.SqlServer.SqlServerHelper
public static int GetInt(SqlDataReader reader, int columnIndex)
public static long GetLong(SqlDataReader reader, int columnIndex)

Sitecore.Links.LinkDatabaseHelper
public static void UpdateLink(Item item)

Sitecore.Publishing.PublishHelper
public bool CanPublish(ID itemId, User user, ref string explanation)
public virtual PublishItemResult DeleteTargetItem(ID itemId)
public virtual Item GetSourceItem(ID itemId)
public virtual Item GetTargetItem(ID itemId)
public virtual Item GetVersionToPublish(Item sourceItem)
public PublishItemResult PublishSharedFields(Item sourceItem)
public bool SourceItemExists(ID itemId)
public bool TargetItemExists(ID itemId)

Sitecore.Reflection.NexusHelper
public static FastInvoker GetInvoker(string typeName, string methodName, bool isStatic)

Sitecore.Security.AccessControl.FieldAuthorizationHelper
public override AccessRuleCollection GetAccessRules(Field field)
public override void SetAccessRules(Field field, AccessRuleCollection rules)

Sitecore.Security.AccessControl.ItemAuthorizationHelper
public override AccessRuleCollection GetAccessRules(Item item)
public override void SetAccessRules(Item item, AccessRuleCollection rules)

Sitecore.Security.Authentication.AuthenticationHelper
public virtual bool CheckLegacyPassword(User user, string password)
public virtual User GetActiveUser()
public virtual void SetActiveUser(User user)
public virtual bool ValidateUser(string userName, string password)

Sitecore.Security.SecurityHelper
public static bool CanRunApplication(string applicationName)

Sitecore.SecurityModel.License.RSAHelper
public string Decrypt(string message)
public string Encrypt(string message)

Sitecore.Text.NVelocity.VelocityHelper
public static string Evaluate(VelocityContext context, string template, string logName)
public static void PopulateFromSitecoreContext(VelocityContext context)

Sitecore.Web.IPAddresses.IPHelper
public static IPList GetIPList(System.Xml.XmlNode node)

Sitecore.Xml.Xsl.SqlHelper
public string connect(string connection)
public string fullsearch(string search, string table, string displayFields, string searchFields)
public string select(string sql)

Sitecore.Xml.Xsl.XslHelper
public virtual string Application(string name)
public virtual string day()
public virtual string displayname(XPathNodeIterator iterator)
public virtual string EnterSecurityState(bool enabled)
public virtual string ExitSecurityState()
public virtual string formatdate(string isoDate, string format)
public virtual string fullUrl(string fieldName, XPathNodeIterator ni)
public virtual string GetDevice()
public virtual Item GetItem(XPathNodeIterator iterator)

Designing a Page Editor Experience: 1 - Setting up the Code

$
0
0

Shout Out

As a shout out, this series started because I was recently speaking with Mark Ursino and he gave me a demonstration of a recent build where he had taken advantage of a few really slick features in Page Editor. I decided to do some research to see what Page Editor could really do. In doing my research I came across a lot of great posts that individually detail pieces of functionality, some of which I'd seen demonstrated at the last Symposium. Others go in depth to explain why certain choices are lend to a better framework. For this post, I wanted to weave together as much of a cohesive picture along with some commentary and lots of referential links for developers to see a fuller Page Editor experience.

Preface

Before I begin, I want to bring to light some things to consider. The Page Editor has evolved to allow you to you store related content in DataSource fields on sublayouts as opposed to storing this information in fields, which is traditionally how many Sitecore sites were built. It's important to understand how the relationships between DataSources are stored in Sitecore and how they're changing between Sitecore 6.6 and 7. In 6.6 the DataSource for a sublayout is stored in the presentation details using the item's path (ie: /sitecore/content/home/pagename). As of 7 this was changed so that the DataSource is stored in the link database as an item ID. This is important for a few different reasons:

  • if you move, rename or remove a data source item, any sublayouts storing the item's path will not be updated, the values will not be easy to cleanup and if you don't code your pages correctly, you could have exceptions being thrown when your page tries to access fields on null objects.
  • as sublayouts are added to the site and items are created as data sources, if a user removes the sublayouts from a page, the items will likely not be removed with them and you'll have to figure out a process for garbage collection on this content.
  • if and when you upgrade from 6.6 to 7, consider that you will want to convert your system from storing the datasources in the presentation details to the link database. I'm not sure if there is any formal way to do this or not yet so do your research.

If you do choose to go ahead with 6.6 and store the DataSource information in the Presentation Details of a page, one thing you can do to help yourself is modify your system so that Sitecore stores the item ID's instead of their paths. This should at least make it somewhat easier to convert and more importantly much less prone to breakage.

Also, if you rely on Lucene search, consider that when you're storing the relationships between a page and it's content in the Presentation Details or link database, you're going to face new challenges to associate this content to a page. With 6.6 you'll need to figure out how to extract the DataSource ID's from the xml in the Layout field and with 7 you'll have to figure out how to gather the information from the link database.

Getting Started

Ok, with that said, whether or not you choose to store content relationships in the presentation details, Sitecore's Page Editor, has a lot to offer. When you get started thinking about designing an experience for Page Editor your journey will start with the code itself. There's a lot of great posts coming out focusing on component based approach including this really thorough series. This push has been for reuse and modularity but also more importantly to support the tools provided by Sitecore for tracking and personalization. What this means is that when you're designing your templates you should do so with consideration towards breaking up the page into reusable components whose content is interchangeable. Components are a general reference to user control sublayouts, xsl renderings and library server controls. I prefer working with sublayouts so going forward my examples will use them but there are other options.

On each of the sublayouts (renderings or server controls), to make the content fields accessible you should be using FieldRenderers. They live in the Sitecore.Web.UI.WebControls namespace and there are several controls built specifically for different field types: Date, Link etc. To learn more about these controls you can bone up a bit by perusing the Presentation Component Reference(System Overview), the Presentation Component API Cookbook(In-Depth Developer Reference) and the Presentation Component Cookbook(Working inside Sitecore)
Otherwise if you need to customize the output you may implement your own by extending the FieldControl class or higher(IE: FieldRenderer).

Once you've wired up your UI, following the path of the sublayout design patterns emerging, you'll want to setup a base class for your sublayouts that provide access to the sublayout parameters and datasource(as opposed to using the Sitecore.Context.Item). Below I've provided a sample base class that provides access to a DataSourceItem, ContextItem and a DataSourceOrContext. DataSourceOrContext will fall back to the ContextItem if the DataSourceItem is null. The reason for this is because by default, FieldRenderers select the Sitecore.Context.Item as their datasource but that might not be preferred. By using this type of base class provides a way for sublayout to specifically decide which source it is targeting by overriding the PreferredDataSource which if untouched, defaults to DataSourceOrContext. This class then recursively sets the datasource for all FieldRenderers on the sublayout. I sourced the logic for this from this great post. As an example, if you change the PreferredDataSource item to use only the DataSourceItem, the recursive function will hide the controls if the DataSourceItem is null so that the FieldRenderer's won't attempt to display data from the Sitecore.Context.Item.

public class BaseSublayout : System.Web.UI.UserControl {
	private Item _DataSourceItem;
	public Item DataSourceItem {
		get {
			if (_DataSourceItem == null)
				_DataSourceItem = Sitecore.Context.Database.GetItem(((Sublayout)Parent).DataSource);
			return _DataSourceItem;
		}
		set { _DataSourceItem = value;  }
	}

	public Item ContextItem {
		get {
			return Sitecore.Context.Item;
		}
	}

	private Item _DataSourceOrContext;
	public Item DataSourceOrContext {
		get {
			if (_DataSourceOrContext == null)
				_DataSourceOrContext = (DataSourceItem != null) ? DataSourceItem : ContextItem;
			return _DataSourceOrContext;
		}
	}

	private NameValueCollection _Parameters;
	public NameValueCollection Parameters {
		get {
			if (_Parameters == null)
				Parameters = Sitecore.Web.WebUtil.ParseUrlParameters(((Sublayout)Parent).Parameters);
			return _Parameters;
		}
		set {   _Parameters = value;  }
	}

	public virtual Item PreferredDataSource { get { return DataSourceOrContext; } }

	protected void ApplyDataSource() {
		foreach (Control c in Controls)
			RecurseControls(c, PreferredDataSource);
	}

	private void RecurseControls(Control parent, Item dsItem) {
		string typeName = parent.GetType().FullName;
		if (dsItem == null) {
			parent.Visible = false;
		} else if (typeName.StartsWith("Sitecore.Web.UI.WebControls")) {
			if(typeName.EndsWith("FieldRenderer"){
				Sitecore.Web.UI.WebControls.FieldRenderer f = (FieldRenderer)parent;
				f.Item = dsItem;
			} else {
				Sitecore.Web.UI.WebControls.FieldControl f = (FieldControl)parent;
				f.Item = dsItem;
			}
		}

		foreach (Control c in parent.Controls)
			RecurseControls(c, dsItem);
	}

	protected virtual void Page_Load(object sender, EventArgs e) {
		ApplyDataSource();
	}
}

I'm also bootstrapping the recursive function in the Page_Load method so if you want to work within that method on the local instance you'll want to override the method but make sure you call the Page_Load method from the base class:

protected override void Page_Load(object sender, EventArgs e) {
	... your code ...
	base.Page_Load(sender, e);
} 

Designing a Page Editor Experience: 2 - Sublayout fields

$
0
0

After you've decided what components your pages will be broken up into and created the sublayouts (renderings or server controls) each extending your base class, you can begin working on the setup inside Sitecore by configuring your component item's fields and setting up component insert options. These settings are stored on the sublayout items themselves and I'll detail them here.

The "Description" field is largely for you to provide a description of what the control does. I'm not aware of any place it is displayed but it's possible that it has other uses.

The "Parameter Templates" is one of the most interesting concepts. The intention is to allow developers the ability to customize the user interface for entering parameter values. If the default key/value UI control doesn't meet your needs you can attach a template to provide you with additional fields that you do need. First create a new template.

parameter template create

Add some fields.

parameter template fields

And set the new template to inherit from the Sitecore template: /System/Layout/Rendering Parameters/Standard Rendering Parameters.

parameter template base template 

Then you'll want to set the parameter template field on the sublayout item

parameter template set template 

Please note this is not a comprehensive guide to creating a template. I'm going to skip adding icons, standard values tokens and help text for convenience here but you should not. Now when you're working with a sublayout and you click "Edit" on the sublayout in the Presentation Details or in the Page Editor More->Edit the Component Properties, you'll see the new field section.

parameter template edit fields 

These values will then be accessible through the Parameters property defined on the BaseSublayout class provided in the previous post

The "Open Properties After Add" field allows you to choose if the component properties (datasource, caching, params etc.) windows will be opened automatically when a content editor adds a sublayout to a page. It has values of "Default", "Yes" and "No".

open properties on add 

These value affect the behavior of the checkbox "Open the properties dialog box immediately" when you insert a component/sublayout

insert sublayout 

A "Default" value will allow the content editor to choose. A "Yes" value disables the checkbox and automatically checks the box while a "No" value also disables the checkbox and keeps it unchecked.

The "Customize Page" field is apparently a legacy field. Quoting from the Presentation Component Reference: "The Customize Page property in the Editor Options section of rendering definition items is a legacy replaced by rendering settings data templates." Basically if you haven't used it yet, don't start.

The "Editable" field will prevent a content editor from being able to insert the sublayout through the Page Editor although an admin still has access and can add the sublayout through Presentation Details.

The "Datasource Location" field is used similar to the source on a template field. It's the location in the content tree where Sitecore will start a TreeList for you to select a data source item from when you click a to set the DataSource field on a sublayout. By default if you insert a sublayout you will not be asked to select a datasource. You can optionally edit the component properties to do so but if you select an item for this field you'll automatically see a popup asking you to choose a data source for the sublayout you're inserting.  Here I'll set the datasource to an item just under the home item.

 datasource location set

You'll see that the result tree now starts at that item.

datasource location select
  
The "Datasource Template" field is used to limit what types of content can be selected as a datasource. When you're selecting the datasource, if you select an unsupported type you'll receive a nice friendly message.

datasource template error 

Unfortunately the value for this field is stored as an internal link field and you can only select one template type. That being said, you may only need to select one template but pray you never rename your templates because this link will not update automatically.

The "Compatible Renderings" field is intended for situations where you want to swap out an existing sublayout but keep the datasource. You'll want to take note that this works best by selecing renderings that have use the same Datasource Templates so that if you swap it out the sublayout doesn't throw errors because the sublayout is expecting a different data source item type. To set this up, you first select a sublayout from the compatible renderings field.

compatible rendering field 

Then in the Page Editor after you've inserted this sublayout and click on it, you'll see a red and green arrow button with tooltip text: "Replace with another component".

compatible rendering change 

This will popup a dialog window for you to select fromt he sublayouts you've chosen

compatible rendering sublayout

The "Page Editor Buttons" field will allow you to add buttons to the detail window that displays when you select a sublayout in the Page Editor. There are two types of buttons: WebEdit buttons makes calls to the commands from the commands config file and Field Editor Buttons open a window that let you edit a specific set of fields. By default the bar is pretty light and you're only provided WebEdit Buttons to choose from.

web edit button types 

Each has it's own unique function:

  • the insert button will give you a popup that allows you to insert items for the context item.
    web edit button insert 
  • the delete button allows you to delete the context item (as long as it isn't the site start item)
    web edit button delete 
  • the sort button allows you to sort the child items through buttons or a drag and drop interface (if the browser supports it)
    web edit button sort 
  • the move up/move down moves the context item up and down in the Sitecore tree

If and when you decide you need some custom functionality available or you want to open up access to fields that aren't for display such as metadata fields, you'll want to jump to the Core database. The templates for the two button types are the "WebEdit Button" or the "Field Editor Button" and are both stored under: /core/sitecore/templates/System/WebEdit. 

web edit button templates 

You'll want to be creating them under: /core/sitecore/content/Applications/WebEdit/Custom Experience Buttons. The fields for a WebEdit Button are Header, Icon, Click(name from command file), Tooltip and Parameters.

web edit fields 

If you want to create a Field Editor Button there's a bit of setup involved so I'll walk through the steps. I'll note that the fields accessed will be on the Datasource item if it's been set so in this case metadata fields are only going to be useful if the datasource or context item is a page.

  • First you'll need to define meta data fields on a template
    template fields
  • Then jump into the core database and drill down to: /core/sitecore/content/Applications/WebEdit/Custom Experience Buttons
  • Insert an item of template type: /core/sitecore/templates/System/WebEdit/Field Editor Button
    field editor template
  • Set the fields you want to edit on the item. If there are multiple, separate the field names using the pipe(|) delimiter
    field editor item
  • Jump back into the master database assign the button to a sublayout item
    edit sublayout buttons
  • Open up Page Editor and select a sublayout with the button on it
    editor button
  • Click the button and you'll see a popup with the fields available to edit.
    field editor

For another article that specifically caters to this feature, you can read up here.

The "Enable Datasource Query" field is available in Sitecore 7 and I to be honest I don't know what it does yet. It's likely related to the new field type that Sitecore is going to replace the Datasource field with that John West explains here.

As for the "Data" and "Caching" sections of the sublayout, I'm going to gloss over them since they're not likely to be used as often. They store the same component property values that you select when you add a sublayout to a page but this will set them globally for all uses. It's definitely possible that you could find this useful but generally you'll want to set these values specifically where they're applied to a page or standard values item.

The "Login control" field is one exception to the previous sections that I will explain. I dug into the Sitecore.Kernel and found that this is a level of security that allows you to show another sublayout if !Context.User.Identity.IsAuthenticated. To enable access, you can authenticate users with a couple of quick lines of code:

string domainUser = Sitecore.Context.Domain.Name + @"\username";
bool loginResult = AuthenticationManager.Login(domainUser, "password");

This can be used for an ad-hoc extranet which you can manage permissions for different groups. I have a more extensive guide for Setting up a Sitecore Extranet which explains the process in more detail.


Designing a Page Editor Experience: 3 - Placeholder Settings

$
0
0

Placeholder settings are used to create predefined insert options for components (sublayouts, renderings and web controls). You have two basic approaches to use: setting the placeholder key on the item's Placeholder Key field: 

placeholder settings global key 

or setting the key when you apply it to an item through it's presentation details.

placeholder settings local set

Setting the placeholder key on the placeholder settings item will set the component insert options of any placeholders with a matching key. This could be useful for global elements or in single site deployments where you're not concerned about managing individual settings for each site.

Setting the placeholder key on the standard values of a template will allow you to set the same group of component insert options on different templates or placeholders. This, I suspect, may be most useful especially in multi-site instances since it would allow you to specify the sublayouts used for your specific site's placeholder keys.

There are a two out-of-the-box placeholder setting items in Sitecore: content and webedit. You can incorporate these into your strategy or create your own. I'll demonstrate the latter. Browse to /master/sitecore/content/Layout/Placeholder Settings. Right-click on the placeholder settings item and insert a new Placeholder item.

placeholder settings insert

If you're going to set the placeholder key on the placeholder setting item you may want to name the item based on the content placeholder key it will be used for. On the other hand if you're going to be specifying the placeholder key in standard values, you'll want to name it based on its intended use. IE: Sidebar Controls, Header Content etc. The new item has four fields: Placeholder Key, Allowed Controls, Description and Editable.

As mentioned earlier, the Placeholder Key field is optional. If it is set Sitecore will apply the component insert options to any placeholders with a key that matches.

The Allowed Controls field allows you to select the components that you want to be available to an editor when they click on the "Add to here" buttons in the Page Editor. For this example I'm going to add my New Sublayout under allowed controls. So on the field I'll click the "edit" button and will receive this dialog window.

placeholder settings allowed

Then click "Ok" and you'll see that the field has selected the item.

placeholder setting fields

The Description field appears to be informational. I didn't find any place where it was visible other than on the control itself.

The Editable checkbox determines if the placeholder settings will be editable from the presentation details windows. Here I've unchecked the checkbox and you can see the edit option is greyed out. 

placeholder settings editable

I'll want to also add the placeholder setting to the standard values of an item. I'm going to open the presentation details and then I'll click on the edit button for the default device.

placeholder settings presentation details 

Then I'll select the Placeholder Settings section and click add.

placeholder settings add

From Here I can select the New Sublayout item and enter in "content" for the Placeholder Key.

placeholder settings local set 

Now if I go to the Page editor and click on the blue component button to insert a component. I'll see the "Add to here" buttons.

placeholder settings add component 

When I click on the "Add to here" button, I'll now only be given the New Sublayout as an option.

placeholder settings option

Designing a Page Editor Experience: 4 - Personalization and Multivariate Testing

$
0
0

One of the nicest features that came along in the last few Sitecore versions was the rules engine. Along with that power was the ability to harness the conditional sublayout rendering to change the datasource or component design based on specific visitor information, otherwise known as personalization. There's all kinds of identifiable markers that are available to you about your end users such as location and search keywords. You can use this information as is to create rules, you can create "profiles" which categorize your users to help you reach your intended audience and you can use those "profiles" to build "Engagement Plans". I don't plan on going into detail about how to create these profiles or engagement plans since it's a near science unto itself but I did find a quick guide on personalization terms and an excellent articles on setting up personalization profiles and creating engagement plans.

Another powerful feature is Multivariate Testing. Multivariate Testing allows you to determine which configuration for a component (datasource and design) is best meeting your goals. Again I'm not going to detail how to develop a strategy for testing because there are articles dedicated to explaining multivariate testing, and optimizing your testing.

If you're going to use the personalization or multivariate testing aspect of the rules engine, you will need to have a special Sitecore license since it's a separate product. If you don't have a license you'll be wondering why you don't see these options in your system.

Once you've decide on how you're going to target key demographic groups, you're ready to begin applying some of the rules to your components(sublayouts, renderings, web controls). These rules are applied in the presentation details window of an item. When you open up the presentation details window is that there are two new buttons: Presonalize and Test.

presentation details

Personalize

If you select the controls section then select a component and click on the Personalize button, you'll get the personalization window.

conditions

What you're seeing is a list of the conditions(rules) that will determine how and what the sublayout displays. You're provided a default condition which doesn't contain a condition or rule since it's the default condition and will be used when no other more specific condition is met. All conditions allow you to hide the component, personalize the content(change the datasource) or personalize the component(change the sublayout). If you choose to personalize the content, you'll be provided with another dialog window to choose a content item from a tree which is sourced from the sublayout's Datasource Location field.

set content

If you click the Enable personalization of component design, you'll see a Personalize Component section appear.

Enable personalization of component design

By clicking on the grey "..." button at the bottom of that block, you'll be prompted to select a new sublayout.

Enable personalization of component design

When you want to create a new condition, you'll start by clicking on the New Condition button. Another condition section will be added which you can then name.

new condition

The hide component and personalize content ares are provided but there are also a few other buttons. There is an action button which allows you to remove or rename the item.

actions

There's also an Edit button in the condition section.

edit condition

If you click the Edit button, you'll be shown another popup that will allow you to build a condition from a series of rules.

edit rules

Here I've chosen to build a condition for visitors from Boston searching for the text "Sitecore" and changed the datasource to child1.

conditions

Once I click ok, you'll move back a step and find that the icon for the sublayout you just personalized has just changed to display a "2" to indicate that there are now two conditions on it.

presentation details

All of this is of course accessible from the Page Editor as well and in some ways is easier to work with from the Page Editor. If you click on a sublayout, you'll see the border with the helpful toolbar but with the personalization license there are a few new items. First is the button to personalize the content.

page editor buttons

There's also a button to add A/B tests.

page editor button

Then there's a bar that helps you navigate the conditions associated with the item.

page editor button bar

The arrow allows you to change the condition your viewing so you can quickly scroll through them and see how they'll each look. There is a numerical indicator next to it so that you'll know what number you're on in the list. The drop down area provides a summary of the condition rules and allows you to jump directly to an individual condition. There's also an Edit Conditions button that will popup a dialog window to edit the conditions.

You can also set some specific personalization settings through the component properties which are available in the presentation details when you click to edit a component.

component properties

If you have the license for this but don't see the fields, you can try the solution described here.

Tests

If you select the controls section then select a component and click on the Test button, you'll get the variation window.

new test

If you click on the New Variation button, you'll see a new variation item appear which you can name and set the properties (datasource and sublayout) just like with personalization.

test

You'll need to have at least two test variations for the OK button to be accessible.

tests

Unlike when you personalization a control, you won't be provided any icon but you will notice that the personalize button is now greyed out.

greyed out personalize button

Again, you can work with these settings from within the Page Editor which can sometimes be easier.

test button bar

Unlike the personalization, you'll have a new toolbar in the Page Editor at your disposal: Testing.

test toolbar

In the toolbar at the top, there's a Components and Combinations field that will highlight the sublayout that it refers to on the page when you click on it.

select test

Tests must be started so when you're ready to run it you'll want to click on the Start Test button. When you do you'll be provided a friendly warning to make sure you want to proceed and ask you to provide a name for the test.

start test

All your tests will be stored in Sitecore and can be viewed in the Marketing Center application available from the Sitecore start menu.

marketing center

While the test is running you'll see the toolbar change and give you the ability to stop it with the Stop Test button.

run test

When you click on the stop test, you'll see a popup that will ask you how to handle the components being tested. The form will allow you to select which component setup you want to keep and the numbers along the right will indicate their performance based on your goals. Here they're all zero but the higher number would indicate better performance.

stop test

Choose a single item comprised of combinations.

stop test combinations

Or you can stop the test and revert to the original design by clicking on a text link at the bottom of the window. This last option will remove the tests from the sublayouts and return you to the original state of the component before you added tests.

Sheer UI: 1 - A Tale of Two Systems

$
0
0

Preface

My journey began with a rebuild of a Sheer UI Wizard. I didn't build it, but I was about to learn to. After finding a dearth of literature on Sheer and realizing the documentation was a little out of date (I'm guessing SC 4), I knew I was going to write something on it. When I began writing I thought I knew a fair amount about Sheer from my experience creating different utilities but as it turns out, I knew very little. Though, from some of the applications I've seen, there seems to be a few who have faced the abyss and become steely Sheer developers, I'm betting there are more like myself with passable knowledge but not the whole picture. There was a recently released video walkthrough from the 24 hours of Sitecore led by Robert Hock that details setting up a Sheer UI application that's also worth watching and a video of the presentation I gave to the Sitecore User Group.

I should mention that Sitecore is currently moving away from Sheer toward using Speak for a UI framework. It's initial release is in Sitecore 7.1 but up until you make it there or for backward compatibility with existing tools, you'll still need to work with Sheer. 

Sheer UI

So what is Sheer UI? Sitecore, in all their industry, built a framework that is used for creating the UI of Sitecore itself. The applications, development tools, security editors among other things are built using XML Controls, although there are a few .aspx pages used also. They work similar to .NET pages in that they have a front and back end file, page lifecycle and events that you can work with. You can add to Sitecore's UI by creating your own XML controls for use in Applications, Item Editor Tabs, Rich Text Editor Buttons, overriding the existing controls and an infinite number of other ways.

XML Controls

XML controls are structured presentation XML markup and can also reference a class for functionality. Each XML node in that control, after the first and second root nodes are references to other XML controls or library web controls. Sitecore supports having more than one XML control per file but it's recommended to separate them into individual files. Sitecore stores it's controls in two locations: /sitecore/shell/Applications and /sitecore/shell/Controls

Now here's where the world that I knew, began to look like a pale blue dot. There really are two supported types of XML control syntax: The original "controls" and the newer "xamlControls". For the release of version 5.3, Sitecore redesigned how they managed XML controls altogether and added a new handler extension to resolve the new compilation model. There was also a XamlSharp.config file that exposed configuration options for many settings including the compiler itself. There was a lot of thought put into it and it was big improvement but I'm aware of no documentation for it.

Structure

So what do these XML controls look like? Here's an example of the original "control" type using common elements:

<control
     xmlns:def="Definition"
     xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">

     <ControlName>
          <Stylesheet Src="/sitecore modules/shell/YourApplication/css/style.css"/>
          <Script Src="/sitecore modules/shell/YourApplication/js/script.js"/>
          <FormPage>
               <CodeBeside Type="Namespace.Class,Assembly"/>
               <GridPanel Height="100%" Width="100%" runat="server">
                    <Border ID="Container" runat="server">
                         <Literal ID="ltl" Text="Hello" runat="server" />
                    </Border>
               </GridPanel>
          </FormPage>
     </ControlName>
</control> 

A "control" file's extension is .xml. It's root node name can be anything but the convention is to name it "control". On the "control" node you do need to have either of the two attributes shown in the example for Sitecore to recognize it as an XML control. The text of it should be copied verbatim.

Here's an example of the newer "xamlControl" using common elements:

<xamlControls
     xmlns:x="http://www.sitecore.net/xaml"
     xmlns:ajax="http://www.sitecore.net/ajax"
     xmlns:rest="http://www.sitecore.net/rest"
     xmlns:r="http://www.sitecore.net/renderings"
     xmlns:xmlcontrol="http://www.sitecore.net/xmlcontrols"
     xmlns:p="http://schemas.sitecore.net/Visual-Studio-Intellisense"
     xmlns:asp="http://www.sitecore.net/microsoft/webcontrols"
     xmlns:html="http://www.sitecore.net/microsoft/htmlcontrols"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

     <XamlHelloWorld x:inherits="Namespace.Class,Assembly">
          <Sitecore.Controls.HtmlPage runat="server"> <Stylesheet runat="server" Src="/sitecore modules/Shell/ApplicationName/css/style.css" x:placeholder="Stylesheets"/>
               <Script runat="server" Src="/sitecore modules/Shell/ApplicationName/js/script.js" x:placeholder="Scripts"/>
               <GridPanel Height="100%" Width="100%" runat="server">
                    <Border ID="Container" runat="server">
                         <Literal ID="ltl" Text="Hello" runat="server" />
                    </Border>
               </GridPanel>
          </Sitecore.Controls.HtmlPage>
     </XamlHelloWorld>
</xamlControls> 

Unlike the older "control", "xamlControls" file extensions' are .xaml.xml and their root node name must be named "xamlControls" otherwise you'll be the beneficiary of a blue screen: Xaml files must have a root node named "xamlControls". Well technically it's yellow, but blue sounded better. The attributes on the root node are used to help with intellisense and to help Sitecore know where too look for different elements. The xmlns (XML Namespace) attribute in the opening xml tag defines the prefix and the namespace. The prefix is local to that xml node in the file so it can be anything but the namespace is important. The "Definition" namespace used on "control" items were built into the early compiler but the "xamlControls" uses namespaces that are defined in the XamlSharp.config file under the <attributeAssigners> section. Now I'm still a little fuzzy on the entirety of the inner workings of how the compiler uses all these elements to build controls but from the limited time I have, that seems to be what's going on.

For both types of controls, the first child of the root node is where the control name is defined. The name is important since Sitecore references the control by name. Each control should have a unique name because if there are duplicates only the first one found by Sitecore will be used and the rest will be ignored. In regular use, since control names should be unique, it may be advantageous to name the control with a namespace which appears to be the convention on newer controls.

You should understand how each types loads supporting classes. The "control" and "xamlControl" types can use def:inherits or x:inherits respectively. Also the "control" types support the <CodeBeside> tag to define a class but the "xamlControl" does not. I'm not sure why this is so, since other controls in the same namespace will work but I'm chalking this one up to the inner mysteries of the compiler. One thing I do know is that neither the "control" nor the "xamlControl" support each other's type as an inner control. Take the use of the <FormPage> and <Sitecore.Controls.HtmlPage> controls as an example. The <FormPage> is a "control" type that lives here: /sitecore/shell/controls/Standard/Form Page.xml and the <Sitecore.Controls.HtmlPage> is a "xamlControls" type that lives here: /sitecore/shell/Applications/Xaml/Controls/HtmlPage.xaml.xml. You could not use the <Sitecore.Controls.HtmlPage> in a "control" nor could you use the <FormPage> in a "xamlControl".

Rendering

Another improvement made, was how Sitecore loads the control. For both methods you will need to be logged in to use the tools because they both run on the shell website.

"controls" can be previewed by the url: http://localhost/sitecore/shell/default.aspx?xmlcontrol=CaseSensitiveControlName 

"xamlControls" can be viewed by the handler url: http://localhost/sitecore/shell/~/xaml/ControlName.aspx. In some contexts in Sitecore, there are also querystring parameters such as: ID, Language, Version and Database. This handler url is configured in the <customHandlers> Sitecore section of the web.config.  

When you load a Sheer XML control, Sitecore, after first checking for the control in cache, will convert the control into an interim class file that inherits fromSitecore.Web.UI.XmlControls.XmlControl for "control" types and Sitecore.Web.UI.XamlSharp.Xaml.XamlControl for "xamlControls" respectively. This class is then compiled into its own assembly, an instance created and then rendered to a page. The compiled classes only recompile when the XML file is modified sufficiently, allowing you the flexibility of modifying the layout without having to recompile your project.

Configuration

There's a few web.config settings that can be set to modify XML control behavior in your system. I'm not sure if this is universal to both systems or not.

  • disableXmlControls (<sites>): If set to true, loading Xml Controls as pages will be disabled.
  • XmlControls.ThrowException (<settings>): Specifies if an exception is thrown when an Xml Control can't be loaded. Errors are written to the log.
  • XmlControls.OutputDebugFiles (<settings>) : If true, the *.cs files will be saved in to the debug folders associated with the XmlControls

"control" configuration

If you're working with the original "control" types, you can optionally configure the using statements, assemblies and control sources that Sitecore will insert when converting the XML controls into classes. Here's the sitecore web.config sections where you can manage these settings:

  • /sitecore/ui/using
  • /sitecore/ui/references
  • /sitecore/controlSources

Sitecore uses the sitecore/controlsources section of the web.config to define what libraries and folders are used to search for the controls used in an XML file. The default web.config includes these namespaces:

  • Sitecore.Web.UI.HtmlControls
  • Sitecore.Web.UI.WebControls
  • Sitecore.Shell.Web.UI.WebControls
  • Sitecore.Shell.Applications.ContentEditor
  • Sitecore.Shell.Web.Applications.ContentEditor
  • Sitecore.WebControls
  • System.Web.UI.WebControls
  • System.Web.UI.HtmlControls
  • Sitecore.Web.UI.Portal
  • ComponentArt.Web.UI

 and loads XML control files from these directories:

  • /sitecore/shell/override
  • /layouts
  • /sitecore/shell/controls
  • /sitecore/shell/applications
  • /sitecore modules

"xamlControls" Configuration

If you're working with the new "xamlControls" there is an /App_Config/XamlSharp.config file where settings are configured. One of the improvements using this newer system is that, similar to many other aspects of Sitecore, you have the ability to override Sitecore's default behavior such as the control compilers. You can also define custom folders and namespaces where controls can be used from. I'll give a brief rundown on what each section controls.

Compilers

The compilers sections defines the class that renders the XML Control files into a renderable control. This is where Sitecore has the ability to inject a handful of attribute features, such as class overriding and passing in parameters

ControlCompilers

The control compilers are the definitions of classes that are used to find the library control to use to render each control tag type in the XML markup.

Extensions

Extensions are custom tags that affect the control you're working on. The extensions can modify the attributes on the parent control, provide xsl-like if/else/for-each controls and even set local parameters.

AttributeAssigners

Attribute Assigners allow you to set attributes that aren't properties on the control but will be rendered by the Sitecore's compiler.

AttributeEvaluators

Attribute Evaluators allow you to write c# expressions inside attribute tags that get rendered when the control is compiled.

Sources

Sources define the folders and class libraries that are used to support the controls used in the XML.

The default XamlSharp.config namespaces included are:

  • Sitecore.Web.UI.HtmlControls
  • Sitecore.Web.UI.WebControls
  • System.Web.UI.WebControls
  • System.Web.UI.HtmlControls

and it includes these folder paths for Xaml Controls:

  • /sitecore/shell/Applications
  • /sitecore modules

DefinitionCreators

Definition Creators define the file extension that is used to identify XML controls.

HtmlControls

Html Controls are the html tags and what library control is used to render them. Anything that isn't defined on the list, should pass through as is.

Creating Custom Controls

When you create XML controls, consider putting them in your own folder under /sitecore modules/shell/ folder, which is already included by the web.config and XamlSharp.config. And for consistency, try to name the file, the XML control name and class name the same, otherwise it will be confusing to use.

The preferred base class for "control" types were Sitecore.Web.UI.Sheer.BaseForm and the preferred base class for "xamlControls" isSitecore.Web.UI.XamlSharp.Xaml.XamlMainControl.

You can create your own types or new folder locations, just make sure to update the web.config or XamlSharp.config to reference your library or folder so that Sitecore knows to look there for them. Since, as I previously mentioned, Sitecore references controls by their names and will use the first it finds, ignoring duplicates, you could override existing controls with your own by placing the config reference to your folders before the existing references.

Tutorial Sample

There is a tutorial Xaml control that lives under /sitecore/shell/Applications/Xaml. The url to view the control in action is this:http://localhost/sitecore/shell/~/xaml/Sitecore.Xaml.Tutorials.Index.aspx. Some of the pages were throwing exceptions which I was able to resolve once I removed the AjaxScriptManager control from the page. This, of course, breaks the ajax and rest functionality but that's a bug I don't know how to fix.

There is also a sample wizard that you can view or use the source to, that lives in the same folder:http://localhost/sitecore/shell/~/xaml/Sitecore.Shell.Xaml.WebControl.aspx

Alright, now that I've explained a bit about what Sheer UI is and how it's configured in Sitecore, my next articles will go through some Hello World examples.

Sheer UI: 2 - Hello World

$
0
0

With the previous article explaining a bit about what Sheer UI is, setting up a hello world will make much more sense. For this, I'm going to setup the XML control and supporting class file and then give examples of calling them from Applications and Item Editor Tabs

Code Behind

Let's start by creating the class file that is referenced by the XML control. I'll provide an example for both the "control" and "xamlControls" types below.

"control" example

I created a class named HelloWorld and it will inherit from the Sitecore.Web.UI.Sheer.BaseForm in the Sitecore.Kernel assembly. There are also a few other base classes you can inherit from that live in the Sitecore.Web.UI.Pages namespace depending on what you are trying to do. Here's the class code I'm going to be using for this example:

using System;
using System.Web;
using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Sheer;

namespace MyLibrary {
     public class HelloWorld : BaseForm {

          protected Literal ltlOut;

          protected override void OnLoad(EventArgs e) {
               Assert.ArgumentNotNull((object)e, "e");
               base.OnLoad(e);

               ltlOut.Text = string.Format("Querystring: {0}", HttpContext.Current.Request.Url.Query);
           }
     }
}

"xamlControls" example

I created a class named XamlHelloWorld and it will inherit from the Sitecore.Web.UI.XamlSharp.Xaml.XamlMainControl in the Sitecore.Kernel assembly.

using System;
using System.Web;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.XamlSharp.Xaml;

namespace MyLibrary {
     public class XamlHelloWorld : XamlMainControl {

          protected Literal ltlOut;

          protected override void OnLoad(EventArgs e) {
               Assert.ArgumentNotNull((object)e, "e");
               base.OnLoad(e);

               ltlOut.Text = string.Format("Querystring: {0}", HttpContext.Current.Request.Url.Query);
           }
     }
}

To be able to reference the controls on the front end, you'll need to add a reference to them here with the ID name matching the ID attribute value on the front end. Sitecore handles populating the control when the class is constructed. In the event a control reference is null, it can be difficult to determine why. Try adding a runat="server" attribute on the XML control or make sure to check the control name id name and casing as well as properly closing the control. Also make sure to set the security on the class reference to at least protected. Setting it to private would not work. In this example, I'm displaying the querystring parameters, which can provide context for you depending on where you launch the control from. For example: Applications only provide the XML control name whereas Item Editor Tabs provide the item ID, language, version, database and other useful context information.

XML File

The next step is to create an xml file. For this example and for yourself, I recommend creating a folder under /sitecore modules/shell.

"control" example

I'm going to name this control "HelloWorld" and create the following file here: /sitecore modules/shell/helloworld/HelloWorld.xml. The contents of that file will look like this:

<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
     <HelloWorld>
          <FormPage>
               <CodeBeside Type="MyLibrary.HelloWorld,MyAssembly"/>
               <Literal ID="ltlOut" runat="server"/>
          </FormPage>
     </HelloWorld>
</control>

"xamlControls" example:

I'm going to name this control "XamlHelloWorld" and create the following file: /sitecore modules/shell/helloworld/XamlHelloWorld.xaml.xml. The contents of that file will look like this:

<xamlControls
  xmlns:x="http://www.sitecore.net/xaml"
  xmlns:ajax="http://www.sitecore.net/ajax"
  xmlns:rest="http://www.sitecore.net/rest"
  xmlns:r="http://www.sitecore.net/renderings"
  xmlns:xmlcontrol="http://www.sitecore.net/xmlcontrols"
  xmlns:p="http://schemas.sitecore.net/Visual-Studio-Intellisense"
  xmlns:asp="http://www.sitecore.net/microsoft/webcontrols"
  xmlns:html="http://www.sitecore.net/microsoft/htmlcontrols"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

     <XamlHelloWorld x:inherits="MyNamespace.XamlHelloWorld,MyAssembly">
          <Sitecore.Controls.HtmlPage runat="server">
               <Literal ID="ltlOut" runat="server" />
          </Sitecore.Controls.HtmlPage>
     </XamlHelloWorld>
</xamlControls>

 

Applications

When you want to setup a Sheer UI Application, you need five basic parts: the code behind file (with the functionality), the xml file (with xml markup referencing the code behind), the layout (referencing the xml file), the application (referencing the layout) and the shortcut (referencing the application). Since we already defined the first two, we'll just need the last three.

Layout Item

Now that we have a control file, we need create a layout reference in Sitecore to it. To do this log into Sitecore's Desktop application and change to the Core database. Open up the content editor and browse to core:/sitecore/layout/Layouts and, to help with organization, insert a plain old folder named "Modules". From that modules folder, right-click and "insert from template" and select the core:/sitecore/templates/System/Layout/Xml layout and name it "HelloWorld".

core layouts

"control" example

On the "HelloWorld" layout item fill out the "Control" field with "HelloWorld". The "Control" field value is what is used by Sitecore to identify the name of the control from the xml file, not the file name or class it references.

layout control field

"xamlControls" example

On the "HelloWorld" layout item fill out the "Path" field with "/sitecore/shell/~/xaml/XamlHelloWorld.aspx". The "Path" field value is what is used by Sitecore to identify the url by referencing the name of the control from the xml file, not the file name or class it references.

layout path field

Application Item

Applications live in the Core database under core:/sitecore/content/Applications. I'm going to add a common folder named "Modules", again for organization, for my application to live in. Then right-click and "insert from template" on this folder and select the template core:/sitecore/templates/Sitecore Client/Applications/Application.

core applications

This item will now need to have the "HelloWorld" layout item applied through the presentation details, so select the item, jump to the presentation tab and click on the details button and set the layout to the one just created. In my case the layout file is located at core:/sitecore/layout/Layouts/Modules/HelloWorld.

application presentation details

Now it's worthwhile to select an item icon so that it's more readily identifiable. When you do, copy the path to the icon you chose and paste it into the "Icon" field on the item. Then fill out the "Display Name" and "Tool tip" fields.

application fields

On the item there's an appearance field section. This will allow you select the dimensions and look and feel of the application. There's a few checkbox fields that will change the display and a few drop down fields. The only one worth mentioning is the "Chrome" field. There's several types of "Chrome" or window treatments. Leaving the field empty will default the application to "WindowChrome". For this example I'm going to leave this value blank but here's how each value affected the display of my application

BorderWindowChrome has no top toolbar which means it's non-draggable.

border window chrome

ToolWindowChrome has a partial top toolbar with the window control buttons (minimize, maximize and close) which allows it to be draggable

tool window chrome

ContentEditorChrome has a partial top toolbar with the window control buttons and the application name which allows it to be draggable.

content editor chrome

WindowChrome has a full toolbar across the top with the window control buttons, the application name and icon. It is also draggable.

window chrome

WindowFrame pops open a bare window with no link in the desktop tool bar. It's more like an alert or confirm window.

window frame

BorderlessChrome displays nothing. possibly a daemon/service app.

Shortcut Button

Now that you've got your application setup, the last part is to create a shortcut button in the start menu to launch the application. Shortcuts live in core:/sitecore/content/Documents and settings/All users/Start menu. Before you create this item, you'll want to consider the audience when you choose where to put the button. If you check the security on the Left, Right and Programs folders you'll see that the read is disabled for Client Users. This is to allow each button or group to be allowed only by specific client roles. I don't want to go into modifying any permissions for this example so I'm just going to create the shortcut in the Development Tools folder and inherit the permissions there. To create this shortcut right-click on core:/sitecore/content/Documents and settings/All users/Start menu/Right/Development Tools and insert "Application shortcut" and name it "Hello World". For reference the template is core:/sitecore/templates/Sitecore Client/Applications/Application shortcut.

core shortcuts

Once you've created the item, you'll want to set the icon to match the application. Then select the application item with the Insert Link button on the "Application" field but once you've clicked ok, click into the field and remove the ".aspx" from the path otherwise the application won't display in the navigation. It's annoying. And a bug. This results in the "Application" field value looking like this: /Applications/Modules/HelloWorld. If you're on Sitecore 7, the fields are not storing the path value, only the ID so you'll have to go to view and edit the raw value of the field. Add the attribute url="/Applications/Modules/HelloWorld". Then finish it off the shortcut by populating the "Display name", "Icon" and "Tool tip" fields.

shortcut fields

Now refresh your browser and open the start menu and click into Development Tools and there's the Hello World or Xaml Hello World button.

menu hello world

Now you're ready to run the application. So click away to start the application.

application running

If you're using the "control" example, you'll see the querystring value each time you open the application like this: ?xmlcontrol=HelloWorld but if you're using the "xamlControls" example it will be blank.

Item Editor Tabs

The editor tab is altogether different from an application. As far as I can tell it only supports the newer "xamlControls". An editor has four basic parts: the code behind file, the xml file, the editor item (referencing the xml file) and a template's Standard Values' editor list. The first two we already created, so we just need the last two. If you're not sure what an editor tab is, I'm referring to the tabs used on a folder item which shows a list of its subitems.

editor folder

Editor Item

To show a XAML control in an items editor tab, you'll need to create an editor tab item in the core database at core:/sitecore/content/Applications/Content Editor/Editors. Right-click and insert a common folder. I'm naming mine "Modules" and then right-click, "insert from template" and select the core:/sitecore/templates/Sitecore Client/Content editor/Editor template and name the new item "EditorHelloWorld". 

core editors

Then set the "Header" and "Icon" fields. The "Url" field will not be a file path but instead an odd mix of file path and custom nomenclature: /sitecore modules/shell/~/xaml/HelloWorld.aspx.

editor fields

Template's Standard Values

The template is where the control will be viewed so switch to the master database and go into master:/sitecore/templates/Sample/Sample Item/__Standard Values (or any standard values item). Editors should be applied to the Standard Values item like Presentation Details etc. Then, in the ribbon, click on the "Configure" tab and click the "Editors" button. 

editor button

Inside the dialog window, select the /Modules/EditorHelloWorld item and click ok.

editor add

Now you can view the tab by re-clicking on the standard values item or selecting an instance from the content tree. When you view the editor tab, you'll see the querystring value like this:

?id=%7b9DFBB778-512F-4BA7-8293-C01503572D30%7d&la=en&language=en&vs=1&version=1&database=master&readonly=0&db=master. 

editor run

These are all available context parameters you can use to display information or provide custom editor functionality for the item.

You can also read the article I wrote about setting up a Rich Text Editor Button if you're curious about how to do that as well. The next article will delve deeper into the syntax of the XML controls.

Sheer UI: 3 - Syntax and Controls

$
0
0

The previous articles explained what Sheer UI was and how to do basic integrations into your system but what else can it do? There's a good deal of syntax that is supported and a number of useful controls at your disposal.

Extension Tags

These are custom tags defined in the XamlSharp.config that modify the local page. They're unique only to the "xamlControls". They can provide action elements with logic statements similar to XSL. You also have the ability to reference class libraries to support custom web controls, set variables and attach attribute values to the existing tags. Here's what they look like:

attribute 

Attaches an attribute and value to the parent tag

<x:attribute runat="server" name="type" value="value"/>

include

Includes another control into this control as if they were the same control.

<x:include runat="server" href="Sitecore.Web.Controls.Properties" />

param

Allows you to set parameter values inside a control

<x:param name="Icon" value="Applications/32x32/about.png" />

register

Includes a reference to support controls from non-referenced libraries

<x:register assembly="ComponentArt.Web.UI" namespace="ComponentArt.Web.UI"/>

styleattribute

Adds the css style to a style attribute on a parent tag

<x:styleattribute runat="server" name="text-align" value="{Align}"/>

Style

Sets a style tag with the class and properties

<x:Style x:Key="MyStyle">
      <Setter Property="Foreground" Value="green" />
</x:Style>
<x:style runat="server" key=".MyStyle">
      <Foreground>green</Foreground>
</x:style>

Using

this is similar to the import statement on a page control

<x:using runat="server" namespace="Sitecore.Controls" />

XSL-like control tags

<x:template name="testTemplate">
<x:variable name="Color" value="lime"/>
<x:set-variable name="OutsideScope" value="{Color2}" />
<x:for-each select="/sitecore/system/*">
<x:value-of select="@@name"/>
<x:if test="$Color = 'blue'">

Parameters

Sitecore provides a structure for passing values into an XML control. To do so, you use the attributes on the instance of that control. When the value is retrieved on the control definition, it can only be used inside another attribute. This means if you wanted to pass in some page text, you'd have to use a Literal control to display it. Here's an example demonstrating the control definition retrieving values:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <ChildControl>
          <div class="$ClassName">
               <Literal Text="$TextValue" runat="server"/>
          </div>
     </ChildControl>
</xamlControls>

And here is how to pass the values in through the instance control's attributes:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <ParentControl>
          <ChildControl ClassName="someClass" TextValue="Some Text"></ChildControl>
     </ParentControl>
</xamlControls>

Another example, using a different syntax to query for the parameters, passes in the Title and Background values for the Sitecore.Controls.HtmlPage. On the Sitecore.Controls.HtmlPage definition they are referenced using the like so (I've shortened it for the example):

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <Sitecore.Controls.HtmlPage>
          <x:param name="Title" value="Sitecore" />
          <x:param name="Background" />
          <html>
               <html:Head runat="server">
                    <html:Title runat="server" Text="{Title}" />
               </html:Head>
               <HtmlBody runat="server">
                   <x:styleattribute runat="server" name="overflow" value="{Overflow}" />
               </HtmlBody>
          </html>
     </Sitecore.Controls.HtmlPage>
</xamlControls>

And here is an example of using these parameters to the instance control:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <Sitecore.Controls.HtmlPage runat="server" Title="Page Title" Background="url(/sitecore/shell/themes/backgrounds/building.jpg)"/>
</xamlControls>

Attribute Evaluators / Expressions

Expressions are similar to the <% %> .NET syntax. The expression within will be evaluated at runtime. In the control, the expression is inserted directly into the code, so any valid C# code can be used. Similar to the parameters though, this can only be used inside the value of an attribute.

Here's the example from the original documentation on how to use this:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <ColoredText>
          <Border Style='color:${StringUtil.GetString($Color, "red")}'>
               <Literal Text="$Text"/>
          </Border>
     <ColoredText>
</xamlControls>

And here is how you would create an instance of this control and pass in the parameters:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <TextSample>
          <ColoredText Color="blue" Text="Blue Text"/>
          <ColoredText Color="yellow" Text="Yellow Text"/>
     </TextSample>
</xamlControls>

Attribute Assigners

There are attributes that can be used on controls that aren't properties of that control. They are supported by Sitecore's compiler to allow you greater flexibility with the controls you're working with. The syntax for using them on a "control" is slightly different than using then on a "xamlControls" item and I'm not sure all supported attributes are the same for both types.

On a "control" you'd setup the namespace and reference it like so:

<control xmlns:def="Definition">
     <SampleControl>
          <Literal def:AttributeName="value"/>
     </SampleControl>
</control>

With "xamlControls" you'd reference them like:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <SampleControl>
          <Literal x:AttributeName="value" runat="server" />
      </SampleControl>
</xamlControls>

I recommend using the same conventions provided in previous examples for consistency. I don't know all available attributes but here's a few samples:

x:ID

The ID property is used to help manage unique control ID's. Let's say you had a SampleControl that looked like this:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <SampleControl>
          <Literal ID="ltlOut"/>
     </SampleControl>
</xamlControls>

If you were to include this control twice on another control like this:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <InstanceControl>
          <SampleControl/>
          <SampleControl/>
     </InstanceControl>
</xamlControls>

This will cause an exception to be thrown: Multiple controls with the same ID 'ltlOut' were found. FindControl requires that controls have unique IDs.

To fix this you'll need to add the prefix to the ID attribute which will allow the rendered ID to be unique to the control. The result would look like this:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <SampleControl>
          <Literal x:ID="ltlOut"/>
     </SampleControl>
</xamlControls>

x:inherits

The inherits attribute allows you to define which class is used to represent the control. This can be used on controls who already have a class file defined allowing you to override it. The "xamlControls" in the previous example used this like so:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <XamlHelloWorld x:inherits="MyNamespace.XamlHelloWorld,MyAssembly">
</xamlControls>

x:Placeholder

This attribute tells the rendering engine which placeholder control it should be added to as a child control. A common example would be when you use the Sitecore.Controls.HtmlPage. On this file you'll see this placeholder inside the <html:head>

<Placeholder runat="server" key="Stylesheets"/>

and when you use this page you'd reference it like so:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <Sitecore.Controls.HtmlPage runat="server">
          <Stylesheet runat="server" Src="style.css" x:placeholder="Stylesheets" />
     </Sitecore.Controls.HtmlPage>
</xamlControls>

Controls

With the controls provided by Sitecore out of the box there's two general categories of controls: There's structural (like tables, divs and menus) and there's functional (like form inputs).

Border 

This outputs a div around an whatever it is wrapping.

GridPanel 

This will output any child controls inside a table structure. If you set the number of columns it will fill each table division with a control until the next line.

Scrollbox 

This outputs your controls into a div whose height can be specified and sets the overflow to scroll.

Ribbon

Placing a ribbon on your page requires a few extra step. There's, of course, more than one way to do this but here's the way I recommend. I'm registering the <Ribbon> tag by using the <x:register> tag to identify the library it exists in. I'm also including the css file to make sure its displayed correctly. The XML looks like this:

<xamlControls xmlns:x="http://www.sitecore.net/xaml">
     <XamlRibbonExample x:inherits="MyLibrary.RibbonExample,MyAssembly">
          <Sitecore.Controls.HtmlPage runat="server">
               <x:register assembly="Sitecore.Kernel" namespace="Sitecore.Web.UI.WebControls.Ribbons"/>
               <link rel="stylesheet" href="https://www.markstiles.net/sitecore/shell/Themes/Standard/Default/Ribbon.css" />
               <Ribbon ID="Ribbon" runat="server" />
          </Sitecore.Controls.HtmlPage>
      <XamlRibbonExample>
<xamlControls>

The class will implement the IHasCommandContext interface to tell Sitecore that there is a CommandContext to use. The CommandContext is used to set the Uri of the item in the core database that represents the ribbon. In this example, I'm just using the same ribbon from the package designer. The class would look like:

using System;
using System.Web;
using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.XamlSharp.Xaml;

namespace testsite.library {
     public class RibbonExample : XamlMainControl, IHasCommandContext {

          protected override void OnLoad(EventArgs e) {
               Assert.ArgumentNotNull((object)e, "e");
               base.OnLoad(e);
          }

          CommandContext IHasCommandContext.GetCommandContext() {
               Item itemNotNull = Client.CoreDatabase.GetItem("/sitecore/content/Applications/Tools/Installer/Designer/Ribbon");
               CommandContext commandContext = new CommandContext();
               commandContext.RibbonSourceUri = itemNotNull.Uri;
               return commandContext;
          }
     }
}

There are other notable controls that I won't provide examples for such as the Edit (TextBox), Combobox (DropDownList) but they are powerful controls that allow you to build some really slick applications.

Exceptions

When you're working through development you may experience different exceptions or issues that aren't clear what the problem is. Here's a few I caught while working:

Parameter name: parent Sitecore.Web.UI.Sheer.ClientPage.AddControl Value cannot be null. Parameter name: parent

If you're getting this exception, you might not be setting a value on a control that should be set. I've hit this a few times using the TreePicker when I didn't set the DataContext Folder property. The DataContext usually needs to have the GetFromQueryString() method called, the Root and Folder properties set.

The resource cannot be found 

This can occur if you're trying to view the control and you're not logged in or there is a compiler error with your xml control. Sometimes if you try to call another control, you might be able to get a different exception that is closer to identifying the issue.

There are still a number of other topics which I didn't have time to research and write about such as persistence (short and long term storage), events and messaging and, of course, how to build a wizard. If you plan on trying to build an application using Sheer, peruse the tutorial information I mentioned in the first article and even how look over how Sitecore builds the applications within itself and you should be able to follow the breadcrumbs and put the pieces together.

Viewing all 178 articles
Browse latest View live