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

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.

pers-pres-det.jpg

Personalize

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

pers-conditions.jpg

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.

pers-enable-comp.jpg

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.

pers-test-stop.jpg

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="/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. Apparently there was some wellspring article by Mike Edwards but the page appears to have vanished. 

"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:PathToNUnitTestingWebsiteinNUnitTesting.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:PathToNUnitTestingWebsiteinNUnitTesting.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)WebsiteinNUnitTesting.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 modulesData Importscripts 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.  

The Gist

You can download the Extranet Module on the Sitecore Marketplace. You can also download the Sitecore install package from Github and the full source is available here.

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.

I, Sitecore: Integrating machine learning

$
0
0

I've more recently released a video recap of the presentation I gave to the New England Sitecore User Group.

Homage

You've unlocked a door with a key of imagination. Beyond it is another dimension. A dimension of clouds. A dimension of sites. A dimension of mind. You're moving into a land of both shadow and substance, of things and ideas. You've just crossed over into, the Sitecore zone. (shocking music)


On a rather nice day in November, at a bar in Cambridge, after an event known as the Laidlaw Classic (annual birthday football game), an Oasis employee mentioned to me that he was taking a class on Genetic Algorithms. After questioning him to death, since I'd never heard of it, I looked up what he recommended and found a hello world genetic algorithm on GitHub. With it, I was able to take it apart and understand the mechanics. After finally realizing what it was doing, I was able to hotwire it, abstract it a bit, tweak it, and make it provide content for a website based on what type of links I'd been clicking.  

If this sounds like I'm headed in a particular direction, then you're right, it's personalization. I've written before about some aspects of personalization dealing with the rules engine to provide content but this could prove to be much easier. Instead of several steps including analyzing your analytics, a task list better managed by an algorithm, marketers and content editors would be free to focus on what they do best: creating killer content. In turn the technical tasks would fall to those who prefer to do them: developers.  

At the time of the release of the article, what I've built isn't ready to be deployed. It's a prototype. This is an open call to developers who are interested, to take some time and experiment with it. There's a few different uses for an algorithm like this that I can think of (Personalization, Device Detection, Caching and Security) but I'm sure there's more. Also this project isn't tied to Sitecore's personalization engine. It is free standing and the algorithm data and click events are only stored in session. It was a lot easier to build the protoype by limiting my scope and there are still some problems yet to be solved for this to be a package-able, turnkey solution. 

That being said, let's not be deterred. Let's shuffle on. 

Machine Learning

Ok, so at the core of this technology I'm using a Genetic Algorithm. This is branch of Machine Learning which is a branch of AI. It's not gonna get up and walk away on it's own but it can traverse a large field of data that you yourself couldn't. The field of data I'm referring to is the combination of all your users, their interactions and all the content available to them to view. Given a sufficiently large user base and content repository, you're looking at quite a large number of combinations. 

Manually creating personalization schemas will take you forever to get the level of detail the algorithm can produce. Sure, the algorithm isn't perfect but it will improve over time and since a user can change it's mind at any given time, what constitutes a "good" solution is a moving target. This is the real motivation for allowing an adaptive algorithm to do the work. Instead of constantly tweaking and tuning your personalization, the algorithm will continually adjust itself, leaving you free to spend the day doing more productive things.   

Genetic Algorithm

So what's going on behind the scenes? How does the algorithm work? Well, it's using a combination of random chance and probability. It's using random chance to create variability (randomized solutions). Then it's using probability to focus those variations into a productive direction. There's a number of percentage values that are used by the algorithm to set limits on things like mating, mutation and elitism. I've also added a few into my implementation for additional functionality. 

Populations

The algorithm starts by generating a population of entities known as Karyotypes. These entities will have a sequence of Genes. Genes can represent anything. In this case, they represent content categories; tags. The number of genes relates to the number of renderings you need filled. The Gene list of tags tells you what category of content to provide for the renderings. This is so that each rendering can have its datasource set with content from that specific content category. Given two renderings and three categories, an entity's Gene could look something like this: Category1-Category1 or Category3-Category2. The population will start with around 100 different entities and each entity will be a complete random mix of tags for genes. Over time, the population will partition a set of entities that best represent your clicking behavior. 

Fitness

The way the algorithm determines which entity to use as a solution, is to compare their fitness. Fitness can be anything you want but in this case, I've decided that the the total clicks for a category is it's value and that list of categories an entity contains can then be summed up. In my sample site (provided in the source code) I've used colors as tag categories. This was to make it easier to visualize what was happening. As the user clicks on navigation items, values are stored by category. The more clicks, the more valuable it is. The image below shows how a number of clicks for colors would be used to measure the fitness of entities for a page with 2 renderings. For example, since Red was clicked twice, it's value is 2 and a if an entity is Red-Red, then it's sum is 4. 

fitness

In this algorithm, I'm shrinking the value of a click over time so that more recent activity is more valued than further in the past. Big shout out to Andrew Blackmore at Velir for his post that I used as a model. By default it will not reach zero but a fraction of the original value. Only when you've clicked on something a sufficient number of times will the value remain above the threshold. In the image here, you can see the decay of two separate clicks on Red and how on the second click, the value begins to grow:

fitness threshold

Genetic Algorithm Lifecycle

The fitness values are used to sort the entities and provide the best solution. The algorithm then starts to mate (mix genes) between entities and then re-sort. This is repeated until an "optimal" solution is found but in this case the "optimal" solution changes with the user's habits. 

ga lifecycle

Elitism

A percentage of the population at the "most fit" end is not replaced during the mating process so that the population will maintain its fitness or improve but never decline. This percentage is known as the elites.  

Mating

Mating will loop through proletariat (non-elites) in the population and randomly select a partner to mate with and replace itself with the child. The probability that a proletariat will end up mating is determined by the Crossover Ratio, which generally is greater than 50% since you want a good chance of mating. If an entity is chosen to mate, it randomly selects a partner and continues to try looking for a more fit parent a certain number of times. The number of times it will try is the Tournament Size. During this process it's possible the other parent could end up being an elite. When mating finally does occur, the process of mixing the genes between two parents, in a biological sense, is known as Meiosis.

 

elitism

Meiosis

To understand Meiosis you'll have to know a bit more about how the Genes are stored in an entity. This diagram shows the class hierarchy within my version of the algorithm. Each subsequent row expands the object into it's components: 

relationship

For an individual entity (Karyotype) there are two Haploids, one from each parent. They are made up of a Dictionary of Chromosomes. Each Chromosome is a List of Genes. Meiosis produces a new Haploid by randomly mixing the two in itself. This means that each parent will provide a completely new Haploid for a new entity. Here's an example of how a single Chromosome is combined:

meiosis

Phenotype

When a new entity is created, it needs to determine what characteristics it will display. Only half of it's available Genes will be used. The other half remains hidden. The half that is shown is known as the Phenotype. It will compare the Genes from both parent by using the Dominance setting to determine the Phenotype. The Phenotype of an entity is what is used to judge its fitness. Each Gene has a Dominance setting, which is set randomly in population initiation. If the Gene's Dominance settings are the same, one is chosen randomly, but if they differ, the Dominant wins over the Recessive.  

phenotype 

Mutating

There's also a bit of random mutation thrown into the mix which provides yet more variability but too much can cause instability. It, like mating, is reserved for the proletariat section of the population. The Mutation Ratio is the probability that an entity will mutate.

mutation

Page Lifecycle

So now that you know how the algorithm works, how does it apply this to the page content? The crux of the situation is setting the datasource for a rendering to a value defined in the config and then during a pipeline processor, the datasource is switched out with a content item based on what the algorithm has chosen. Then of course there's a click event handler, which stores what tags have been clicked. Here's a high level breakdown of the chain of events. 

page lifecycle

For a developer, you'll first want to create an algorithm definition for your site. This contains all the values needed for the algorithm to run and then add an attribute to your site node that tells it which algorithm definition to use. Each site can have it's own or they can share. From there, you'll just need to determine which renderings you'll want this be used on and set the datasource to a common value in the config. Above and beyond setting up the algorithm, you'll also need to have a tagging system. At least a folder full of tags and a field on both content items and navigation items. I don't recommend tagging any more navigation than you need to. Top level tagging is really enough. The concept of having the algorithm choose sub-category content would require a separate list of click event data and running the algorithm on it separately. I'm not sure how computationally expensive that will be but I imagine it could grow to be quite taxing. For now it's just a top-level category selector. 

For a content editor, their life is actually pretty cushy. All they need to do is create tags, tag content and then tag the navigation to line up with the content. The goal really was to make this a more user friendly experience.

The 411

Ok, so I've got the source code for my project, called SitecoreGeneticAlgorithm, on GitHub. It's broken up into several C# projects: GA.Nucleus (genetic algorithm), GA.SC (Sitecore specific adaptation), GA.SC.UI (Sitecore specific config, web service, layouts, and packages) and GA.SC.UI.SampleSite (generic site adaptation). The sample site should be replaced by your web app. It doesn't take a lot to wire up the system and it provides a working example of what you'll need to do. Here's a breakdown of the libraries starting at the top:

GA.SC.UI.SampleSite

The project really only contains a few files with little custom code that you'll need to change.

 

GA.SC.UI.SampleSite.config

The config is a standard site config definition with one additional attribute: gaName. This tells the system which algorithm definition to use. In this case, it's set to use the "gamanager", which is used in a diagnostic tool which I'll explain in the next library. You will likely want to create your own algorithm definition and tweak the values from there.

GASampleSite-releasedate.zip

The GASampleSite*.zip is the installation package for the site including the content sublayouts and templates.

 

GAMaster.aspx

The GAMaster.aspx is standard but with one addition: the click event handler. The way that user clicks are registered is through a web service. I'm connecting to it using jQuery. 

BaseSublayout

This class is used for easy access to the datasource.

 

GADrivenContent.aspx

The GADrivenContent.aspx is actually completely standard. The name implies it does more than just simply display the datasource but it doesn't. It's only responsible for displaying the datasource that gets set. Keeping this agnostic will require the search part of the Sitecore adaptation to be tweaked to match content types to rendering types. Currently it's expecting a known type. 

 

GAPage.aspx

The GAPage.aspx actually only displays the field content and doesn't have any algorithm specific function.

 

TagUtil.cs

The TagUtil.cs is used on the GADrivenContent page to retrieve the tag value from a datasource. It does this by taking the "Tag" field (search box field) from the datasource, converting it to items and retrieving their tag names. The "Tag" field is used to pair content with navigation elements so that when a navigation element is clicked a corresponding content item can be chosen. An example would be to have navigation elements like electronics, home goods and books. The electronics link would be tagged with "electronics" and then computers, phones etc. would also be tagged as "electronics" so that when the electronics section was clicked on, more computers, phones etc. would be displayed.

GA.SC.UI

This project also doesn't contain many files. I don't forsee a lot of changes to what's already there but there will likely be additions over time.

GA.config

The GA.config file contains all the information needed to run a Genetic Algorithm for Sitecore as well as class references for the algorithm, engagement values and a pipeline event that sets datasources on renderings. Here's a breakdown of the values:

  • tagFolder - The ID of the folder (Bucket) where tags are stored
  • contentFolder - The ID of the folder (Bucket) where content is stored
  • contentTagField - The field on the content items that tags are set
  • datasourceValue - The value of the datasource field that indicates it should be replaced
  • chromosomeName - The name of the chromosome in the Haploid dictionary for an entity (only works with one so far)
  • populationType - The class type of the population
  • karyotypeType - The class type of the karyotype
  • evProviderType - The class type of the EV Provider
  • crossoverRatio - The probability the proletariat will mate
  • elitismRatio - The percentage of population that is elite
  • fitnessRatio - The percentage of the most fit value that is allowed to be chosen as a solution
  • fitnessThreshold - The threshold the combined click events must be to initiate the algorithm
  • mutatioRatio - The probability the proletariat will mutate
  • tourneySize - The number of times a better parent is searched for
  • popSize - The size of the population
  • evModifier - Defines how click events decay over time
    • minRatio - The lower bound percentage a click value will reach
    • decimalPlaces - The number of decimal places a value should be calculated to
    • timespanAspect - The time frame the half life is based on (milliseconds, seconds, minutes, hours, days) 
    • halfLife - Half life in terms of Timespan Aspect. IE: 2 days. 

 

GAManager.aspx

The GAManager.aspx is a diagnostic tool to help understand the mechanics and test the algorithm without having to implement a site. 

 

GAModule-releasedate.zip

The GAModule*.zip installs the GAManager under /sitecore/system/Modules and the core content items, templates and sublayouts it requires. 

 

EventTracking.asmx

The EventTracking.asmx is the web service that is being called by the Sample Site to store click event data. 

GA.SC

This project does contain a good deal of the functionality and will be where most of the changes will be coming to. 

EngagementValues (EV)

There are three classes: DefaultEngagementValueProvider, DefaultValueModifier and EngagementValue. Then there are three interfaces these are based on so that you can provide your own. 

An EngagementValue stores a Value and a DateTime. The DateTime can be used to determine if a value is still relevant.

The DefaultValueModifier will take each EngagementValue and measure the value over time on a curve. The properties for this curve can be set in the config. The concept for this was pulled from Andrew Blackmore's post on Velir's blog. The idea behind it is to make a user's older clicking habits obsolete at some point in time. By gradually bringing the value down to zero, you're able to work against current activity. My particular implementation doesn't bring them to zero but instead a fraction of the original value. This allows the algorithm to still imply a value on something that has been clicked a lot in the past but not recently. Calculating values on a curve is an increasing taxing task and I currently am disabling it because it's too computationally intensive. It will likely be moved to a separate server to be run and queried.

The DefaultEngagementValueProvider is using a static (singleton) to store the values. This is hacky and will be replaced with a provider that supports a database for querying. 

Pipeline

There is an InsertRenderings pipeline processor called SetGADatasource, that fishes out all the renderings who need their source set by the genetic algorithm. It starts the algorithm and then tells it how many genes it needs (length of chromosome). Then provides it all the tags available to use as genes (IGene). The combination of the length and content for an entity is the Genotype. From a biological perspective, I believe I am using this correctly but it might be off. Feel free to correct me.

Web Service

The way that click events are tracked is by hitting a web service that stores them. This is implemented in using jQuery in the Sample Site. 

ConfigUtil

All of the config values are accessible through this class.

Algorithm Specific

The PageKaryotype, SCPopulation and TagGene are the implementation of the core algorithm library. The PageKaryotype is an entity in the SCPopulation. It defines a Fitness method using the TagGenes and EVs to sort the entities, which is the heart of how to guide the algorithm. 

The SCPopulation class manages storing/retrieving the population in session. This will eventually be changed to a provider system so it can be stored in session, file or database. Otherwise it doesn't actually modify the functionality in the DefaultPopulation class that it extends. 

The TagGene is the implementation of the IGene interface by providing the tag name as the GeneID. Genes are the building blocks of the life-imitating entity that is created. 

GA.Nucleus

This is, as the name implies, the core of the algorithm. The focus of this class is around Genes, Chromosomes, Haploids, Karyotypes and a Population. The biological jargon that I'm using is an attempt to be accurate to the model of nature, not solely as an attempt to be nerdful. But definitely somewhat because I'm a nerd... Aaaanyway.

Populations and Karyotypes

An entity in a Population is a Karyotype. The Karyotype is made up of two Haploids and a Gender(bool). The Gender prevents mating with oneself. It's two Haploids are from it's parents. Both the mother and father provide mixtures of their Haploids. 

Haploids

A Haploid is a dictionary of Chromosomes which adds depth to the detail an entity has. The sample site only uses one: PageContent. Although there could be multiple chromosomes for a page to represent different page areas (right and left column) or the types of content for a page (ad, form, informational). This is the base for institutional memory. The state of this information at any given time is the result of the time provided for it to traverse the information you've provided, the group of entities that constitute progress (elites) and the mixture of random possibilities (proletariat).

Chromosomes

The Chromosomes are a List of Genes. In the sample site the List of Genes are a list of tags that tell you what category of content to provide for that rendering. The content is currently done by simple Sitecore Query but will be replaced by an implementation of a search engine, like Lucene. 

Genes

Genes are only responsible for having an ID which is used to identify what it represents and a Dominance property which is used when combining the genes of the parents to know what it will express in the Phenotype.

Phenotype

The Phenotype is the set of Genes an entity expresses. It is used to judge it's fitness. It is a Haploid that is defined by comparing the parent's Genes and their Dominance. These are all set randomly in the population initiation. A combination of two Dominant Genes will choose one by 50/50 chance. Same if both are Recessive. But in a match between Dominant and Recessive, the Dominant will win out. The purpose for adding this into the system is to increase the amount of variation. An entity won't only have a set of Genes that it's displaying but will also contain an entirely hidden set. This information, although hidden, can resurface in a child upon Mating. It really allows a small population the ability to adapt. 

Genotype

This stores the length and list of Genes to be used per Chromosome. The GenotypeList is a Dictionary of Genotypes that pairs with a Haploid. When the Haploid is created it uses the matching Genotype as a guide for what to construct.

Overall Usage

The Population, Haploid, Chromosome and Gene all provide a default class that can be used. The only thing you'll need to create, is an extension of the abstract class BaseKaryotype. I've provided this implementation in the GA.SC.PageKaryotype. This implements the fitness function and defines what makes a good result. These also are based on an interface so that you can replace them with your own implementations if desired. 

Eventually population size becomes a limiting factor. Data storage and algorithm processing will grow with a population. I have not yet found this limit and don't have recommendations on what is required to support any given number of users.

Another limiting factor is the Engagement Value Modifier. When calculating values on a curve, eventually the cost of measuring the asymptote can be demanding on a processor. There may be ways to cut it short through the calculation or simply by not calculating values past a certain date. It's likely this will need to be passed off and run as a batch on another machine to work at scale.

The end? Hardly. 

Although that's the end of this article, that's not where this ends. This is only the beginning of a long and interesting journey. I'm not going to stop until some robot from the future and crazy lady come busting into my home trying to make me stop. Your move universe. (squints at the sky)

References

In no particular order:

 

Symposium 2014: Rise of the Machines

$
0
0

Viva Las Vegas!

I'm now watching mountains drift beneath the wing of my plane (sunglasses on). I'm leaving Las Vegas richer not poorer. You could say, I've just had quite an amazing Sitecore Experience (wink). 

leaving las vegas

The symposium held a palpable tension as the collective agreement was that we were participating in something much greater than what we could have expected. Each talk contributed to increasing awareness that Sitecore is no longer just visionary but enablers of everyone else's visions. 

Sitecore 8

experience branding

Sitecore is now about experience management not just content management. They've expended a lot of resources engineering new ways to support the growing needs of business with insights and communication but with Sitecore 8, you now see the bigger picture. The UI has been redesigned entirely and what they've done with SPEAK will leave you speechless. For a taste of what I'm talking about, here's the new login page, application manager and content editor (pictures taken from slides and Sean Holmesby's rig. Thanks btw) :

login screen

app home page

content editor

The most immediate difference you'll notice is that there's only one way to login. After that it's using the app paradigm which you're likely very familiar with. Apps are now built using SPEAK so that even how you create apps has changed. And when you want to start building them, I was told several times and completely agree that Martina Welander has written great articles on SPEAK to get up and running. The most important thing to understand is that you work through Sitecore Rocks entirely. The structure is stored in Sitecore automatically for you and there's a lot of source binding functionality built in. Most of what you'll end up writing is javascript to manage the interaction. Oh and there is a pipeline to tap into... yes from javascript. Just let that sink in.

One of the details that isn't entirely obvious is that the underlying system is going to be getting a lot better in terms of modularity. The features that, in the past, have been most challenging to integrate should be getting a lot easier to tie into. There's a general rule that there should be generic connection points for all features so that they can be swapped out with competing technologies. It's more of a platform with an ecosystem of tools than an isolated ecosphere.  

Federation

I didn't catch Stephen Pope's presentation but it was talked about so much I feel like I did. Like the Borg, reaching out across space to assimilate all in it's path, you can use Sitecore to "Federate" your external (not in Sitecore) sites by applying analytics and personalization. As perplexing of a task as it may seem, the solution they developed, adding javascript to access the page, makes it seem absolutely easy. A testament to the amount of effort obviously invested.

xDB and Mongo

Because of the scale of information generated when tracking contact/customer/user data, as Dan Solovay (a friend and fellow writer) demonstrated, Mongo DB can be used to cope with the task of excessive growth. At its core, since it's NoSQL, there's a configurable search index to complement the data. Mongo is much better at providing quick access to tons of data than making complex queries so the index is used to provide that end. The bulk of that kind of processing is fed through a series of other report analysis systems. I'm not sure exactly what the implications are for managing these systems in-house but it should be a relief to know that Sitecore will be offering a cloud service that does this for you. Here's a slide from an earlier presentation that explained the xDB architecture:

xdb structure

Alex Sheeeeeebaaaaaaaa showed off a pretty flashy (SVG not flash) analysis tool. It visualizes content from the xDB and allows you to walk a tree made up of user interactions. Each node is a page and it's weight (size) is indicated by it's value along the path to goals. It makes it easier to see how your site is being used and help you identify what's working (or I suppose what's not). He's been busy building all sorts of graphing utilities as well and demonstrated how we could create our own reports with the new personalization app:

analytics viewing

Power Tools

Sean "Sherlock" Holmesby and Mike "_I_ See Interfaces" Reynolds gave one of, if not THE most memorable moment. I won't ruin the surprise for #symeu but needless to say I laughed so hard I almost fell off my chair. 

Jokes aside, most of the presentation touched on a host of powerful tools. From SIM to Powershell to RAZL and more, don't underestimate how much time you could be saving yourself. Knowing what tools are even available is just as important as knowing how to work with what you've got. Technology moves pretty fast, if you don't stop and look around once in a while, you could miss it.

Commerce Server 

The purchase of an eCommerce system was a smart move but making it use a generic connection so that other systems can replace it, is wise. I don't do any eCommerce so I spent most of my time attending other sessions but I can tell you what I learned tangentially; being able to integrate whatever platform you want shows that Sitecore is playing chess not checkers. I'm sure a good swathe of customers are selling products online and would love to be able to improve their customer experiences with personalization and derive insight from the analytic and path tracking data.  

Coveo Search

In a jaw dropping moment of clarity, Coveo is now offering a free edition of their search appliance. The concept is brilliant. For basic use, you're going to get an awesome search tool and when you want advanced features or support, you upgrade. I was skeptical at first but after seeing how easy they made the integration I'm sold. The feature set is extensive and I expect the number of people getting hooked on using it will be high. Here's a breakdown of the difference between the free and enterprise versions shown from their slides:

coveo free highlights

coveo free versus enterprise

coveo facet search

If you're like me and you're working with Lucene, here's a few reasons you may want to consider switching:

  • no index lock issues
  • simple as switching search providers
  • sublayout components with configurable properties like page and result count
  • results filter through sitecore security
  • multi-lang support
  • developer support available
  • suggested results
  • facets

Project Skynet

Jim Ward a.k.a. @jerrong a.k.a. Miles Dyson a.k.a. bucket boy (deal with it) a.k.a. the wiz a.k.a. Tim gave the talk that I was most intrigued with. Since my summer was spent thinking about how to integrate machine learning into Sitecore, what I saw affirmed that it was time well spent. True to form he presented deftly and with wit. For sure I'm several steps behind him at the moment. There's the concept of training models (supervised learning), integration with goals and patterns in xDB (db formerly known as DMS, OMS etc) and a different train of thought along how I was using machine learning. 

Until I get my hands on the Sitecore 8 release I can only explain what I saw but my interpretation is to have users create content and set the algorithm to a/b test it. Over time the algorithm can provide insight into what content could be used in other areas to improve engagement. My gut reaction is that it should go one step further and just let the algorithm automatically test all new content. Allow it to take the individual analytics for a user and match/compare with successful group statistics to make decisions. Ignore what doesn't work and use what does then provide a report to the user. People can then pick out patterns in the reports and modify their creation strategies accordingly. But again, I could very well be saying what they're already doing. 

Final Thought

Sitecore has begun turning itself into a hub of technological business solutions; all hot-swappable. The new UI will lead to ubiquitous device support and expect custom application development times to drop. Designing an amazing experience is now the new normal.  

Adventures in Unit Testing

$
0
0

Watch transcription.

I'm quite literally, so furiously, thinking of ways to explain the funky stuff inside my brain. I couldn't explain! You might understand though. In fact, I'd wager you will. 

So I manage a modest multi-site Sitecore solution. There's a lot of shared code. There's also a lot more overrides. At a certain size, a system must be tested so that I know I'm in control and not my old nemesis, EEENTROOPYYYYY !!! ... ; * ' , . (ahem) The only way to know for sure if some piece of code is working properly, is to test it. 

Test coverage, of course, comes with costs. Time to design it, time to build, time to test it. I've been building web tests for this system for years now. I rolled them into a project last april on GitHub named TestStar that uses NUnit and included some unit testing. It's a web app that works beside my Team City server. After a build, unit tests were run. Then the integration environment was updated and web tests were run against it. This offloaded the work my desktop was doing to a single test system that I could then also run in regular intervals. Like daily. Fed to an RSS feed. So I had a history. The tool works well and does an awful lot of work for me. But I now have a second case for its use, and eventually, hopefully I'll find a third

Over time I've accumulated a few modules on the Sitecore Marketplace. Being able to successfully manage all of them is beginning to require I automate my testing. Mostly in this case in the form of unit testing. I typically run a few version (numbers) of Sitecore. I include my projects from a gitroot folder. This way I can include each, multiple times into other solutions. If I add tests to the projects, when I run a build, all needed files for testing can be in any given environment. The easiest way to test this is to have a modular Sitecore <site> that tests itself and other sites. In this way, it's consolidated PLUS (big plus), it can query for items and quite thoroughly test live content. My first iteration of TestStar also had batch scripting ability, if I wire that up in some friendly way, and or Powershell scripting, I'll be back to sleeping at night... (maybe)

After a time, I plan to release the latest source as a module, fully tested and ready for the daily grind. Part of this involves doing some of my favorite work: UI, UX or whatever nomenclature you prefer. For this I'm riffing off Mass Effect 2 and DJ Jazzy Jeff. Haven't decided which background works better. Here's where I'm at:

unit testing: space background unit testing web testing: space background web testing

Thoughts?

Update

So I've recorded a quick video walking through how the TestStar will work and how it's built into the Sitecore backend.

Here's a second video on the progress I'm making on TestStar. It's now almost ready for release.

DI, IOC, TDD, OMG WTF; WHY?!?!?!

$
0
0

Why: adverb; for what reason or purpose. 

It may come as a surprise to some of you that I'm not an Automata. Surprise! I'm a human being. Unlike a robot or even a cyborg, I can't instantaneously learn Kung-Fu by downloading it. It takes time to accumulate knowledge and experience. I have to read, try and sometimes fail before I learn about something but eventually with persistence, I get there. 

For those readers who are not human, this process of learning begins with why. The pursuit of why something happens is what leads to understanding, not the other way around. I often find the why conspicuously absent from technical articles, which focus solely on the how. Not that there's anything wrong with that, mind you. Sometimes that's really all that's needed but other times, what usually starts as a journey, can end up becoming a mission. Searching, scrounging and sifting until, hopefully, I understand why.

Dependency Injection (DI) and Inversion of Control (IOC)

I'll admit, I had no practical understanding of the terms. I heard them mostly through a bluster of buzzword salads which I find unappetizing. (I'm wordy but it's a means, not an end) This time though it came up recently several times so I curiously began querying for answers. Sure enough, High Priests are already condemning it's lack of ubiquity so I felt as though I MUST be missing something big. I was sure it was some sophisticated strategy, tantalizing technology or epistemological epiphany. Why else would it be held in such reverence, such obfuscation, such... prolixity?  

Dependency Injection(Wikipedia): is a software design pattern that implements inversion of control for software libraries, where the caller delegates to an external framework the control flow of discovering and importing a service or software module.

Wow. Yeah. Ok. Sure. Wait, what? Reading this is taxing. Like I mentioned previously, I'm not a machine so I have no interface for language this obtuse. It's like being asked for the time and responding: "The 4th dimension is exactly 33/60 through 18/24 of a rotation of the earth on it's axis." Accurate only technically but borderline gibberish. Certainly of no help to anyone looking to understand this seemingly inexplicable topic. 

After finally finding a thoughtful explanation, (most were so vapid as to leave me more confused) I was at first a little doubtful what I read was the answer. Why was there so much ado about using interfaces? It's one of the core components of all object oriented languages. Surely there isn't a design pattern SIMPLY TO TELL YOU TO USE AN INTERFACE! But sure enough there is. It's worth mentioning that with the paucity of boundaries given in most articles, one would be left thinking that you should be replacing every class reference with an interface. EVERYWHERE! Just go out and double the size of every library with interfaces. No limit. Why? Because, you imbecile! Dependency Injection! (face palm) 

Absolutism is a poor, poor practice which leads to poor outcomes. I certainly don't know everything but I do know one thing: when to use an interface. Perhaps it's time to explain a bit about... what's the word.... damn... uhh.... oh right; WHY! So here's my take:

If you use interface types as method or constructor parameters, those parameters can be swapped out with any implementing object available in any given situation. If you ever even knew why interfaces existed, you probably already understand the basics of this concept but if you don't, no worries. Living is learning. Let's forge on with an illustration using a good 'ol car analogy. If you were to design a car with wheels permanently fixed to the car, you'd be buying a new car every time you get a flat tire. However, if you design a car to work with a standard wheel interface (four or five lug nuts), then you'll be able to swap them out with snow tires, rain tires, 50" rims or whatever your heart desires. The point isn't to "have a caller delegate to an external...." or other vague rhetoric intending to sound smart (sigh). The POINT... is to make a modular framework that is flexible and reusable.  

Interfaces describe the minimum requirements to be functional. If your class provides this functionality (known as implementing the interface), it'll fit right in. But again. If you were only shown how to use an interface, you might miss the forest for the trees. Knowing why will help you understand that you don't need to replace every class reference with an interface. You'd infer that you only need to consider replaceable parts. Again with the car analogy: If you damage the frame of a car, you're not going to find after-market parts for a frame. It's simply not a replaceable part. Making it replaceable makes NO sense. But again, not everything is a car. Heck I've even written a class library almost entirely consisting of interfaces! So use your best judgement. 

Test Driven Development (TDD)

So if you ask around, it's all the rage to be practicing Test Driven DevelopmentTM. Everything is better with Test Driven DevelopmentTM. It's so easy to start Test Driven DevelopmentTM. Test Driven DevelopmentTM saves lives. Test Driven DevelopmentTM cures blindness. Test Driven DevelopmentTM can even make you popular! You're not already using Test Driven DevelopmentTM? What's wrong with you?!?! Oh right, repeating something tirelessly, breathlessly doesn't help you understand why you should do something. 

So consider if you will, the lifecycle of designing software. Testing what you've built should at least cross your mind. Once you've been burned badly enough by maintaining untested or manually tested code, you'll probably want to seriously consider automating your testing. Then once you begin automating your testing, you'll find it easier to incorporate testing into the design from the beginning. Voila! Test Driven Development is born. It's more than just an obnoxious phrase used to bludgeon any and all conversations into a stalemate. It's actually helpful.

So now that we know why, what does this have to do with DI and IOC? Well, to effectively manage testing any code, you'll need to isolate the core of your code from the implementation. In a web application, the core is generally found in the libraries. The implementation is generally in the sublayouts, web services and other end points. In a unit test however, the end points are irrelevant. You can, and I do, test end points with web tests, but I really want to focus on the unit testing here. To be able to run your code outside of a live environment means you'll have to replace situational content from databases and web services with static, testable components. To do so, you'll need to make these inputs provided as... wait for it... interfaces.

Wow. Full circle right there. Development begets testing. Testing begets interfaces. Interfaces begets modularity and apparently it's very own design pattern (shrug).

Okay, so one last thing about testing because, I've been hearing conflicting opinions on what you should focus on when testing. In my, albeit limited, experience, I've come to align myself with the mindset that tests can serve both as checks on your system and as documentation. In this way of thinking, if it in anyway shows the next guy(or gal) how to instantiate and use your code, it's helpful. Most importantly though, testing is about stability. If it helps you maintain a stable code base, do it. I'd rather have too many tests than not enough.  

A Few Minutes with Andy Rooney Mark Stiles

Alright, I'm almost off my soapbox. Here's where if you didn't get the sarcasm and insinuations, I spell it out for you. If we never learn to question why we do things, it's no longer a science, it's a religion. There's no nuance, only "The one true way". What's the answer to the unique problem you're faced with? Apply approved patterns. Don't think, just do. To be sure, there are plenty of optimal approaches for certain types of problems but I'd rather know why things work or don't so that in the event my situation is unique, which it ALWAYS is, I know how to adapt what I've learned. I'm not a compilation of things I've memorized. I'm a human being, not a robot doing. So don't be afraid or discouraged to try something new or fail. Encourage exploration, collaboration and most of all, discussion.

</RANT>

 

An Old Trope

$
0
0

Not a very long time ago,
in a repository not particularly far away....

It is a period of automation and obsolescence.
Sitecore developers committing
code, day and night, globally.
Spiraling like the arms of a galaxy
sharing tasks, techniques and trade-craft.

During the past few months
I've been developing my 
ultimate tool, the TestStar
A sitecore module with enough
power to test an entire platform.

In pursuit of stability whose breadth
and depth I've not yet known.
To restore freedom for productivity to all.

 

Far Out On The Outer Rim of the Galaxy...

I've reached the end of a long road. I've been building and rebuilding a testing tool for myself for roughly four years now. It began as a web testing Visual Studio project, then morphed into a stand-alone web application with batch script support and unit testing running against NUnit. It has now has grown into a Sitecore module that installs as a web site and can be run from Powershell scripts.

The battle station is heavily shielded and carries a firepower greater than half the starfleet. In it's core reactor system is a set of manager classes that you can request to run tests. I do this through a web service call from javascript. It allows me to run them single-file and post the results as they're completed, giving quick user feedback in the event (especially while web testing) that your system is throwing errors. It also allows me to call the web service from a Powershell script. In this way, tests are running in a Sitecore instance with access to stored information that you can test with, such as IP addresses, language codes and redirect domains.  

The draw for me is that it provides a framework for unit and web testing within a Sitecore context that can help nurture test driven development. This force can have a strong influence on the weak minded and if you were planning on having your junior developers do the testing by hand, those are not the droids you're looking for. A side benefit here too is that it can accelerate development by providing stability controls and automation handlers. I can now do the Kessel run in less than 12 parsecs. I do point-5. That's light speed.  

Here's a diagram showing how the application flows:

TestStar workflow

  1. User engages with test forms
  2. Form contacts web service
  3. Web tests are run
  4. Unit tests are run
  5. PowerShell scripts are generated
  6. Scheduled Tasks run generated PowerShell scripts
  7. PowerShell contacts web service to run tests
  8. Test results can be read from RSS feed

 

The Dark Side

You might be skeptical about ANOTHER module that you have to keep up with AGAIN. Well, consider this, if your empire doesn't have a tight defense, bugs will create weakness in your web application. If your approach is manual testing, it will not be easy. The target areas can sometimes be only two meters wide. Which requires a precise hit. Only a precise hit will setup a chain reaction. I know what you're thinking. "That's impossible even for a computer." But it's not impossible. I used to bulls-eye womprats in my t-16 back home. They're not bigger than 2 meters. 

... Wait... Yeah. I might be getting a bit off course. What I meant is that although, it seems like just another thing to keep track of, it's additional value is well worth the effort. 

Sure, there's all sorts of ways to run unit tests but having multiple tools allow you more flexibility and a greater variety of testing surfaces like through web testing. This tool can also run groups of tests so you can include unit tests provided with installed modules. I'm not sure how widespread that practice is yet but I'm including them within this module and have plans to do it for others I manage. If you want to be able to call these tests as a single effort it might be easier to keep up with a powershell script that can be auto-generated. I'm just saying.     

Engage Your Tractor Beams

If I'm beginning to pique your interest, there's a few ways to get started:

 

With the TestStar, no web system will dare oppose you now. You may fire when ready and may the force be with you. 

Sitecore Dictionary Redux

$
0
0

Sitecore DictionaryI always knew Sitecore provided Dictionary support for a long time but I also knew there were, shall we say, scalability problems. Like, let's say, general user access (it was located under the system node) and the inability to create individual dictionaries for individual sites. Because of that, I, and apparently others, have been inanely custom rolling dictionaries. That is, until I came upon Scott Mulligan's article highlighting the latest changes in Sitecore 6.6 (yesss, it's been a while, I know [sighs, rolls eyes]) that convinced me to finally stop reinventing the wheel and adopt the Sitecore provided solution. 

There are resources out there to help get started like John Wests' seemingly canonical post. There's also a super handy article by "sitecorecode" (I couldn't find the author's actual name but I guess you know who you are). And my long time friend M3, as he's known colloquially, breaks it downlike a legend.

 

That being said, there's no cookbook so any real depth on the topic is up to us/me/we/you/them/it to decompile and sift through the debris [duh-briss], then craft clearer guides from zero to working. 

1. Setting up a dictionary

First thing you'll need to do, assuming you have a Sitecore instance running and you're logged into it (because without that you're basically reading the wrong site), is insert a "Dictionary Domain" [dik-shun-air-ee doh-main] object from the template: "/sitecore/templates/System/Dictionary/Dictionary Domain". There are no icons for the dictionary items so you'll just have to deal. However, since the larger structure is unlikely to change without a developer, you could probably get by with changing the icons on the instance and be ok. In case you're wondering, the red book is in the "People" [peep-pull] icon section. Second row, all the way to the right. It would however be a lot better if someone at Sitecore (wink, nudge, cough......) were to add the icons to the system itself. Fabulous even.

Dictionary Domains

Moving right along. Once you've got a "Dictionary Domain" (not scare quotes) created, you'll need to specify which dictionary your site is going to use. The way to do this is to add an attribute to the site node in your config file: dictionaryDomain="NameOfDictionaryDomainItem". 

Dictionary Site Attribute

If you're wondering how this helps Sitecore find the dictionary in the tree, I'm assuming (perhaps incorrectly) that the underlying code is using a "Lucene" [lou-seen] index to get all the dictionary domain items and pull the correct one by name. Probably.

Fallbacks

There's a fallback concept with dictionaries that should be explained. On the Dictionary Domain node you created is a "Fallback Domain" [fall-bak doh-main] field. If you were to have local (to the site) dictionary and a global (to mulitple sites) dictionary, then this would make sense to have a fallback. Maybe you want to have most of your content global but then a few overrides locally. The field seemingly also pulls results from Lucene allowing you to place your dictionaries wherever you need them.

Dictionary Fallback Field 

2. Adding entries

Once you've got the basic structure in place, you'll want to populate some values. You have two insert options: a folder and an entry. 

Dictionary Insert Options

Foldering

I like to separate into groups by function such as form labels, global labels, navigation etc. I'm going to create a single folder called global and a single entry called copyright. This is pretty common for a site so that should be easy to follow.

Dictionary Domain Tree

The Keys to the Car

When you create your entries, the name should be short and common sense. It's only going to be used by content editors to logically find content. On each item you have two values to populate: the key and the phrase. 

The key is what will be used to call this entry from code. It's a shared field so you won't have to set it on each language you translate to, thankfully. For the key value, I think it makes sense to use the format folder.subfolder.entryname. If we included the site name we'd hobble ourselves when it looks for fallback values. So in this instance it would be "global.copyright" [glow-bull dot kop-ee-rite] (since I didn't create any level of subfolders). I also recommend using lowercase since that introduces less room for error.

Dictionary Entry Key and Phrase

The phrase is the text value that shows up. This is not a shared field so it's value is unique to all languages used. Translate away!

3. Accessing the entries

Now the last step is how to access it where it's needed. You're going to be using the Sitecore.Globalization.Translate namespace. There's a list of methods available but mostly you're only going to care about a few. And they are: 

Sitecore.Globalization.Translate.Text(

string key,
params object[] parameters

)

This is the most basic query and it will look at your context site and try to find the "dictionaryDomain" attribute to find the set of keys to use. If you didn't set it on your site node it will default to the global dictionary (in Sitecore) and if it doesn't find the key there (which is likely), it will return you the key itself. If you think that's a bad user experience and don't want the keys to show up as a default, Mikey "Interfaces" [he-see-them] has a nice write-up on how to fix that. 

There's one thing you should wonder about and that is what do the object[] parameters do? Good question, I'm glad you were paying so close attention. This is a hidden gem that allows you to pass in parameters for the entrie's phrase field which is fed through the string formatter. In the previous step, for example, I'm passing in the year and having it replace {0}. Here's the honey:

Dictionary Entry Format

Sitecore.Globalization.Translate.TextByDomain(

string domain, 
Sitecore.Globalization.TranslateOptions options, 
string key, params object[] parameters

)

This method assumes you haven't added any attribute to your site, or want to reach into a different dictionary than the context is set to. You specify the "domain" which is the Dictionary Domain instance item name. 

The TranslateOptions allows you to specify the Database, FallbackDomains, SuppressDefaultFallback and SuppressFallback. I can see this being useful for several types of edge cases. Sitecore modules come to mind since you might need your content to be in the core database or you might want to prevent fallback to dictionaries outside of your control. 

Also supports parameter goodness.

Sitecore.Globalization.Translate.TextByLanguage(

string key, 
Sitecore.Globalization.TranslateOptions options,
 
Sitecore.Globalization.Language language,
string key, params object[] parameters

)

This builds further by allowing you to specify a language to get the content by. I would imagine that if you have a translated site but always wanted to keep, perhaps, your business name pulled from one language, you could. 

In Summation

The process is a little jarring at first and without icons, can be slightly less visually appealing but it's now (as in a while ago) possible to create a dictionary, specific to a site, populate unique keys that are translatable and control how to display them. Better late than never amirite? I meant me using it, not Sitecore changing it. Well, I guess that too, but also me. Anyways [that's-all-folks].


Sitecore Upgrade : The Art of War

$
0
0

Sun-Tzu: The Art of WarWhen you enter a battlefield, be it land, mind or disk, be aware (wink):

"No plan survives contact with the enemy"
-Helmuth von Moltke the Elder

That's not to say you should have no plan though. Quite the contrary:

"Victorious warriors win first and then go to war, while defeated warriors go to war first and then seek to win."
-Sun Tzu

The War of the Folders

Starting at the top, you've got to upgrade the updater so you can install updates. You'll be copying in new files. You've got to run SQL scripts. Then you fix everything that broke. If you cross more than one point version, well then you're doing this all, multiple times.

If you've done yourself any favors, you've downloaded the packages, scripts, updates, a "Site Root" zip of the version you're installing and last but not least, the steps for each upgrade path and broken them up into folders like "7.0 to 7.1". You should also consider removing Sitecore from your solution (except configs and binaries) and publish onto a stock installation. In my experience, you're best bet is to upgrade to the nearest point version and jump from initial release to initial release. 

Hopefully your config modifications from a stock build will be in a separate "include" config far away from the ground-zero. In a quiet, zen-like state. This state is within you. It is you. Well, at least, if you want to have any hope of survival. But really, this is because the heart of the battle lies in preventing a cataclysmic collision of config files. In your hands are your helmet, sword and shield: Winmerge, Visual Studio and Git.... Silence. My pupils dilate and the caffeine is flowing.  

"He who knows when he can fight and when he cannot, will be victorious."
-Sun Tzu

At first contact, you're looking each other square in the eye. There's no illusions about what you've gotten yourself into. You've entered the Thunderdome (shout out to all you Aussies). It's looking at you. You're looking at it. Neither speaks. Neither blinks. You breath deep and dive in. You get a quick thrill knocking down the first few dozen line ending changes. Alt-Down. Alt-Left. Alt-Down. Alt-Left. Oops, CTRL-Z. But after a time, you're just playing a two-bit game of Chuck Norris, Roundhouse Config Kicker. WHACK! WHACK! WHACK! WHACK! You become numb. The tension is worn away in an anti-climactic grind.  

Suddenly something pops up. You find the new default values have changed. Oooh. Your mind is mildly sated. A small meal though. No more.

"He who is prudent and lies in wait for an enemy who is not, will be victorious."
-Sun Tzu

As time wears at you like a sweaty shirt, you ease new entries into place. You delicately tip-toe through custom config sections. Bob and weave around modules and rewrites. Man-handle handlers and services. You win some, you lose some but you live to fight another day.

Apocalypse How

In the process of upgrading a system through 7.5 there's a slight bump in the road: xDB. Or rather, MongoDB. The xDB overview doc is a good start to understanding what's going on but long story short, you need to download, install, configure, run as a service, setup connection strings to it, modify the analytics settings, setup at least one goal so that it begins tracking (I couldn't find an article anywhere explaining this) and last but not least deploy it

Like a good soldier, Dan Solovay has written about Sitecore moving to xDB and a great piece on MongoDB with Sitecore and Nick Wesselman has a whole series on it

The highlights are the four "Analytics" databases (analytics, tracking.live, tracking.history and tracking.contact) that run on MongoDB and one "Reporting" MSSQL database whose file is named "Analytics". It's the one that comes with "Core", "Master" and "Web". That's confusing, you might say, why isn't it named "Reporting"? Because it's your old analytics database that's been converted. Well, I mean that you're going to convert/rebuild it right now (which is what the reporting.secondary connection string is for.). If you need help, tactical options are available

Ok, let's assume you figured all this out and your local environment works, what's going to happen when you go live? You have to deploy MongoDB somewhere secure, that your other environments can access. Maybe there's a CLOUD solution for all that BIG DATA, but I have to roll my own so I wouldn't know. If you decide to manage it yourself, MongoDB has a good deal of documentation but it's might require some translation (they've been changing their API with newer versions too) so you'll be spending a good deal of time googling, guessing and testing.  

If you make it all the way to Sitecore 8, which is what I'm doing, you should also be aware of a new tool being added called Phantom.js. You should download this from Sitecore's Dev site and not the product's site. It's contents are added to the /data/tools folder which is where some core pipeline expects it to be. This is used to take screenshots when the system is testing, or as I found out, it runs when I update presentation details on standard values.  

Enemy at the Gates

If you made it this far, "Achievement Unlocked: Dragonborn". But don't get too comfortble. You're out of the quagmire but your still in the trenches. The light at the end of the dark tunnel though, is an end-boss. One with more comebacks than King Koopa: IIS

You dive into your foxhole, reach for Visual Studio, load your solution, cock back NCrunch and start launching builds, like a battery of artillery fire. 

Publish-Development..... The output window gives you a mime's version of atomic gears grinding. The silence is deafening.

MISFIRE: Sitecore namespace cannot be found. Awwwrrrr. (update the reference to new binary). 

Publish-Development..... 

MISFIRE: awfffwwah (projects target the wrong .NET version) 

Publish-Development.....

MISFIRE: Could not find add method: SetCommitPolicy. Should I call in air support? (squints. raises eyebrow. scratches head. glances at Stack Overflow

Publish-Development.....(sound of a missile launching)

SUCCESS! A DIRECT HIT! 

You're palms are sweaty, you alt-tab to the nearest browser (never to IE though, just out of spite). CTRL-F5. 

"Config Error Cannot read configuration file because it exceeds the maximum file size"

 

fffff

Pfffwa. Buh. Muh... Guh...

REALLY?!?! Not a mis-spelling. Not a double-entry. NOT EVEN A SINGLE MISSING BINARY?!?! Wow. You NEVER, and I mean NEVER see that one coming. 

"Thus, what is of supreme importance in war is to attack the enemy's strategy."
-Sun Tzu

So now you're on an epic side-quest to peel off as much of the config into includes as possible. Oh don't forget, if you use config transformation (.NET not Sitecore), the build can't follow Sitecore's config references to the "include" config files. They haven't been joined in holy matrimony yet so be aware of that minefield. If you need some suggestions on what to move, I think it makes sense to pick sections that don't differ by environment but are fairly large. It was enough for me to move <databases>, <events>, <mediaLibrary> and <log4net>. For guidance just follow how the commands.config is managed.

Back to the future, you're heart is beating intensely. You straighten up in your seat. CTRL-F5. We've got to get it on the run! Grab the exception and find the nearest google. Furiously read the saveragery of battles past and learn from the bloated body of blogs and Stack Overflow. One, Two, Ten, Infinity. Found it. What was .... awwwwrrrrr...my fault. 

"The supreme art of war is to subdue the enemy without fighting."
-Sun Tzu

All Quiet on the Testing Front

Like a fever breaking, the page loads. You can't hardly believe you've done it. It's only one battle in many for victory but at least it wasn't Pyrrhic. Now that's not to speak of all the features that aren't quite working correctly like the "Experience Editor" (How do you choose which site you want to edit from the home page?). Or the Lucene indexes (I was using the scSearchContrib which is now deprecated). And let's not forget, caching (I had it customized).

Reconciliation

The update process is a work in progress but know that as technology advances:

"The wars of the future will not be fought on the battlefield or at sea. They will be fought in space, or possibly on top of a very tall mountain. In either case, most of the actual fighting will be done by small robots. And as you go forth today remember always your duty is clear: To build and maintain those robots."
-The Commandant, head of Rommelwood Military School 

In work, as in war, although the best laid plans of mice and men often go awry, this is your upgrade guide, so you won't be forced to hire Seven Samurai

"Make an upgrade plan if you're soon to, and defeat your chances of ending with Seppuku."
-Mark Stiles

 

 

Catching Exceptions Episode 1 : Nick Wesselman

How a Poster is Born

$
0
0

Last December, before a soul crushing winter had fully descended on Boston, Sitecore sponsored a posh Meetup at Stoddards pub. While mixing it up, I ended up talking to Rick Cabral about making some t-shirts for the user group. While at the Sitecore Symposium the previous fall, I noticed a few other groups had made their own shirts and I thought it was really cool and as it turns out, I may know a guy who makes t-shirts. So knowing I could supply the backend, I asked Rick what kind of design he thought would be good. I didn't really expect it but his eyes widened and he started talking about how he always envisioned this soviet-era propaganda with the slogan "Join the Sitecore". I almost died laughing because I could totally see it. I didn't need any other explanation and let's be honest, with the goatee, he looks kinda like Vladimir Lenin and would perfectly fit the caricature. And just so you know, as Rick continues to insist, he didn't come up with the idea of immortalizing himself, I did.

Shortly after the meetup I contacted my old, old friend Jay at Antidesigns to produce the design. Working with his team, we went back and forth for few rounds, working from some cruddy source images I pulled off the intarwebs but the image wasn't good enough. It was just too grainy and he wasn't projecting the right message. Here's a few of the early designs:

1.jpg

2.jpg

3.jpg

4.jpg

Eventually I just needed to get a good photo of Rick, so I grabbed him after the user group and he breaks out this World War II helmet, gives this classic pose and everything else fell into place. 

With the design locked down. The next step was producing product but I wasn't sure how to get the funding for the shirts and I didn't want the design to languish so I ponied up the dough myself for the first run. Of course, if you're making a propaganda piece, quite literally, I really didn't think making t-shirts could do it justice; I had to make posters. Talk about getting sidetracked though.

I started with a run of 25 and have since ordered another 15. They're limited edition, artist signed and numbered. They're printed on a parchment type paper and they're absolutely awesome. Rick got the first and I gave him 10 more to dole out as he sees fit for the user group as prizes or whatever. The rest I'm going to be giving away as mementos. I wish I could give one to everyone but they're high quality art and they cost a bit so I couldn't but as owner of the copyright on the design I'm opening it up to the community to use freely (see copyleft).

You can download the vector files here!

Here's the resulting work in all it's glory:

full size

and here's a close up. You can kind-of, sort-of see the grain of the paper... or not:

close up

Anyway, I'm hoping that this makes it easier to raise dinero for the shirts for our user group since it's much easier to explain something that is tangible. 

Also since I'm opening the design to anyone to use or modify, I strongly encourage anyone who wants it, to brand it for their user group. You could even commission the same artist to make some more Sitecore art for your user group or even modules like Powershell, SIM, TDS etc. (wink, nudge). So please hit up Antidesigns and tell them I sent you. They can print on all kinds of stuff and do everything by hand. I'd absolutely love to see what you can come up with. 

Otherwise, long live the #sitecorecommunity!    

Catching Exceptions Episode 2 : Sean Holmesby

Catching Exceptions Episode 3 : Lars Floe Nielsen

Viewing all 178 articles
Browse latest View live


Latest Images