COBAL Renderings in Sitecore


Sitecore’s flexible rendering module allows developers to create their own custom rendering types.  If you wish to avoid XSLT, Web Forms or Razor you can also create your own rendering type.  I was doing some work for an older generation of developers that didn’t want to get retrained on .NET and we came up with this easy to use rendering type that supports COBAL.

 

First we create our COBAL code file on the server.  Notice the use of the DISPLAYFIELD method to allow support for the Page Editor.

cobal_code

 

Next you just insert the new COBAL rendering type into the content tree and set the path to point to your code file or mainframe system you would like to return the content from.

cobal_rendering

 

The GetRendering pipeline was then extended to call out to a AS400 Mainframe witch runs the COBAL code and calls back to the Item Web API to get the rendered content.

All in all it works pretty good, and was very easy to implement due to Sitecore’s flexible rendering engine.

 

 

 

Advertisement

Personalizing Staticlly Based Componenets with the Sitecore Rules Engine


Someone in the SDN forums had asked how to personalize a statically placed component on a page so I thought I’d come up with a solution. One one option would of course be to use template inheritance and to personalize the component on your base page template but that’s no longer a statically placed component.

I decided to create a web control that has settings for the default rendering , datasource, and parameters but allows you to personalize the component via the rules engine.  My first attempt had me creating my own rule context and actions but after a quick rewrite I realized I didn’t need all of that and was able to just use the per-existing context and actions.

Here’s the solution.

Create a Sitecore Template with a Field and call it “Rendering Rule” and make it of type Rule and set the Datasource to “/sitecore/system/Settings/Rules/Conditional Renderings”.

Add a new one to the tree and set the rendering rule. Something easy to test is “Where the current user is adminstrator change the rendering item”. Just make sure not to try and test in preview.

Next, I created the control below which will execute the rule actions for the item

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI.WebControls;
using Sitecore.Rules.ConditionalRenderings;
using Sitecore.Data.Items;
using Sitecore.Web.UI.WebControls;
using System.Web;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Diagnostics;
using Sitecore.Rules;
using Sitecore.Data.Fields;
using Sitecore.Layouts;

namespace ConditionalRenderings
{
    public class StaticRendering : WebControl
    {
        public string DataSource { get; set; }
        public string RenderingItem { get; set; }
        public bool IsHidden { get; set; }
        public string RenderingRule { get; set; }
        public string Parameters { get; set; }

        protected override void OnLoad(EventArgs e)
        {

            Assert.IsNotNull(RenderingRule, "Rendering Rule cannot be null");
            Item RenderingRuleItem = Sitecore.Context.Database.SelectSingleItem(RenderingRule);
            Assert.IsNotNull(RenderingRuleItem, "Rendering Rule Item not found.");
            Assert.IsNotNull(RenderingItem, " Rendering Item cannot be null");
            Item renderingItem = Sitecore.Context.Database.SelectSingleItem(RenderingItem);

            //create the rendering reference and set some defaults
            RenderingReference reference = new RenderingReference(new RenderingItem(renderingItem));
            Assert.IsNotNull(RenderingItem, "Rendering Item not found");

            if (!string.IsNullOrEmpty(DataSource))
            {
                reference.Settings.DataSource = DataSource;
            }

            if (!string.IsNullOrEmpty(Parameters))
            {
                reference.Settings.Parameters = Parameters;
            }

            List references = new List();
            references.Add(reference);

            //new up the rule context and execute the rule
            ConditionalRenderingsRuleContext ruleContext = new ConditionalRenderingsRuleContext(references,reference);

            RuleList rules = RuleFactory.ParseRules(Sitecore.Context.Database, RenderingRuleItem.Fields["Rendering Rule"].GetValue(true));
            rules.Run(ruleContext);

            //make sure hide component wasn't selected as an action
            if (ruleContext.References.Count >= 1)
            {
                //add the control to the page
                this.Controls.Add(ruleContext.Reference.GetControl());
            }
            base.OnLoad(e);
        }

    }
}

Next, Add the control source to the web config under system.web/pages/controls.

<add tagPrefix=”cr” namespace=”ConditionalRenderings” assembly=”ConditionalRenderings”/>

Finally, add the control to a page pointing it at the Rendereing Rule Item we created and and default Rendering Item.

<cr:StaticRendering runat=”server” RenderingItem=”{2B556B84-364A-4AB3-A64C-933787D19619}” RenderingRule=”{11BD1B9F-A953-4F80-8692-4663B6DB4C78}” />

Package to just download and install is available here. You will still have to update the web.config to use the control.

Sitecore MVC Area Controller Rendering Type


I saw this post a couple weeks back about working with Areas in Sitecore MVC. One of the issues was that you had to specify the view path in all of the actions because Sitecore didn’t understand what area you are in. To get around this I created my own rendering type that allows me to specify an Area field. The Area is then added to the Route Values Dictionary. I’m expecting this would also fix other problems with working in areas, like linking. I do however recomend that you strongly type the controller name when entering it as Sitecore is not using the Area field when it resolves the controller. Perhaps that code could be updated as well.

The code is here and the package to install it is here.

There are three classes that I will describe below :

AreaController
This class checks to see if the Rendering Item inherits the Area Controller Template. If it does we Assign the AreaControllerRenderer as the Renderer.

AreaControllerRender

This class passes the Action, Controller and Area to the AreaControllerRunner and returns the html to Sitecore.

AreaControllerRunner

This class executes the controller.  The big change is made here where I pass the Area in the Route Values Colleciton.  The important line to get area support is below.

requestContext.RouteData.DataTokens[“area”] = this.Area;

 

There is also a Sitecore.MVC.SitecoreAreas.config file that causes the code to execute in the GetRenderer Pipeline and a new template ‘Sitecore/Templates/User Defined/Mvc Areas/Area Controller’ that has the template to inherit from your rendering.

I personally think everything should be done in areas as it makes things more portable to other projects.  If you’re not using areas you can also use this project as a sample on how to extend Sitecore to support your own rendering type.

Extending Sitecore Rocks – Creating a ContentTreeCommand and Server Component


Sitecore Rocks is completely extensible, much like Sitecore itself.  In this example I’ll be extending it to support cloning with a Sitecore Rocks Server Component and a Sitecore Explorer Command Plug-in.

The Server Component

•Server Components sit in the bin folder of the websites.
•The Hard Rocks Webservice is used for processing custom plugins.
•All assemblies must begin with Sitecore.Rocks.Server
•The class’s namespace must begin with Sitecore.Rocks.Server.Requests
•The class must have an Execute method that returns a string
•The Execute method may contain any number of string parameters

1. Create a new Sitecore Rocks Server Component Project named  Sitecore.Rocks.Server.Cloning

2. Rename the HandleRequest class to Clone in the Requests folder

3. Change the Namespace to Sitecore.Rocks.Server.Requests.Cloning

4. Use the following commented code

using System.IO;
using System.Xml;
using Sitecore.Data;
using Sitecore.Data.Items;
using System;
using Sitecore.Configuration;
namespace Sitecore.Rocks.Server.Requests.Cloning
{
    public class Clone
    {
        public string Execute(string databaseName, string sourceItemId, string targetItemId, string databaseUri)
        {
            StringWriter stringWriter = new StringWriter();
            XmlTextWriter xmlTextWriter = new XmlTextWriter(stringWriter);
            xmlTextWriter.WriteStartElement("result");
            try
            {
                //clone the item
                Database database = Factory.GetDatabase(databaseName);
                Item item = database.SelectSingleItem(sourceItemId);
                Item destination = database.SelectSingleItem(targetItemId);
                Item item2 = item.CloneTo(destination);
                //write out the database uri
                xmlTextWriter.WriteStartElement("database");
                xmlTextWriter.WriteString(databaseUri);
                xmlTextWriter.WriteEndElement();
                //write out the new item id
                xmlTextWriter.WriteStartElement("item");
                xmlTextWriter.WriteString(item2.ID.ToString());
                xmlTextWriter.WriteEndElement();
            }
            catch (Exception ex)
            {
                //write out an error if we have one and Rocks will throw the error
                xmlTextWriter.WriteString("***ERROR*** " + ex.Message);
            }
            xmlTextWriter.WriteEndElement();
            return stringWriter.ToString();
        }
    }
}

5. Compile and place the assembly in the bin of your website.

Creating the Rocks Plug-in

When Rocks loads it scans the C:\Users\{user}\AppData\Local\Sitecore\Sitecore.Rocks\Plugins folder for assemblies and loads types that are marked with certain attributes[Command],[Pipeline],[FieldControl] etc…

In the code below will be doing the following :
  • Modify the constructor to set the name, group and sort order.
  • Modify the CanExecute Method to make sure only one item is selected.
  • Modify the Execute Method to Show a Dialog that allows the user to select where to place the clone and then call the Server Method.
  • Add an ExecuteCompleted  method to handle the return call from the server and refresh the screen.
  • Add a method to handle the XML Parsing.
1. Create a new Sitecore Rocks Plug-in Project and name it Sitecore.Rocks.Plugins.Cloning
2. Rename the Command to Clone
3. Replace the code with the following commented code
namespace Sitecore.Rocks.Symposium.Commands
{
    using Sitecore.VisualStudio.Commands;
    using Sitecore.VisualStudio.ContentTrees;
    using System.Linq;
    using Sitecore.VisualStudio.Data;
    using Sitecore.VisualStudio.UI.SelectItem;
    using Sitecore.VisualStudio.Extensions.IItemSelectionContextExtensions;
    using Sitecore.VisualStudio.Data.DataServices;
    using System.Xml;
    using System;
    using System.Windows;

    [Command]
    public class Clone : CommandBase
    {
        public Clone()
        {
            this.Text = "Clone";
            this.Group = "Edit";
            this.SortingValue = 2300;
        }
        public override bool CanExecute(object parameter)
        {
            //this method enables or disables the menu option
            var context = parameter as ContentTreeContext;
            if (context == null || context.SelectedItems == null || context.SelectedItems.Count() != 1)
            {
                return false;
            }
            return true;
        }

        public override void Execute(object parameter)
        {
            //this method executes when the button is pushed
            var context = parameter as ContentTreeContext;
            if (context == null || context.SelectedItems == null || context.SelectedItems.Count() != 1)
            {
                return;
            }

            //get the selected item
            IItem selectedItem = context.SelectedItems.FirstOrDefault() as IItem;

            if (selectedItem != null)
            {
                // get the root "sitecore/content" item for our select item dialog.
                var contentItemId = IdManager.GetItemId("/sitecore/content");
                var contentItemUri = new ItemUri(selectedItem.ItemUri.DatabaseUri, contentItemId);

                //Show a popup to select a target
                SelectItemDialog dialog = new SelectItemDialog();
                dialog.Initialize("Select Target Item", contentItemUri);

                if (dialog.ShowDialog() == true)
                {
                    //item is selected to clone to , send the command to the server
                    var targetItem = dialog.SelectedItem;
                    if (targetItem != null)
                    {
                        context.GetSite().DataService.ExecuteAsync("Sitecore.Rocks.Server.Requests.Cloning.Clone,Sitecore.Rocks.Server.Cloning", //The class to exectue on the server
                            completed, // the completed event
                            selectedItem.ItemUri.DatabaseName.Name,  //the database name for sitecore
                            selectedItem.ItemUri.ItemId.ToString(), //the source items id
                            targetItem.ItemId.ToString(), // the target items id
                            selectedItem.ItemUri.DatabaseUri.ToString() // the database uri to return to server
                            );
                    }
                }

            }
        }

        ExecuteCompleted completed = delegate(string response, ExecuteResult result)
        {
            //this method is fired after the item is created on the server
            if (!DataService.HandleExecute(response, result))
            {
                return;
            }
            //get the active content tree
            ContentTree activeContentTree = Sitecore.VisualStudio.UI.ActiveContext.ActiveContentTree;
            if (activeContentTree != null)
            {
                // get the return values from the xml that was returned
                string database = GetResultValue(response, "//database");
                string item = GetResultValue(response, "//item");

                //get the databaseUri in the rocks format
                DatabaseUri dbUri;
                DatabaseUri.TryParse(database, out dbUri);
                //get the itemUri in rocks format
                ItemId itemId = new ItemId(new Guid(item));
                ItemUri itemUri = new ItemUri(dbUri, itemId);
                if (itemUri != null)
                {
                    //send the user to our newly created content
                    activeContentTree.Locate(itemUri);
                }
            }
        };

        //parse xmlvalue
        public static string GetResultValue(string xml, string xPath)
        {
            XmlDocument xmlDocument = new XmlDocument();
            xmlDocument.LoadXml(xml);
            XmlNode xmlNode = xmlDocument.SelectSingleNode("//result" + xPath);
            return xmlNode.InnerText;

        }
    }
}
4. Compile the Project.
5. Open up a new instance of visual studio and you should see your new ‘clone’ command when you browse to an item.

Adding Site Filters to DMS Reports with Sitecore 6.5


DMS does not have a Site Filter for multi site enviornments in the latest version of Sitecore 6.5 but I found adding one is quite easy so here is a little tutorial.

Step 1 Create a Filter

  • Browse to /sitecore/system/Marketing Center/Analytics Filters/Report Filters.
  • Insert a new ‘Filter’ named Site.
  • Set the Type to ‘Sitecore.Analytics.Reports.Filters.ValueListFilter, Sitecore.Analytics’
  • Set the ColumnName to ‘MultiSite’

Step 2 Add Filter Values

Select your new new ‘Site’ Filter and insert two new ‘Predefined Filter Values’ name them ‘website’ and ‘site2’ and set their values accordingly.

Step 3  Add the Filters to each Report

  • Browse to the reports in /sitecore/system/Settings/Analytics/Reports SQL Queries.  ( for this example I used ‘Latest Visits’ )
  • Add the column [Visits].[MultiSite] to the SQL fields.
  • Add the new ‘Site’ filter to the Filter field.

Step 4 Go Test the filter on your report

  • Open the Report from the ‘Engagement Analytics’ Menu Option.
  • Test your new filter with the Filter Button up top.

Federated Authentication with Sitecore and the Windows Identity Foundation


UPDATE : There is a new version of the module here:  https://marketplace.sitecore.net/en/Modules/ADFSAuthenticator.aspx

 

Over the past few months I’ve done some work integrating Sitecore with multiple Federated Authentication systems like Ping Identity, ADFS and some home grown ones.  The way Federated Authentication works is instead of logging directly into an application the application sends the user to another system for authentication. Once that system authenticates the user an encrypted token, typically SAML, is passed back to the requesting application containing credentials and other information, known as claims.

Getting everything working with the Windows Identity Foundation proved to be more challenging than some of the other API’s I worked with. I’d like to thank Owain Jones, Kern Herskind Nightingale and Ivan Sharamok for their help and suggestions that made this all work.

Here is the source and update file for this solution. There is also this sample web.config.

Step 1: Setting up your development environment for claims based authentication.

This tutorial is already written so I’m just going to point to that. I suggest you set up a basic website and get claims Authentication working there before you go and try integrating it with Sitecore.

http://www.codeproject.com/Articles/278940/Claim-based-Authentication-and-WIF-Part-2

Step 2: Add Roles to Sitecore

Login To Sitecore and create the following roles :

Extranet/Registered Users

Sitecore/Author

Assign Sitecore Author to the Sitecore Client Authoring Role so they can login to the system.

Step 3: Modify the mock STS to send the roles

After you have completed that tutorial modify the STS project and change the code in CustomSecurityTokenService.cs that writes out the claims to include two roles that exist in your Sitecore system.

protected override IClaimsIdentity GetOutputClaimsIdentity(IClaimsPrincipal principal, RequestSecurityToken request, Scope scope)

{

if (null == principal)

{

throw new ArgumentNullException("principal");

}

ClaimsIdentity outputIdentity = new ClaimsIdentity();

// Issue custom claims.

// TODO: Change the claims below to issue custom claims required by your application.

// Update the application's configuration file too to reflect new claims requirement.

outputIdentity.Claims.Add(new Claim(System.IdentityModel.Claims.ClaimTypes.Name, principal.Identity.Name));

outputIdentity.Claims.Add(new Claim(ClaimTypes.Role, "Registered Users"));

outputIdentity.Claims.Add(new Claim(ClaimTypes.Role, "Author"));

return outputIdentity;

}

Step 4: Install the FedAuthenticator update package.

Login to Sitecore as an admin

Browse to /sitecore/admin/updateinstallationwizard.aspx

Install the update file found here.

Step 5 : Using Visual Studio or FedUtil.exe update the web.config of your sitecore solution to point to your STS like you did in the tutorial found under Step 1.

see : http://www.codeproject.com/Articles/278940/Claim-based-Authentication-and-WIF-Part-2

Step 6: Modify the web.config file.

Under the Modules Section move the Authentication Modules so they fire after Nexus and replace the Session Module with the one in the FedAuthenticator Module.

<add type=”Sitecore.Nexus.Web.HttpModule,Sitecore.Nexus” name=”SitecoreHttpModule” />

      <addname=“WSFederationAuthenticationModule” type=”Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35″ preCondition=”managedHandler” />

      <addname=“SessionAuthenticationModule” type=”FedAuthenticator.Authentication.WSSessionAuthenticationModule, FedAuthenticator”/>

<add type=”Sitecore.Resources.Media.UploadWatcher, Sitecore.Kernel” name=”SitecoreUploadWatcher” />

Change the Location information inside of the web.config file so it looks like the following:

</system.webServer>

<location path=”sitecore modules/Shell/FedAuthenticator/sso.aspx”>

<system.web>

<authorization>

<deny users=”?”/>

</authorization>

</system.web>

</location>

<system.web>

<!–<authorization>

<deny users=”?” />

</authorization>–>

<!– Continue to run Sitecore without script validations –>

If you now try to hit sitecore modules/Shell/FedAuthenticator/sso.aspx the user should be logged into Sitecore with the Author Role.

Step 7: Create a secured page in Sitecore.

Under the Home Item create a new Sample Item and name it ‘secured’.

Set the permissions up to break inheritance for Extranet/Everyone then grant read access and enable enheritance for the Extranet/Register Users role.

Step 8 : Test authentication to the front end of the website.

Log out of Sitecore:

Clear your cookies to remove the pre-existing token.

Browse to http://yourwebsite/secured

You should be redirected to the STS token issuer. Once you log in you will be brought back to your website and should see the secured item.

It may be helpful to add this code to your sample layout.aspx page so you can see the users information.

<div><%=Sitecore.Context.User.Name %></div>

<div>Is Virtual? <%=Sitecore.Context.User.RuntimeSettings.IsVirtual %></div>

<div>Roles:</div>

<%

foreach (var role in Sitecore.Context.User.Roles)

{

%>

<div><%=role.Name %></div>

<%

}

%>

Code Review

Ideally this solution would have had two processors and nothing else.

The TokenRequestor looks to see if access is denied to an item and sends the user to the STS with a request for a token if the user is not already authenticated.

The UserResolver looks for a token in a cookie, if it exists and the user is not authenticated we log the user in a virtual user.

That’s all I expected to need when I first wrote this but I was having trouble persisting user information.  This led us to the Sitecore support portal and we found we needed a custom authentication provider and we had to overwrite the WSSessionModule to get this to work.

The root of the problem is three fold:

  1. Both Sitecore and the Windows Identity Foundation are fighting over the threads user identity located at HttpContext.Current.Request.User.
  2. The Windows Identity Foundation does not allow you to just request and parse a token just using the API. It forces you to use the http modules. If I could do this without the modules there would be a lot less code.  This is the approach I took when integrating Ping Identity and it was much easier but my Ping Identity code only works with Ping Identity and this solution should work with a lot of other identity providers, including Ping Identity.
  3. The Sitecore Nexus API is obfuscated and I had trouble figuring out where/when the Identity was being set.

So, now here’s all the ‘other’ code that was added to get the two systems to play nice.

WSSessionAuthenticationModule was added to prevent some of the Session overriding. The binding to PostAuthenticateRequest was removed from initialize module and we override the Authenticate Request method so it exits the method if we already have a Sitecore.Context.User.

The FederatedAuthenticationProvider was also added to get the role and user information to persist. The Forms Authentication Provider uses a cookie for saving the value but this cookie wasn’t persisting when we logged in using SSO so we read the Session Token instead inside of the GetCurrentUser method of the Authentication Helper.

The login method was also changed to not write out a forms cookie.

The sso.aspx page is being used to log in to Sitecore.  The user is assigned to the roles based on the Sitecore Domain. That is why the page currently lives under the sitecore modules folder.

Things that could use improvement

  1. Currently the user is logged in on every request when a token exists. I’d like to be able to just parse the token the first time it’s sent from the STS and log the user in once but that didn’t work out.
  2. Single Sign Out is not yet implemented.
  3. This solution is currently only supporting virtual users.

Sitecore setting Timeout on a Virtual User


A customer was having issues getting a virtual users expiration to timeout. In looking at the code in the Authentication Manager I found that the method being used to load the virtual user passes expires as false and didn’t have an override.

To fix it I overwrote the Login(User) method of the  Authentication Provider and passed in the true variable.

Here’s my Authentication Provider.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Sitecore.Diagnostics;
using System.Web.Security;
using Sitecore.SecurityModel;
using Sitecore.Configuration;
using Sitecore.Security.Accounts;

namespace AdventureWorksMembership
{
    public class FormsAuthenticationProvider : Sitecore.Security.Authentication.FormsAuthenticationProvider
    {
        public override bool Login(Sitecore.Security.Accounts.User user)
        {
            Assert.ArgumentNotNull(user, "user");
            if (!base.Login(user))
            {
                return false;
            }
            this.StoreMetaData(user);
            FormsAuthentication.SetAuthCookie(user.Name, true);
            return true;
        }

        private void StoreMetaData(Sitecore.Security.Accounts.User user)
        {
            UserRuntimeSettings runtimeSettings = user.RuntimeSettings;
            if (runtimeSettings.IsVirtual)
            {
                ClientContext.SetValue("SC_USR_" + user.Name, runtimeSettings.Serialize());
            }
        }
    }
}

And in my web.config file

<!-- AUTHENTICATION -->
<authentication defaultProvider="forms">
<providers>
<clear/>
<add name="forms" type="AdventureWorksMembership.FormsAuthenticationProvider, AdventureWorksMembership"/>
</providers>
</authentication>

<authentication mode="None">
<forms name=".ASPXAUTH" cookieless="UseCookies" timeout="20"/>
</authentication>

It should be noted that setting slidingexpiration to true doesn’t work so this could open a whole new set of worms for you.

I have a fix for that here in this forum thread : http://sdn.sitecore.net/SDN5/Forum/ShowPost.aspx?pageindex=2&postid=21977

HTML5 video with Sitecore


I worked with a client that was having trouble getting HTML5 video to play in Sitecore. Inside of our support tickets I found the trick to getting it to work is to add the following MIME Types to the App_Config/MIMETypes.config file.

  <mediaType extensions="ogg">
    <mimeType>video/ogg</mimeType>
  </mediaType>
  <mediaType extensions="m4v">
    <mimeType>video/mp4</mimeType>
  </mediaType>

After that I was able to play video with those two formats in IE9 and the latest versions of FF and Chrome. I have not tested on any mobile devices or older browsers.

With HTML5 you need to specify the source video for each codec you want to support because the browsers do not support the same codecs.

<video controls="controls"">
   <source src="~/media/Files/sample_h264.m4v" type="video/mp4" />
   <source src="~/media/Files/sample_ogg.ogg" type="video/ogg"/>
   <p>Your browser does not support the <code>video</code> element.</p>
 </video>

Multiple Sites with One Authoring Domain in Sitecore


Sitecore allows you to manage multiple sites by binding a hostname to a root content item via the webconfig. This works fine in the delivery enviornment but for authoring you would also now need a seperate entry for each site if your site is dependent on the Sitecore.Context.Site at all.

Adding the configs settings isn’t a huge deal but if you have 50 sites you now need 50 config settings for the authoring urls, 50 for the editing urls, 100 dns entries and 50 authoring bookmarks for your authors.

I came up with the following approach to resolving the site in page editor and preview modes to help alleviate some of this pain. I can now use one authoring domain and when I push the Page Editor or Preview buttons Sitecore will resolve the site for the item.

To achieve this I did four things on top of the normal setup :

  • Added SiteResolver to resolve site based on Item if sc_itemid is in the Query String and we are in Page Editor or Preview mode.
  • Override the LinkProvider to include the sc_site in the Query String when in Page Editor or Preview Mode.
  • Turn Off Rendering.SiteResolving
  • Add A Site Entry for the “Authroing” site with the root of the home node.

Normal Setup
Root Content Items

IIS Site Bindings

Added Sites to web.config (notice host names and start items)


      <site name="website" hostName="training65" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site2"  hostName="site2.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/Site2" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site3"  hostName="site3.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/site3" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />

With the setup above and I log into each domain and author the sites but I can’t author from one site and have the Site resolve correctly.
Authoring Multiple Sites from One Domain
First I add my new authoring site below my last site because I want the others to resolve first on the delivery side.

      <site name="website" hostName="training65" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site2"  hostName="site2.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/Site2" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="site3"  hostName="site3.training" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/site3" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
      <site name="authoring" hostName="" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />

Now if I login to editing.training domain it resolves to this authoring site that doesn’t contain a domain name. I can log in and Edit all three of my sites but if the sites are relying on anything contained in the Sitecore.Context.Site this information will be pointing to the Authoring Site and it will be wrong.

I remembered how the cross site links resolve to the correct domain when Rendering.SiteResolving is set to true so I reflected on that code and came up with a class (mostly copy pasta’d….including the code with the Go To Label) to resolve the Site for an Item without looking at the host name.

This first class is the Link Helper Class and it’s ResolveTargetSite method is what we need.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Web;
using Sitecore.IO;
using Sitecore.Sites;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Prototypes.Links
{
    public static class LinkHelper
    {
        private static Dictionary<string, SiteInfo> _siteResolvingTable;
        private static List<SiteInfo> _sites;
        private static readonly object _syncRoot = new object();
        private delegate string KeyGetter(SiteInfo siteInfo);

        public static SiteInfo ResolveTargetSite(Item item)
        {
            SiteContext site = Context.Site;
            SiteContext context2 = site;
            SiteInfo info = (context2 != null) ? context2.SiteInfo : null;
            if ((context2 != null) && item.Paths.FullPath.StartsWith(context2.StartPath))
            {
                return info;
            }
            Dictionary<string, SiteInfo> siteResolvingTable = GetSiteResolvingTable();
            string key = item.Paths.FullPath.ToLowerInvariant();
        Label_00AE:
            if (siteResolvingTable.ContainsKey(key))
            {
                return siteResolvingTable[key];
            }
            int length = key.LastIndexOf("/");
            if (length > 1)
            {
                key = key.Substring(0, length);
                goto Label_00AE;
            }
            return info;
        }

        private static Dictionary<string, SiteInfo> GetSiteResolvingTable()
        {
            List<SiteInfo> sites = SiteContextFactory.Sites;
            if (!object.ReferenceEquals(_sites, sites))
            {
                lock (_syncRoot)
                {
                    if (!object.ReferenceEquals(_sites, sites))
                    {
                        _sites = sites;
                        _siteResolvingTable = null;
                    }
                }
            }
            if (_siteResolvingTable == null)
            {
                lock (_syncRoot)
                {
                    if (_siteResolvingTable == null)
                    {
                        _siteResolvingTable = BuildSiteResolvingTable(_sites);
                    }
                }
            }
            return _siteResolvingTable;
        }

        private static Dictionary<string, SiteInfo> BuildSiteResolvingTable(IEnumerable<SiteInfo> sites)
        {
            Dictionary<string, SiteInfo> dictionary = new Dictionary<string, SiteInfo>();
            List<string> list = new List<string>();
            KeyGetter[] getterArray = new KeyGetter[] { info => FileUtil.MakePath(info.RootPath, info.StartItem).ToLowerInvariant() };
            foreach (KeyGetter getter in getterArray)
            {
                foreach (SiteInfo info in sites)
                {
                    if (!string.IsNullOrEmpty(info.HostName) && string.IsNullOrEmpty(GetTargetHostName(info)))
                    {
                        Log.Warn(string.Format("LinkBuilder. Site '{0}' should have defined 'targethostname' property in order to take participation in site resolving process.", info.Name), typeof(LinkProvider.LinkBuilder));
                    }
                    else if ((!string.IsNullOrEmpty(GetTargetHostName(info)) && !string.IsNullOrEmpty(info.RootPath)) && !string.IsNullOrEmpty(info.StartItem))
                    {
                        string key = getter(info);
                        if (!list.Exists(new Predicate<string>(key.StartsWith)))
                        {
                            dictionary.Add(key, info);
                            list.Add(key);
                        }
                    }
                }
            }
            return dictionary;
        }

        private static string GetTargetHostName(SiteInfo siteInfo)
        {
            if (!string.IsNullOrEmpty(siteInfo.TargetHostName))
            {
                return siteInfo.TargetHostName;
            }
            string hostName = siteInfo.HostName;
            if (hostName.IndexOfAny(new char[] { '*', '|' }) < 0)
            {
                return hostName;
            }
            return string.Empty;
        }

    }
}

Next I create a custom SiteResolver and place it after the normal SiteResolver in the begin request pipeline.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Web;
using Sitecore.Sites;

namespace Sitecore.Prototypes.Links
{
    public class SiteResolver : Sitecore.Pipelines.HttpRequest.SiteResolver
    {

        public override void Process(Pipelines.HttpRequest.HttpRequestArgs args)
        {
            if ((Sitecore.Context.PageMode.IsPreview || Sitecore.Context.PageMode.IsPageEditor) 
                && HttpContext.Current.Request.QueryString["sc_itemid"] != null)
            {
                Database db = Sitecore.Context.Database;
                if (db != null)
                {
                    Item item = db.SelectSingleItem(HttpContext.Current.Request.QueryString["sc_itemid"]);
                    if (item != null)
                    {
                        SiteInfo site = LinkHelper.ResolveTargetSite(item);
                        SiteContext siteContext = SiteContextFactory.GetSiteContext(site.Name);
                        UpdatePaths(args, siteContext);
                        Sitecore.Context.Site = siteContext;
                    }
                }
            }          
        }
    }
}

Add that to the httpRequestBeing pipeline right after the normal site resolver

        <processor type="Sitecore.Pipelines.HttpRequest.SiteResolver, Sitecore.Kernel" />
        <processor type="Sitecore.Prototypes.Links.SiteResolver, Sitecore.Prototypes.Links"/>

Now when I hit the Page Editor or Preview buttons the site is displayed using the correct Sitecore.Context.Site, but if I click on a link to another page it loses the context….

To fix this I added a custom link provider that appends the sc_site to the query string if we are in preview or pageditor mode.

Here is the class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Sites;

namespace Sitecore.Prototypes.Links
{
    public class LinkProvider : Sitecore.Links.LinkProvider
    {

        public override string GetItemUrl(Data.Items.Item item, Sitecore.Links.UrlOptions options)
        {
            string url = base.GetItemUrl(item, options);
            if (Sitecore.Context.PageMode.IsPageEditor || Sitecore.Context.PageMode.IsPreview)
            {
                url = AppendOption(url, "&sc_site=" + LinkHelper.ResolveTargetSite(item).Name);
            }
            return url;
        }

        public override string GetDynamicUrl(Data.Items.Item item, Sitecore.Links.LinkUrlOptions options)
        {
            string url  = base.GetDynamicUrl(item, options);
            if (Sitecore.Context.PageMode.IsPageEditor || Sitecore.Context.PageMode.IsPreview)
            {
                url = AppendOption(url, "&sc_site=" + LinkHelper.ResolveTargetSite(item).Name);
            }
            return url;
        }

        public string AppendOption(string url, string option)
        {
            return url.Contains("?") ? url + option : url + "?" + option;
        }
    }
}

And here is where I overrode it in the config section

		<linkManager defaultProvider="authoring">
			<providers>
				<clear />
				<add name="sitecore" type="Sitecore.Links.LinkProvider, Sitecore.Kernel" addAspxExtension="false" alwaysIncludeServerUrl="true" encodeNames="true" languageEmbedding="asNeeded" languageLocation="filePath" shortenUrls="false" useDisplayName="false" />
        <add name="authoring" type="Sitecore.Prototypes.Links.LinkProvider, Sitecore.Prototypes.Links" addAspxExtension="false" alwaysIncludeServerUrl="true" encodeNames="true" languageEmbedding="asNeeded" languageLocation="filePath" shortenUrls="false" useDisplayName="false"  />
			</providers>
		</linkManager>

Now my last problem is that because Rendering.SiteResolving is set to true so my domain name is switching so we need to set that to false in the Authoring Environment while we probably want it true on the delivery side.

       <setting name="Rendering.SiteResolving" value="false" />

Notes

  • I wouldn’t expect this to work with the Shared Source DB Site Resolver and I’m not sure about the other Shared Source Multi-Site piece.
  • Posting code in wordpress is painfull.
  • So is copying from it so I’ll try to get a download up

Get Random Item from XSLT Rendering


Had a request to write an XSLT Extensions that can select a random child of an Item.  I wrote this extension that takes in a Sitecore Query and randomly selects one of the times. You could use it to do something like randomly display a Teaser from a Teaser Folder.

After compiling the code you will have to register the XSLT extension in your web.config file and add the namespace to the top of your XSLT Transformation.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Xml.Xsl;
using Sitecore.Data.Items;
using System.Xml.XPath;
using Sitecore.Configuration;
using Sitecore.Xml.XPath;

namespace Sitecore.Prototypes.Client.Xslt
{
    public class Extensions : XslHelper
    {
        private static readonly XPathNavigator emptyNavigator;
        private static Random randomInstance = new Random();

        public XPathNodeIterator GetRandomResult(string query)
        {
            Item[] items = Sitecore.Context.Database.SelectItems(query);
            if (items != null)
            {
                int skip = randomInstance.Next(items.Count());
                try
                {
                    Item item = items[skip];
                    if (item == null)
                    {
                        return CreateEmptyIterator();
                    }
                    ItemNavigator navigator = Factory.CreateItemNavigator(item);
                    if (navigator == null)
                    {
                        return CreateEmptyIterator();
                    }
                    return navigator.Select(".");

                }
                catch (Exception e)
                {
                    Sitecore.Diagnostics.Log.Fatal(e.Message, this);
                }
            }
            return null;
        }
        private XPathNodeIterator CreateEmptyIterator()
        {
            return emptyNavigator.Select("*");

        }
    }
}