Category Archives: Content editor

Handle URL redirects in a Sitecore multisite solution using FileMapProvider

redirect-me-baby_3

I would like to share with you guys how you could do URL redirects in a Sitecore multisite solution with Microsoft’s URL Rewrite Module using the FileMapProvider.

FilemapProvider is very cool, it reads the URL mappings from a text file.

It can be used instead of the built-in rewrite maps functionality when the amount of rewrite map entries is very large and it is not practical to keep them in a web.config file.

And the best thing of all:
No need to do any custom pipelines in Sitecore – it’s clean 🙂
You don’t have to do an IIS reset after the redirect file has been changed/updated.

The idea is this:
We have a Sitecore solution with several websites, each website will have a number of redirects. In Sitecore we will have a Bucket with “URL Redirects” items, each item will contain an old and a new url. From the Content Editor we want to be able to click on a command button that will trigger a remote event(we need it to work in a multi-server environment), which will write the redirects to a text file(for the FileMapProvider).

How about divide the work into following:
1. Configure the FileMapProvider/s.
2. Setup the URL redirects in Sitecore.
3. Create the command button in Sitecore Content Editor.
4. Create the (remote) event and write redirects to file.

1. Configure the FileMapProvider/s.

The FileMapProvider is part of the URL Rewrite Extensibility Samples, which allows us to use custom providers for the rewrite\redirect rules – in our case: external .txt file with URL’s.

/old/catalog/product; /new/category/product
/old/contactus/index; /new/contactus

In our multisite scenario we will have two websites running, testsite1.com and testsite2.com. Which means we will have two FileMapProvider’s, one for each website.

Let’s have a look at how the FilemapProviders are configured in the UrlRewrite.config. Yes it’s a web.config transformation(because Sitecore config file patching only works within the sitecore section)

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <system.webServer>
	<rewrite>
	  <providers>
		<provider name="FileMapProviderForTestSite1" type="FileMapProvider, Microsoft.Web.Iis.Rewrite.Providers, Version=7.1.761.0, Culture=neutral, PublicKeyToken=0545b0627da60a5f"  xdt:Transform="Insert">
		  <settings>
			<add key="FilePath" value="D:\Files\Data\UrlRedirectsForTestSite1.txt"/>
			<add key="IgnoreCase" value="1"/>
			<add key="Separator" value=";"/>
		  </settings>
		</provider>
		<provider name="FileMapProviderForTestSite2" type="FileMapProvider, Microsoft.Web.Iis.Rewrite.Providers, Version=7.1.761.0, Culture=neutral, PublicKeyToken=0545b0627da60a5f"  xdt:Transform="Insert">
		  <settings>
			<add key="FilePath" value="D:\Files\Data\UrlRedirectsForTestSite2.txt"/>
			<add key="IgnoreCase" value="1"/>
			<add key="Separator" value=";"/>
		  </settings>
		</provider>
	  </providers>
	  <rules>
		<rule name="RuleForFileMapProviderForTestSite1" stopProcessing="false" xdt:Transform="Insert">
		  <match url="(.*)"/>
		  <conditions>
		    <add input="{HTTP_HOST}" pattern="^testsite1.com$"/>
			<add input="{FileMapProviderForTestSite1:{REQUEST_URI}}" pattern="(.+)"/>
		  </conditions>
		  <action type="Redirect" url="{C:1}" appendQueryString="false"/>
		 </rule>
		 <rule name="RuleForFileMapProviderForTestSite2" stopProcessing="false" xdt:Transform="Insert">
		  <match url="(.*)"/>
		  <conditions>
		    <add input="{HTTP_HOST}" pattern="^testsite2.com$"/>
			<add input="{FileMapProviderForTestSite2:{REQUEST_URI}}" pattern="(.+)"/>
		  </conditions>
		  <action type="Redirect" url="{C:1}" appendQueryString="false"/>
		</rule>
	  </rules>
	</rewrite>
  </system.webServer>
</configuration>		

If you notice there are two FileMapProvider configurations, one provider for each site – how cool is that 🙂 The provider points to a text file(containing the redirects for that specific site).
Now in order to get the redirect magic to work we need to set up a rule. If you look at the rule RuleForFileMapProviderForTestSite1, you will see that this rule will only work for website – testsite1.com.
We do that by using pattern(below) to check if it matches with HTTP_HOST:

pattern="^testsite1.com$"

Then we use/get the “site” specific provider, FileMapProviderForTestSite1, to do the redirect.

Tip!
If you guys don’t want to install the URL Rewrite Extensibility Sample on a test or production server, fear not. Just grab it from your GAC on your local dev machine.
Fire up your PowerShell(for windows) and use the SUBST Command. Suppose you want to create a G Drive (G for GAC), use the following command:

SUBST G: C:\WINDOWS\ASSEMBLY

Look in folder G:\GAC_MSIL and you will find Microsoft.Web.Iis.Rewrite.Providers 🙂

2. Setup the URL redirects in Sitecore

Next will be to setup the URL redirects in sitecore. The folder structure for the redirects.
treeredirects

UrlRedirectsItem: It will contain two fields – NewUrl and OldUrl
redirectitem

UrlRedirectsFolderItem: I’ve added a field in order to figure out what site it belongs to.
folderitem

3. Create the command button in Sitecore Content Editor

Next will be to create the command that will trigger the writing of redirects to a text file. I wanted the button only to be visible when the “UrlRedirects folder” is selected/marked:
contenteditorcommand
I also added a confirm dialog, in case the editor wants to cancel the request.

In the Sitecore Core database we will add the command button, we will use the Large Button template. Navigate to /sitecore/content/Applications/Content Editor/Ribbons/Contextual Ribbons and create the new ribbon – Sync Url Redirects:
corecommand
We will also create a new command – SyncUrlRedirects:ToFile

In order for the command button only to be visible for a specific template(in this case the Url Redirects folder) we need to go to the template and then in the Appearance Section locate the drop tree field, Ribbon. And finally select our newly created ribbon – Sync Url Redirects:
templatecommand

We will patch the new command to a config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="SyncUrlRedirects:ToFile" type="Sandbox.Website.Commands.SyncUrlRedirectsFromSitecoreToFileCommand,Sandbox.Website" />
    </commands>
  </sitecore>
</configuration> 

Now we need to add some code for the command button.
In the Execute method we will verify that we are standing on the UrlRedirectsSiteFolder item, next will be to get all the children(deriving from template UrlRedirect). The GetChildrenByTemplateWithFallback method is an extension method from the Sitecore.ContentSearch.Utilities.
I need to send some parameters to the Confirm dialog(number of redirect items and the item id of the Redirects Folder) and finally call/execute the Confirm dialog by using Sitecore.Context.ClientPage.Start.

public class SyncUrlRedirectsFromSitecoreToFileCommand : Command
{

	private const string QueryParamRedirectsToBeSynced = "count";
	private const string QueryParamFolderId = "folderid";

	public override void Execute(CommandContext context)
	{
		Assert.ArgumentNotNull(context, "context");

		Assert.IsNotNull(context.Items, "context items are null");

		Assert.IsTrue(context.Items.Length > 0, "context items length is 0");

		Item item = context.Items[0];

		Assert.IsNotNull(item, "Item is null");

		Assert.IsTrue(item.IsDerived(SitecoreTemplates.UrlRedirectsSiteFolder.ID), "Item is not of template UrlRedirectsSiteFolder");

		int numberOfRedirects =
		item.GetChildrenByTemplateWithFallback(SitecoreTemplates.UrlRedirect.ID.ToString()).Count();

		context.Parameters.Add(QueryParamRedirectsToBeSynced, numberOfRedirects.ToString());
		context.Parameters.Add(QueryParamFolderId, item.ID.ToString());

		Sitecore.Context.ClientPage.Start(this, "Confirm", context.Parameters);
	}

	protected void Confirm(ClientPipelineArgs args)
	{

		if (args.IsPostBack)
		{
			switch (args.Result)
			{
				case "yes":
					SyncUrlRedirectsEvent syncUrlRedirectsEvent = new SyncUrlRedirectsEvent(args.Parameters[QueryParamFolderId]);
					Sitecore.Eventing.EventManager.QueueEvent(syncUrlRedirectsEvent, true, true);
					return;

				case "no":
					return;
			}
		}

		SheerResponse.Confirm($"It is important that you publish all redirects before you continue. {args.Parameters[QueryParamRedirectsToBeSynced]} redirects will be synced.");
		args.WaitForPostBack();

	}

	public override CommandState QueryState(CommandContext context)
	{
		Assert.ArgumentNotNull(context, "context");

		if (context.Items.Length != 1)
			return CommandState.Hidden;

		Item item = context.Items[0];

		Assert.IsNotNull(item, "First context item is null");

		Sitecore.Data.Version[] versionNumbers = item.Versions.GetVersionNumbers(false);
		if (versionNumbers == null || versionNumbers.Length == 0 || item.Appearance.ReadOnly)
			return CommandState.Disabled;

		if (!item.Access.CanWrite() || !item.Access.CanRemoveVersion() || IsLockedByOther(item))
			return CommandState.Disabled;

		return base.QueryState(context);

	}

}

In the Confirm method we will check if the if the editor hits the yes button, if so we will call the SyncUrlRedirectsEvent and pass along the item id of the Redirects Folder.

4. Create the (remote) event and write redirects to file

In order to write the redirects to a file on a CD server, we will need a remote event – SyncUrlRedirectsEvent. The event is quite simple, it will just hold the item id of the Redirects Folder.
Here is the event and its EventArgs class:

[DataContract]
public class SyncUrlRedirectsEvent
{
	public SyncUrlRedirectsEvent(string urlRedirectsSiteFolderItemId)
	{
		UrlRedirectsSiteFolderItemId = urlRedirectsSiteFolderItemId;
	}

	[DataMember]
	public string UrlRedirectsSiteFolderItemId { get; protected set; }

}

[Serializable]
public class SyncUrlRedirectsEventArgs : EventArgs, IPassNativeEventArgs
{
	public SyncUrlRedirectsEventArgs(string urlRedirectsSiteFolderItemId)
	{
		UrlRedirectsSiteFolderItemId = urlRedirectsSiteFolderItemId;
	}

	public string UrlRedirectsSiteFolderItemId { get; protected set; }
}

Next is the eventhandler, that is the one which will be called/executed on the CD server, so we can write the redirects for the FileMapProvider. Here we fetch all redirects from the “UrlRedirectsSiteFolderItem id”(which we got from the SyncUrlRedirectsEventArgs) and then write them to a text file.
I will not go into how the SyncUrlRedirectsService works, it’s quite straightforward. If you are interested in how it’s done just ping me and I will show you 🙂

public class SyncUrlRedirectsEventHandler
{
	private static ILogger _logger;
	private readonly ISyncUrlRedirectsService _syncUrlRedirectsService;
	private readonly IDatabaseRepository _databaseRepository;


	public SyncUrlRedirectsEventHandler()
	{
		_logger = IocContext.NonRequestContainer.GetInstance<ILogger>();
		_syncUrlRedirectsService = IocContext.NonRequestContainer.GetInstance<ISyncUrlRedirectsService>();
		_databaseRepository = new DatabaseRepository();
			
	}

	public virtual void OnRemoteSyncUrlRedirects(object sender, EventArgs e)
	{

		if (e == null)
		{
			_logger.Error("SyncUrlRedirectsEventArgs is empty");
			return;
		}
			

		SyncUrlRedirectsEventArgs syncUrlRedirectsEventArgs = (SyncUrlRedirectsEventArgs) e;

		if (syncUrlRedirectsEventArgs.UrlRedirectsSiteFolderItemId.IsNullOrWhiteSpace())
		{
			_logger.Error("UrlRedirectsSiteFolder item id is missing in SyncUrlRedirectsEventArgs");
			return;
		}

		Item urlRedirectsSiteFolderItem =
			_databaseRepository.ContextDatabase.GetItem(new ID(syncUrlRedirectsEventArgs.UrlRedirectsSiteFolderItemId));


		if (urlRedirectsSiteFolderItem == null)
		{
			_logger.Error("UrlRedirectsSiteFolderItem does not exist on {0}. Id: {1}", _databaseRepository.ContextDatabase.Name, syncUrlRedirectsEventArgs.UrlRedirectsSiteFolderItemId);
			return;
		}

		string sitename = urlRedirectsSiteFolderItem.GetString(SitecoreTemplates.UrlRedirectsSiteFolder.Fields.UrlRedirectsSiteName);

		if (sitename.IsNullOrWhiteSpace())
		{
			_logger.Error("Site name is not set in UrlRedirectsSiteFolder item id, {0}", urlRedirectsSiteFolderItem.ID);
			return;
		}


		IList<string[]> urlRedirectsList =	urlRedirectsSiteFolderItem.GetChildrenByTemplateWithFallback(SitecoreTemplates.UrlRedirect.ID.ToString())
			.Select(
				item =>
					new[] {item.GetString(SitecoreTemplates.UrlRedirect.Fields.UrlRedirectOldUrl), item.GetString(SitecoreTemplates.UrlRedirect.Fields.UrlRedirectNewUrl) }).ToList();


		Tuple<IList<string>, string> redirectsAndFilePath =  _syncUrlRedirectsService.PrepareRedirectsAndFilePathForFile(urlRedirectsList, sitename);

		_syncUrlRedirectsService.TryWriteRedirectsToFile(redirectsAndFilePath, sitename);

	}


	public static void Run(SyncUrlRedirectsEvent syncUrlRedirectsEvent)
	{
		_logger.Information("SyncUrlRedirectsEventHandler - Run", typeof(SyncUrlRedirectsEventHandler));

		SyncUrlRedirectsEventArgs args = new SyncUrlRedirectsEventArgs(syncUrlRedirectsEvent.UrlRedirectsSiteFolderItemId);

		Event.RaiseEvent("syncurlredirects:remote", args);
	}
}

The Run method is called from a Hook.

To glue it all together(event and eventhandler) we need to register and trigger the eventhandler, for that we will use a hook. See the hook as a very light pipeline.

public class SyncUrlRedirectsHook : IHook
{
	public void Initialize()
	{
		Sitecore.Eventing.EventManager.Subscribe(new Action<SyncUrlRedirectsEvent>(SyncUrlRedirectsEventHandler.Run));
	}
}

Here is the config file for the event and the hook:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
	<hooks>
	  <hook type="Sandbox.Website.Redirect.Events.SyncUrlRedirectsHook, Sandbox.Website" />
	</hooks>
    <events>
      <event name="syncurlredirects:remote">
        <handler type="Sandbox.Website.Redirect.Events.SyncUrlRedirectsEventHandler, Sandbox.Website" method="OnRemoteSyncUrlRedirects" />
      </event>
    </events>
  </sitecore>
</configuration>

Instead of using a hook, you can use a pipeline(for the event). Here are some great documentation regarding events in Sitecore:
http://sitecore-community.github.io/docs/pipelines-and-events/events/

That’s all for now folks 🙂

Advertisements

Query your datasource using custom tokens in Sitecore

Before heading into the post I would like to thank Sitecore for giving me the great honor of being awarded as one of the 167 MVPs. I truly enjoy sharing my experiences with people so this means a lot to me. Check out all the skilled MVPs at Sitecore MVPs 2015

Datasource

I would like to show you guys how to do queryable datasources for your renderings. This is nothing new and there are a lot of posts out there describing it very well, so why reinvent the wheel? I found some really nice posts and used most of their code.
Queries in Datasource location on Sitecore Layouts by Thomas Stern aka Dr Stern 🙂
Multiple Datasource Locations using Queries are back in Sitecore 7.2 Update-2 by Sean Holmesby

As Sean pointed out in his post – Since Sitecore 7.2 you can have multiple datasources, that is really cool.

Sitecore 7.2 Update-2 fixes this, and Sitecore’s default code will allow you to define multiple Datasource locations by having them pipe separated in the ‘Datasource Location’ field on a rendering/sublayout

I checked Sean’s and Thomas code and created my pipeline for supporting queryable datasources to the renderings. I started out doing some queries and it worked great but then I needed to to go “above” my site node to find its datasource folder.

Sitecore
–Content
—Site1
—Site2
—Site3
—Site4
—Datasource content
—–Global
—–Site1
——-Some folder
—–Site2
—–Site3
—–Site4

In other words I need to get the name of an element and then add it to the datasource path, something like this:

query:./ancestor-or-self::*[@@templatename='Site']/../Datasource content/*[@@name=name of site element])/Some folder

I read all kinds of stuff on what you can do with the query using XPath functions. Here are some good posts you should read:
SITECORE QUERY CHEAT SHEET
Querying Items from Sitecore

Good old Sitecore SDN stuff:
Sitecore Query Syntax
Data Definition Reference – chapter 4:3
Using Sitecore Fast Query

At the end I came to the conclusion that I needed following XPath function:
name(node) – XPath, XQuery, and XSLT Functions, unfortunately this one is not supported in Sitecore’s XPath

So what to do? Why not make a custom token since I already have my own pipeline for the queryable datasource. The custom token should be something like this:

<< node?Field attribute >> 
<<ancestor-or-self::*[@@templatename='Site']?@name>>   

I found this nice old post about accessing special properties of a Sitecore item the same way you get a typical field’s value:
Getting a special field value in Sitecore by Sean Kearney

Here is how the datasource finally will look like:

/sitecore/content/Datasource content/Global/Social sharing|query:./ancestor-or-self::*[@@templatename='Site']/../Datasource content/*[@@name=<<ancestor-or-self::*[@@templatename='Site']?@name>>])/Social sharing

Here is the code for the pipeline:

public class MultipleDataSourceLocationsWithQueries
{
    private IDatasourceQueryService _datasourceQueryService; 

    public void Process(GetRenderingDatasourceArgs args)
    {
        Assert.IsNotNull(args, "args");

        this._datasourceQueryService = new DatasourceQueryService(args.ContextItemPath, args.ContentDatabase, args.RenderingItem["Datasource Location"], args.DatasourceRoots);

        if (!this._datasourceQueryService.IsQueryInDataSourceLocation()) 
            return;

        this._datasourceQueryService.ProcessQuerys();
    }
}

Then I created a service for the datasource stuff.

public class DatasourceQueryService : IDatasourceQueryService
{
    private readonly string _contextItemPath;
    private readonly Database _contentDatabase;
    private readonly string _datasourceLocation;
    private readonly List<Item> _datasourceRoots;
    private readonly Regex _regEx;

    public DatasourceQueryService(string contextItemPath, Database contentDatabase, string datasourceLocation, List<Item> datasourceRoots)
    {
        this._contextItemPath = contextItemPath;
        this._contentDatabase = contentDatabase;
        this._datasourceLocation = datasourceLocation;
        this._datasourceRoots = datasourceRoots;
        this._regEx = new Regex(GetRegexPattern());
    }

        
    public bool IsQueryInDataSourceLocation()
    {
        return this._datasourceLocation.Contains(QueryIdentifier);
    }

    public void ProcessQuerys()
    {
        ListString possibleQueries = new ListString(this._datasourceLocation);
        foreach (string possibleQuery in possibleQueries)
        {
            if (possibleQuery.StartsWith(QueryIdentifier))
            {
                ProcessQuery(possibleQuery);
            }
        }
    }

    private string GetRegexPattern()
    {
        return String.Format(".*{0}(.*){1}.*", SpecialStartItemDelimiter, SpecialEndItemDelimiter);
    }


    private void ProcessQuery(string query)
    {
        //Remove the "query:."
        query = query.Replace(QueryIdentifier, "");

        Item[] datasourceLocations = ResolveDatasourceRootFromQuery(query);

        if (datasourceLocations == null || !datasourceLocations.Any()) 
            return;

        foreach (Item dataSourceLocation in datasourceLocations.Where(dataSourceLocation => !this._datasourceRoots.Exists(item => item.ID.Equals(dataSourceLocation.ID))))
        {
            this._datasourceRoots.Add(dataSourceLocation);
        }
    }

    private Item[] ResolveDatasourceRootFromQuery(string queryPath)
    {
        Match matchedData = this._regEx.Match(queryPath);

        int numberOfMatchedTokens = 1;

        while (matchedData.Success)
        {
            //Just in case - I hate while loops
            if (numberOfMatchedTokens > MaxNumberOfIterations)
                break;
                
            queryPath = GenerateQueryPathForSpecialToken(matchedData, queryPath);
            matchedData = this._regEx.Match(queryPath);

            numberOfMatchedTokens++;
        }

        return GetItemsFromQuery(queryPath);
    }

    private string GenerateQueryPathForSpecialToken(Match matchedData, String queryPath)
    {
        string specialQueryPath = matchedData.Groups[1].ToString();

        string translatedValue = ResolveSpecialItemInQuery(specialQueryPath);

        return queryPath.Replace(
            string.Format("{0}{2}{1}", SpecialStartItemDelimiter, SpecialEndItemDelimiter, specialQueryPath),
            translatedValue);
    }

    private Item[] GetItemsFromQuery(string queryPath)
    {
        try
        {
            return this._contentDatabase.SelectItems(string.Format("{0}/{1}", this._contextItemPath, queryPath));
        }
        catch (Exception ex)
        {
            Log.Error(String.Format(@"Datasource query was not valid:{0}", queryPath), ex, this);
            return null;
        }
    }

    private string ResolveSpecialItemInQuery(string specialQueryPath)
    {
        if (!specialQueryPath.Contains(FieldDelimiter))
            return string.Empty;

        string specialItemPath = specialQueryPath.Split(char.Parse(FieldDelimiter))[0];

        if (string.IsNullOrWhiteSpace(specialItemPath))
            return string.Empty;

        Item[] specialItems = GetItemsFromQuery(specialItemPath);

        if (!specialItems.Any())
            return string.Empty;

        string fieldProperty = specialQueryPath.Split(char.Parse(FieldDelimiter))[1];

        if (string.IsNullOrWhiteSpace(fieldProperty))
            return string.Empty;

        return specialItems.Select(item => item[fieldProperty]).FirstOrDefault();

    }

    private const string QueryIdentifier = "query:.";
    private const string SpecialStartItemDelimiter = "<<";
    private const string SpecialEndItemDelimiter = ">>";
    private const string FieldDelimiter = "?";
    private const Int32 MaxNumberOfIterations = 10;

}

The interface for the service

public interface IDatasourceQueryService
{
    bool IsQueryInDataSourceLocation();

    void ProcessQuerys();
}

In order to make the pipeline work we need to do a patch config file.

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getRenderingDatasource>
       <processor type="Sandbox.QueryableDatasource.Pipelines.GetRenderingDatasource.MultipleDataSourceLocationsWithQueries, Sandbox.QueryableDatasource"
                   patch:after="processor[@type='Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceLocation, Sitecore.Kernel']" />
        </processor>
      </getRenderingDatasource>
    </pipelines>
  </sitecore>
</configuration>

That’s all for now folks 🙂

Create a custom control in Sitecore: DateTime with TimeZone

DateTimeWithTimeZone

2015 will be a great year, especially when Sitecore recently released their flagship Sitecore 8. I just love the design and all the new cool things that comes with it.

I want to show you guys how to make a custom control – DateTime with TimeZone. There is a great post out there which explains it very well – Sitecore Date Time picker with time zone. In this case I needed to store the DateTime together with the TimeZone.
In “raw values” the control will look like this:
DateTimeWithTimeZoneRaw

What we need to do:
1. Create the control(Content editor)
2. Register the control in Sitecore(Core database)
3. Create a render field pipeline(For page editor and the “website”)

Let us dive in to the code 🙂

Create the control

First we do the actual control which will have a DateTime picker and a dropdown containing all TimeZones. We need to create a class which inherits from Sitecore.Shell.Applications.ContentEditor.DateTime(Sitecore DateTime picker). In the DoRender method we will add/create the dropdown with timezones.

protected override void DoRender(System.Web.UI.HtmlTextWriter output)
{

    if (!string.IsNullOrWhiteSpace(base.RealValue) && base.RealValue.Contains("|"))
    {
        TimeZone = _dateWithTimeZoneService.GetTimeZoneValue(base.RealValue);
        base.SetValue(_dateWithTimeZoneService.CalculateDateTimeWithTimeZone(base.RealValue));
    }
                 
    output.Write("<div style='display:inline;float:left'>");
    base.DoRender(output);
    output.Write(TimeZoneDroplist());
    output.Write("</div>");

}

Since we store the DateTime value together with the TimeZone id (base.RealValue property from class Sitecore.Shell.Applications.ContentEditor.Date holds the “raw value”) we need to trick the Sitecore DateTime Picker (It expects a DateTime value). That’s why we set a calculated DateTime value in the base.SetValue method (From Sitecore.Shell.Applications.ContentEditor.Date)

The method for generating the dropdown with timezones is quite straight forward. To get the timezones we use the System.TimeZoneInfo.GetSystemTimeZones() method.

private string TimeZoneDroplist()
{
    StringBuilder stringBuilderSelect = new StringBuilder();

    stringBuilderSelect.AppendFormat(@"<select {0} {1} >", this.GetControlAttributes(), string.IsNullOrWhiteSpace(base.RealValue) ? "disabled" : string.Empty);
    stringBuilderSelect.AppendLine();

    stringBuilderSelect.AppendFormat(@"<option value='' >{0}</option>", "Please select a time zone");
           
    foreach (TimeZoneInfo timeZoneInfo in TimeZoneInfo.GetSystemTimeZones())
    {
        stringBuilderSelect.AppendFormat(@"<option value='{0}' {1} >{2}</option>", timeZoneInfo.Id, TimeZone == timeZoneInfo.Id ? "selected" : string.Empty, timeZoneInfo.DisplayName);
        stringBuilderSelect.AppendLine();
    }

    stringBuilderSelect.AppendLine();
    stringBuilderSelect.Append("</select>");

    return stringBuilderSelect.ToString();
}

The TimeZone property(viewstate) holds the selected TimeZone Id.

To store the the combined data, DateTime value and TimeZone id, we need to set the RealValue property (From Sitecore.Shell.Applications.ContentEditor.Date). We will do this in the LoadPostData method.

protected override bool LoadPostData(string value)
{
    if (value == null)
        return false;

    if (!base.RealValue.Contains(value))
        base.RealValue = string.Format("{0}|{1}", _dateWithTimeZoneService.GetDateTimeValue(base.RealValue), value);
           
    return true;
}

The “value parameter” will contain the selected TimeZone id from the TimeZone dropdown.

Here is the full code for the control.

public class DateTimePickerWithTimeZone : Sitecore.Shell.Applications.ContentEditor.DateTime
{

    private readonly IDateWithTimeZoneService _dateWithTimeZoneService;

    public string Format
    {
        get { return base.GetViewStateString("Value.Format"); }
        set
        {
            base.SetViewStateString("Value.Format", value);
        }
    }

    public string TimeZone
    {
        get { return base.GetViewStateString("Value.TimeZone"); }
        set
        {
            base.SetViewStateString("Value.TimeZone", value);
        }
    }

    public DateTimePickerWithTimeZone(): base()
    {
        _dateWithTimeZoneService = new DateWithTimeZoneService();
        Format = "MM/dd/yyyy";
    }

    protected override void DoRender(System.Web.UI.HtmlTextWriter output)
    {

        if (!string.IsNullOrWhiteSpace(RealValue) && base.RealValue.Contains("|"))
        {
            TimeZone = _dateWithTimeZoneService.GetTimeZoneValue(RealValue);
            SetValue(_dateWithTimeZoneService.CalculateDateTimeWithTimeZone(RealValue));
        }
                 
        output.Write("<div style='display:inline;float:left'>");
        base.DoRender(output);
        output.Write(TimeZoneDroplist());
        output.Write("</div>");

    }

    private string TimeZoneDroplist()
    {
        StringBuilder stringBuilderSelect = new StringBuilder();

        stringBuilderSelect.AppendFormat(@"<select {0} {1} >", this.GetControlAttributes(), string.IsNullOrWhiteSpace(RealValue) ? "disabled" : string.Empty);
        stringBuilderSelect.AppendLine();

        stringBuilderSelect.AppendFormat(@"<option value='' >{0}</option>", "Please select a time zone");
           
        foreach (TimeZoneInfo timeZoneInfo in TimeZoneInfo.GetSystemTimeZones())
        {
            stringBuilderSelect.AppendFormat(@"<option value='{0}' {1} >{2}</option>", timeZoneInfo.Id, TimeZone == timeZoneInfo.Id ? "selected" : string.Empty, timeZoneInfo.DisplayName);
            stringBuilderSelect.AppendLine();
        }

        stringBuilderSelect.AppendLine();
        stringBuilderSelect.Append("</select>");

        return stringBuilderSelect.ToString();
    }

    protected override bool LoadPostData(string value)
    {
        if (value == null)
            return false;

        if (!base.RealValue.Contains(value))
            base.RealValue = string.Format("{0}|{1}", _dateWithTimeZoneService.GetDateTimeValue(base.RealValue), value);
           
        return true;
    }
       
}

For calculating DateTime with Timezone and refining data we need a service/helper class(It will also be used by the render field class)

public interface IDateWithTimeZoneService
{
    string CalculateDateTimeWithTimeZone(string combinedDateTimeAndTimeZoneValueSeperatedWithPipe);

    string GetDateTimeValue(string combinedDateTimeAndTimeZoneValueSeperatedWithPipe);

    string GetTimeZoneValue(string combinedDateTimeAndTimeZoneValueSeperatedWithPipe);
}

public class DateWithTimeZoneService : IDateWithTimeZoneService
{

    /// <summary>
    /// Method for calculating datetime with timezone
    /// </summary>
    /// <param name="combinedDateTimeAndTimeZoneValueSeperatedWithPipe"></param>
    /// <returns></returns>
    public string CalculateDateTimeWithTimeZone(string combinedDateTimeAndTimeZoneValueSeperatedWithPipe)
    {

        String dateTime = GetDateTimeValue(combinedDateTimeAndTimeZoneValueSeperatedWithPipe);
        string zoneId = GetTimeZoneValue(combinedDateTimeAndTimeZoneValueSeperatedWithPipe);

        Assert.IsTrue(DateUtil.IsIsoDate(dateTime), "Not valid date");

        Assert.IsNotNull(zoneId, "TimeZone id is missing");

        DateTime currentDateTimeUtc = DateUtil.IsoDateToDateTime(dateTime).ToUniversalTime();
        TimeZoneInfo zone = TimeZoneInfo.FindSystemTimeZoneById(zoneId);
        DateTime localDateTime = TimeZoneInfo.ConvertTimeFromUtc(currentDateTimeUtc, zone);

        return DateUtil.ToIsoDate(localDateTime);
    }

    /// <summary>
    /// Method to get datetime
    /// </summary>
    /// <param name="combinedDateTimeAndTimeZoneValueSeperatedWithPipe"></param>
    /// <returns></returns>
    public string GetDateTimeValue(string combinedDateTimeAndTimeZoneValueSeperatedWithPipe)
    {
        return combinedDateTimeAndTimeZoneValueSeperatedWithPipe.Contains("|") ? combinedDateTimeAndTimeZoneValueSeperatedWithPipe.Substring(0, combinedDateTimeAndTimeZoneValueSeperatedWithPipe.IndexOf("|", System.StringComparison.Ordinal)) : combinedDateTimeAndTimeZoneValueSeperatedWithPipe;
    }

    /// <summary>
    /// Method to get timezone id
    /// </summary>
    /// <param name="combinedDateTimeAndTimeZoneValueSeperatedWithPipe"></param>
    /// <returns></returns>
    public string GetTimeZoneValue(string combinedDateTimeAndTimeZoneValueSeperatedWithPipe)
    {
        return combinedDateTimeAndTimeZoneValueSeperatedWithPipe.Contains("|") ? combinedDateTimeAndTimeZoneValueSeperatedWithPipe.Substring(combinedDateTimeAndTimeZoneValueSeperatedWithPipe.IndexOf("|", System.StringComparison.Ordinal) + 1) : string.Empty;
    }
}

Register the control in Sitecore

Next thing to do is to register the control in Sitecore. We will do this in System/Field types in the Core database. The easiest way is to copy the DateTime item in System Types – /sitecore/system/Field types/Simple Types/Datetime. Give it a good name and remove the “WebEdit Buttons” folder.
registercontrol
Enter the assembly name and the class for the control.

To use it just create a template and select our new field type.
SitecoreTemplate

Create a render field pipeline

OK so now we have a nice control which will work perfect in the content editor but not in the page editor or on the actual “website”. Since we store both the DateTime value and the Timezone id in a field it will mess up the render field pipeline. To fix it we need to make our own “render field pipeline” and it will be very similar to Sitecore’s Sitecore.Pipelines.RenderField.GetDateFieldValue. So we just duplicate the class and give it a good name – GetDateTimeWithTimeZoneValue

public class GetDateTimeWithTimeZoneValue
{

    private readonly IDateWithTimeZoneService _dateWithTimeZoneService;

    public GetDateTimeWithTimeZoneValue()
    {
        _dateWithTimeZoneService = new DateWithTimeZoneService();
    }

    /// <summary>
    /// Runs the processor.
    /// 
    /// </summary>
    /// <param name="args">The arguments.</param>
    public void Process(RenderFieldArgs args)
    {
        string fieldTypeKey = args.FieldTypeKey;
            
        if (fieldTypeKey != "datetimewithtimezone")
            return;

        DateRenderer renderer = this.CreateRenderer();
        renderer.Item = args.Item;
        renderer.FieldName = args.FieldName;
        renderer.FieldValue = _dateWithTimeZoneService.CalculateDateTimeWithTimeZone(args.FieldValue);

        renderer.Parameters = args.Parameters;
            
        if (!string.IsNullOrEmpty(args.Parameters["format"]))
            args.WebEditParameters["format"] = args.Parameters["format"];
            
        RenderFieldResult renderFieldResult = renderer.Render();
        args.Result.FirstPart = renderFieldResult.FirstPart;
        args.Result.LastPart = renderFieldResult.LastPart;
    }

    /// <summary>
    /// Creates the renderer.
    /// 
    /// </summary>
    /// 
    /// <returns>
    /// The renderer.
    /// </returns>
    protected virtual DateRenderer CreateRenderer()
    {
        return new DateRenderer();
    }

}

We will set the renderer.FieldValue by calculating the correct DateTime by using the combined value(DateTime and TimeZoneId) in args.FieldValue.

In order to make the pipeline work we need to do a patch config file.

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <renderField>
        <processor type="Sandbox.CustomFields.Pipelines.RenderField.GetDateTimeWithTimeZoneValue, Sandbox.CustomFields"
                    patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetDateFieldValue, Sitecore.Kernel']">
        </processor>
      </renderField>
    </pipelines>
  </sitecore>
</configuration>

That’s all for now folks 🙂

Happy New Year!

Make your own custom Standard Values tokens in Sitecore

StandardValuesHeader

I just love Sitecores’s Standard Values. They allows you to set default values on an item when it’s created. There are some really nice posts explaining the basics of Standard Values. Please read John West’s post – ALL ABOUT STANDARD VALUES IN THE SITECORE ASP.NET CMS and Jens Mikkelsen’s post – Standard Values in Sitecore

A very cool thing in Standard Values is the tokens. You just enter a token in the field on the standard values and then it will be replaced with an other value when the item is created. The tokens you can use are:

  • $name: The name for the new item entered by the user
  • $id: The ID of the new item
  • $parentid: The ID of the parent of the new item
  • $parentname: The name of the parent of the new item
  • $date: The system date in yyyyMMdd format
  • $time: The system time in HHmmss format
  • $now: The system date and time in yyyyMMddTHHmmss format

But would it not be nice if you could do your own custom tokens? I found some very nice posts about that subject:
John WestADD CUSTOM STANDARD VALUES TOKENS IN THE SITECORE ASP.NET CMS
Mike ReynoldsContent Manage Custom Standard Values Tokens in the Sitecore Client

While working with the PushBroker Manager I needed to get a value from an ancestor item and put it in a descendant item when it was created. In this case I wanted to know what market campaign(DMS) was selected in the campaign item
PushBrokerManagerCampaignArrow
and put it in the “Query Parameters” field in the message item.
PushBrokerManagerMessageArrow

Shorting it down – I need to get values from an ancestor item and put them in a descendant item when it is created. I want to do it by using custom tokens.

The custom tokens will be defined in Sitecore so I did a StandardValue Variable item:
StandardValueFolder

And here is the template:
StandardValueTemplate

  • StandardValueVariableToken: The token
  • StandardValueVariableAncestorOrParentFieldName: The field from the parent/ancestor item
  • StandardValueVariableAncestorOrParentFieldId: The field from the parent/ancestor item
  • StandardValueVariableAncestorOrParentItemTemplateName: The template from the parent/ancestor item
  • StandardValueVariableAncestorOrParentItemTemplateId: The template from the parent/ancestor item
  • StandardValueVariableSourceItemFieldName: The field from the source item in the parent/ancestor item selected from a Droplink, Multilist or a Treelist
  • StandardValueVariableSourceItemFieldId: The field from the source item in the parent/ancestor item selected from a Droplink, Multilist or a Treelist
  • StandardValueVariableSourceItemTemplateName: The template from the source item in the parent/ancestor item selected from a Droplink, Multilist or a Treelist
  • StandardValueVariableSourceItemTemplateId: The template from the source item in the parent/ancestor item selected from a Droplink, Multilist or a Treelist

Next thing to do will be to put the custom tokens in a repository class.

public class StandardValueVariableRepository
{
    public static StandardValueVariable Get(Item item)
    {
        if (item == null || !item.IsDerived(Constants.Templates.StandardValueVariable))
            return null;

        return StandardValueVariableFactory.Create(item);
    }


    public static StandardValueVariable Get(ID itemId, Language language)
    {
        return Get(SitecoreItemRepository.Get(itemId, language));

    }

    public static IEnumerable<StandardValueVariable> Collection(Language language)
    {
        Item standardValueVariablesFolderItem =
            SitecoreItemRepository.Get(Constants.Items.StandardValueVariablesFolder, language);

        return standardValueVariablesFolderItem.GetChildren().Select(Get);
    }

}

Here is is the factory class.

internal class StandardValueVariableFactory
{
    internal static StandardValueVariable Create(Item item)
    {
        return new StandardValueVariable()
        {
            Id = item.ID.ToString(),
            Token = item.GetString(Constants.Fields.StandardValueVariable.StandardValueVariableToken),
            AncestorOrParentTemplateName = item.GetString(Constants.Fields.StandardValueVariable.StandardValueVariableAncestorOrParentItemTemplateName),
            AncestorOrParentTemplateId = item.GetString(Constants.Fields.StandardValueVariable.StandardValueVariableAncestorOrParentItemTemplateId),
            AncestorOrParentFieldName = item.GetString(Constants.Fields.StandardValueVariable.StandardValueVariableAncestorOrParentFieldName),
            AncestorOrParentFieldId = item.GetString(Constants.Fields.StandardValueVariable.StandardValueVariableAncestorOrParentFieldId),
            SourceTemplateName = item.GetString(Constants.Fields.StandardValueVariable.StandardValueVariableSourceItemTemplateName),
            SourceTemplateId = item.GetString(Constants.Fields.StandardValueVariable.StandardValueVariableSourceItemTemplateId),
            SourceFieldId = item.GetString(Constants.Fields.StandardValueVariable.StandardValueVariableSourceItemFieldId),
            SourceFieldName = item.GetString(Constants.Fields.StandardValueVariable.StandardValueVariableSourceItemFieldName)
        };
    }
}

And the model:

public class StandardValueVariable
{
    public string Id { get; set; }
        
    public string Token { get; set; }

    public string AncestorOrParentTemplateName { get; set; }

    public string AncestorOrParentTemplateId { get; set; }

    public string AncestorOrParentFieldName { get; set; }

    public string AncestorOrParentFieldId { get; set; }

    public string SourceTemplateName { get; set; }

    public string SourceTemplateId { get; set; }

    public string SourceFieldName { get; set; }

    public string SourceFieldId { get; set; }
}

The actual “replacing” happens in the class Sitecore.Data.MasterVariablesReplacer which means I need to replace it with my own class but I don’t want to interfere with the standard tokens from Sitecore.

public class CustomTokensMasterVariablesReplacer : MasterVariablesReplacer
{

    private List<StandardValueVariable> _standardValueVariables;

    private IEnumerable<StandardValueVariable> StandardValueVariables
    {
        get
        {
            if (_standardValueVariables != null)
                return _standardValueVariables;

            _standardValueVariables = StandardValueVariableRepository.Collection(Sitecore.Context.Language).ToList();

            return _standardValueVariables;
        }
    }

    public override string Replace(string text, Item targetItem)
    {

        return GetReplacedCustomTokenValue(text, targetItem);

    }


    private string GetReplacedCustomTokenValue(string tokenText, Item targetItem)
    {
        string replacedText = base.Replace(tokenText, targetItem);

        if (replacedText != tokenText)
            return replacedText;

        foreach (StandardValueVariable standardValueVariable in StandardValueVariables)
        {
            if (standardValueVariable == null)
                continue;

            if (string.IsNullOrWhiteSpace(standardValueVariable.Token))
                continue;

            if (!tokenText.Contains(standardValueVariable.Token))
                continue;

            if (string.IsNullOrWhiteSpace(standardValueVariable.AncestorOrParentTemplateId) && string.IsNullOrWhiteSpace(standardValueVariable.AncestorOrParentTemplateName))
                continue;

            if (string.IsNullOrWhiteSpace(standardValueVariable.AncestorOrParentFieldName) && string.IsNullOrWhiteSpace(standardValueVariable.AncestorOrParentFieldId))
                continue;

            if (string.IsNullOrWhiteSpace(standardValueVariable.AncestorOrParentTemplateId))
                continue;

            Item ancestorItem = targetItem.GetAncestors()
                .FirstOrDefault(item => item.IsDerived(new ID(standardValueVariable.AncestorOrParentTemplateId)));

            if (ancestorItem == null)
                continue;

            if (string.IsNullOrWhiteSpace(standardValueVariable.SourceTemplateId) &&
                string.IsNullOrWhiteSpace(standardValueVariable.SourceFieldId))
            {
                return ancestorItem.GetString(new ID(standardValueVariable.AncestorOrParentFieldId));
            }

            //This is for treelist, droplink or multilist
            Item sourceItem = ancestorItem.GetItemValues(new ID(standardValueVariable.AncestorOrParentFieldId)).FirstOrDefault();

            if (sourceItem == null)
                continue;

            //Checking if it's a campaign(DMS). I was going crazy trying to get the value from an iframe field. 
            //Instead I just took the the campaign id... 
            if (sourceItem.IsDerived(Constants.Templates.AnalyticsCampaign) && standardValueVariable.SourceFieldId == Constants.Fields.AnalyticsCampaign.CampaignLink.ToString())
                return string.Format("sc_camp={0}", sourceItem.ID.ToShortID());

            if (!string.IsNullOrWhiteSpace(standardValueVariable.SourceFieldId))
                return sourceItem.GetString(new ID(standardValueVariable.SourceFieldId));
        }


        return string.Empty;

    }

}

I inherit the Sitecore.Data.MasterVariablesReplacer and override method Replace(This is where the replacing of tokens is done).
In GetReplacedCustomTokenValue I use the base.Replace from the inherited class to get the “replaced value”. If the “replaced value” is the same as the token text it means that “the replacing” was not successful. In other words – here is a custom token.

Next thing will be to iterate through the custom tokens(StandardValue Variable items), find the correct one and from it’s settings together with targetItem return the “replaced value”. That’s it 🙂

Oh I almost forgot, here is the patch config file.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <settings>
      <setting name="MasterVariablesReplacer">
        <patch:attribute name="value">Sitecore.Sharedsource.CustomTokensMasterVariablesReplacer,Sitecore.Sharedsource</patch:attribute>
      </setting>
    </settings>
  </sitecore>
</configuration>

That’s all for now folks 🙂