Get last visited pages from a Sitecore DMS (OMS) Profile

Quite a few Sitecore developers have wondered how to get any useful information out of the Sitecore DMS (Digital Marketing System), formerly known as Sitecore OMS (Online Marketing Suite). I’ll call it the Sitecore Analytics, since the namespace is Sitecore.Analytics. The API for Sitecore DMS is – how should I put it – not of the same high quality standard as Sitecore itself. However, this has never stopped anyone, and since DMS is packed with visitor data, I’ll show how to utilize this.

This is a simple example on how to use the DMS. This function extracts the full history of which pages the current visitor have clicked:

IEnumerable allVisitedPages = Sitecore.Analytics.Tracker.Visitor.DataContext.Pages.Reverse();

This is the full history. I reverse the list to get the last visited pages at the top. If the user has visited a page twice, it will show up twice. And any page is in the list, also the web site frontpage.

So the list needs to be filtered somehow. This example creates a fictional list of the 10 last product pages the user has visited:

protected IEnumerable GetProducts(int count)
{
  IEnumerable allVisitedPages = Sitecore.Analytics.Tracker.Visitor.DataContext.Pages.Reverse();

  List lastVisitedProducts = new List();
  int ac = 0;
  IEnumerable products = allVisitedPages.Select(GetItem).Where(item => item != null && item.TemplateName == "product" );
  foreach (Item product in products)
  {
    if (!lastVisitedProducts.Exists(p => p.ID == product.ID))
    {
      lastVisitedProducts.Add(product);
      ac++;
    }
    if (ac == count)
      return lastVisitedProducts;
  }
  return lastVisitedProducts;
}

The real gem here is that the user profile is stored in a cookie, so the data is persisted. The next time the user opens the website, the data is still there and you can display the last visited products list.

Posted in .net, c#, Sitecore 6 | Tagged , , , , , | Leave a comment

Using the Sitecore Audit Log

I guess your code contains just as many log messages as my code does:

try
{
  // lots of stuff going on here...
}
catch (Exception ex)
{
  Sitecore.Diagnostics.Log.Error("Some great error message: " + ex.Message, this);
  // more exception handling here...
}

And the exception is written to the log:

6636 13:35:42 ERROR Some great error message: blahblahblah

In previous versions, the Audit log was a seperate log. But now it’s all handled by Apache Log4Net. So writing to the audit log is similar to writing to the log. The following line:

Sitecore.Diagnostics.Log.Audit("DamForSitecore:RemoveAssetFromCache. AssetID=" + assetID, this);

Produces the following log line:

3360 13:56:27 INFO  AUDIT (sitecore\admin): DamForSitecore:RemoveAssetFromCache. AssetID=4848

You can use the AuditFormatter to format items and fields so they appear with the same pattern for all items and fields in the log.
The following line:

Sitecore.Diagnostics.Log.Audit(this, "Do stuff {0}", new[] {AuditFormatter.FormatItem(Sitecore.Context.Item)});

Produces the following log line:

3360 14:06:18 INFO  AUDIT (sitecore\admin): Do stuff: master:/sitecore/content/Home, language: da, version: 2, id: {110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}

Other articles regarding the subject:

 

Posted in c#, Sitecore 6 | Tagged , , , , | Leave a comment

Get latest news using Sitecore AdvancedDatabaseCrawler Lucene index

This is the first of 2 follow-up posts on how to use the Advanced Database Crawler open source module for Sitecore. From Sitecore 6.5, Sitecore is deprecating the old Lucene index. This means that we have to redo our index work. Fortunately, Alex Shyba has created an open source module that makes indexing and retrieving easy.

This is article 2 of 3 articles:

Part 1 – Configuring the index: Using the Sitecore open source AdvancedDatabaseCrawler Lucene indexer
Part 2 – Simple search: Get latest news using Sitecore AdvancedDatabaseCrawler Lucene index
Part 3 – Multivalue search: Get items based on Metadata using Sitecore AdvancedDatabaseCrawler Lucene index

If you have not read the first article, you should read it, as it explains how to compile the module and how to set up an index that indexes your WEB database.

This example describes one of the most trivial tasks: How to retrieve a sorted list of items that is scattered over the entire website. For example, when you would like to list the latest news articles, it is fast and easy to use an index. This website (Personalestyrelsen, a Danish web site) uses this exact method to list the latest news on the front page.

This is the scenario: I have a news archive where all my news articles are stored:

An example of a news archive

An example of a news archive

Each news template contains a date field and a title field:

Sample News Article

Sample News Article

I would like to use the date field as my sort field, and then display the 3 latest news with the date and title.

My index is already set up so it indexes every field in my solution (read about the configuration of the index here) , so I only have to set up a search. And thanks to Alex Shybas AdvancedDatabaseCrawler, this is easy:

using System;
using System.Collections.Generic;
using System.Linq;
using Sitecore.Data.Items;
using Sitecore.SharedSource.Searcher;
using Sitecore.SharedSource.Searcher.Parameters;

namespace PT.Prototype.Search
{
  public partial class _default : System.Web.UI.Page
  {
    protected void Page_Load(object sender, EventArgs e)
    {
      DataBind();
    }

    public IEnumerable<Item> GetLatestNews(int numberOfNews)
    {
      SearchParam searchParam = new SearchParam();
      // The name of the database to search in
      searchParam.Database = "web";
      // The language to return
      searchParam.Language = "en";
      // The NewsItem template ID
      searchParam.TemplateIds = "{2983E106-CE47-40B5-8704-D299CA9BFF8F}";
      // The QueryRunner uses the name of the index as constructor.
      // I use the same name as the database name.
      QueryRunner runner = new QueryRunner("web");
      // Call the runner to get the items.
      // Parameters are:
      // - ISearchParam: The search to perform
      // - Sitecore.Search.QueryOccurance: The AND/OR/NOT clause for the search
      // - ShowAllVersions: Get all versions of an item
      // - SortField: The name of the field used as my sort
      // - Reverse: Return items in reverse order (newset first)
      // - Start: Which items to return from the search result
      // - End: Which items to return from the search result
      IEnumerable<SkinnyItem> items = runner.GetItems(searchParam, Sitecore.Search.QueryOccurance.Must, false, "date", true, 0, numberOfNews);
      return items.Select(item => Sitecore.Context.Database.GetItem(item.ItemID));
    }
  }
}

All I need to do now is to create an asp:Repeater and databind it to the function:

<%@ Register TagPrefix="sc" Namespace="Sitecore.Web.UI.WebControls" Assembly="Sitecore.Kernel" %>

<asp:Repeater ID="repNews" DataSource="<%# GetLatestNews(3) %>" runat="server">
  <HeaderTemplate>
    <ul>
  </HeaderTemplate>
  <FooterTemplate>
    </ul>
  </FooterTemplate>
  <ItemTemplate>
    <li>
      <sc:FieldRenderer ID="FieldRenderer1" FieldName="Date" runat="server" Item="<%# Container.DataItem as Sitecore.Data.Items.Item %>" />
      <a href="<%# Sitecore.Links.LinkManager.GetItemUrl(Container.DataItem as Sitecore.Data.Items.Item) %>">
        <sc:FieldRenderer ID="FieldRenderer2" FieldName="Title" runat="server" Item="<%# Container.DataItem as Sitecore.Data.Items.Item %>" />
      </a>
    </li>
  </ItemTemplate>
</asp:Repeater>
Posted in c#, Sitecore 6 | Tagged , , , , | 3 Comments

Using the Sitecore open source AdvancedDatabaseCrawler Lucene indexer

From Sitecore 6.5, Sitecore is deprecating the old Lucene search method. This simply means that you can no longer use the current, built in Lucene search, but has to use a new built in Lucene search.

This is article 1 of 3 articles:

Part 1 – Configuring the index: Using the Sitecore open source AdvancedDatabaseCrawler Lucene indexer
Part 2 – Simple search: Get latest news using Sitecore AdvancedDatabaseCrawler Lucene index
Part 3 – Multivalue search: Get items based on Metadata using Sitecore AdvancedDatabaseCrawler Lucene index

There are many good reasons to use the new way of indexing, but it also requires you to redo some of your previous work. The old way of indexing was easy to setup and easy to use. The new way is more complex because it lets you do more advanced stuff.

That’s why Alex Shyba wrote an open source module that makes the searching and indexing easier: The Advanced Database Crawler. You can find the module here:

http://trac.sitecore.net/AdvancedDatabaseCrawler/browser/Branches/v2/

I have always felt that open source modules are the best way of implementing other developers unsolvable bugs. But I have tried this module, and it works.

I will now show you how to set up the module, and in 2 later posts, I will show you how to perform the 2 basic tasks that you do with an index: How to get the latest news, and how to get all items with a certain set of metadata categories.

The new indexing applies to newer versions of Sitecore. Sitecore 6.2 revision 5 should do it, but from 6.4 and forward you are certain that it will work.

First you need to compile the AdvancedDatabaseCrawler. When compiled you get some dlls:

  • Sitecore.SharedSource.SearchCrawler.dll
  • Sitecore.SharedSource.SearchCrawler.DynamicFields.dll
  • Sitecore.SharedSource.SearchDemo.dll
  • Sitecore.SharedSource.Searcher.dll

The Sitecore.SharedSource.SearchDemo.dll is not needed in production, but it implements some test pages (found in /sitecore modules/Web/searchdemo) that you can use to test your index. Copy the DLL’s to your Sitecore /bin/ folder and you are ready to go. Copy the /sitecore modules/Web/searchdemo items to test your index with Alex’s samples.

Now you need to set up an index. I will show you how to set up an index that crawls the WEB database, as I’m going to use the index for frontend indexing.

Create an ???.config file and put it in the /App_Config/Include folder. Add the following

<configuration xmlns:x="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <databases>
      <database id="web" singleInstance="true" type="Sitecore.Data.Database, Sitecore.Kernel">
        <Engines.HistoryEngine.Storage>
          <obj type="Sitecore.Data.$(database).$(database)HistoryStorage, Sitecore.Kernel">
            <param connectionStringName="$(id)" />
            <EntryLifeTime>30.00:00:00</EntryLifeTime>
          </obj>
        </Engines.HistoryEngine.Storage>
        <Engines.HistoryEngine.SaveDotNetCallStack>false</Engines.HistoryEngine.SaveDotNetCallStack>
      </database>
    </databases>
    <search>
      <configuration>
        <indexes>
          <index id="web" type="Sitecore.Search.Index, Sitecore.Kernel">
            <param desc="name">$(id)</param>
            <param desc="folder">web</param>
            <Analyzer ref="search/analyzer" />
            <locations hint="list:AddCrawler">
              <master type="Sitecore.SharedSource.SearchCrawler.Crawlers.AdvancedDatabaseCrawler,Sitecore.SharedSource.SearchCrawler">
                <Database>web</Database>
                <Root>/sitecore/content</Root>
                <IndexAllFields>true</IndexAllFields>
                <fieldCrawlers hint="raw:AddFieldCrawlers">
                  <fieldCrawler type="Sitecore.SharedSource.SearchCrawler.FieldCrawlers.LookupFieldCrawler,Sitecore.SharedSource.SearchCrawler" fieldType="Droplink" />
                  <fieldCrawler type="Sitecore.SharedSource.SearchCrawler.FieldCrawlers.DateFieldCrawler,Sitecore.SharedSource.SearchCrawler" fieldType="Datetime" />
                  <fieldCrawler type="Sitecore.SharedSource.SearchCrawler.FieldCrawlers.DateFieldCrawler,Sitecore.SharedSource.SearchCrawler" fieldType="Date" />
                  <fieldCrawler type="Sitecore.SharedSource.SearchCrawler.FieldCrawlers.NumberFieldCrawler,Sitecore.SharedSource.SearchCrawler" fieldType="Number" />
                </fieldCrawlers>
                <!-- If a field type is not defined, defaults of storageType="NO", indexType="UN_TOKENIZED" vectorType="NO" boost="1f" are applied-->
                <fieldTypes hint="raw:AddFieldTypes">
                  <!-- Text fields need to be tokenized -->
                  <fieldType name="single-line text" storageType="NO" indexType="TOKENIZED" vectorType="NO" boost="1f" />
                  <fieldType name="multi-line text" storageType="NO" indexType="TOKENIZED" vectorType="NO" boost="1f" />
                  <fieldType name="word document" storageType="NO" indexType="TOKENIZED" vectorType="NO" boost="1f" />
                  <fieldType name="html" storageType="NO" indexType="TOKENIZED" vectorType="NO" boost="1f" />
                  <fieldType name="rich text" storageType="NO" indexType="TOKENIZED" vectorType="NO" boost="1f" />
                  <fieldType name="memo" storageType="NO" indexType="TOKENIZED" vectorType="NO" boost="1f" />
                  <fieldType name="text" storageType="NO" indexType="TOKENIZED" vectorType="NO" boost="1f" />
                  <!-- Multilist based fields need to be tokenized to support search of multiple values -->
                  <fieldType name="multilist" storageType="NO" indexType="TOKENIZED" vectorType="NO" boost="1f" />
                  <fieldType name="treelist" storageType="NO" indexType="TOKENIZED" vectorType="NO" boost="1f" />
                  <fieldType name="treelistex" storageType="NO" indexType="TOKENIZED" vectorType="NO" boost="1f" />
                  <fieldType name="checklist" storageType="NO" indexType="TOKENIZED" vectorType="NO" boost="1f" />
                  <!-- Legacy tree list field from ver. 5.3 -->
                  <fieldType name="tree list" storageType="NO" indexType="TOKENIZED" vectorType="NO" boost="1f" />
                </fieldTypes>
              </master>
            </locations>
          </index>
        </indexes>
      </configuration>
    </search>
  </sitecore>
</configuration>

A short explanation:

The /sitecore/databases/database items creates a HistoryEngine on the WEB database. This is needed for indexing at all. No HistoryEngine, no index.

The /sitecore/search/configuration/indexes/index is the actual index. This is taken straight from Alex Shyba’s own examples and defines an index called “web” that contains everything (all items, all fields) from the WEB database.

Read more about setting up indexes here.

This it it. You cannot use Sitecore to rebuild the index anymore. You need to either use the /sitecore modules/Web/searchdemo/RebuildDatabaseCrawlers.aspx or write your own simple code:

JobOptions options = new JobOptions("RebuildSearchIndex", "index", Sitecore.Client.Site.Name, "web", "Rebuild");
options.AfterLife = TimeSpan.FromMinutes(1.0);
Job job = JobManager.Start(options);

In the following posts I will demonstrate how to get the latest news, and how to get all items with a certain set of metadata categories.

More stuff to read:

Posted in c#, Sitecore 6 | Tagged , , , | 18 Comments

Adding YouTube Videos to the Sitecore Rich Text field

NOTE: This post has been updated 04-12-2012 with new iOS 5.x compatible object tags.

This post is not only about adding YouTube videos to Sitecore content. It’s actually about replacing content from the Rich Text editor on runtime using the RenderField pipeline.

Have you ever tried to add <object> tags or other content to the Sitecore Rich Text Editor?

Pasting Object HTML directly to the Rich Text Editor

Pasting Object HTML directly to the Rich Text Editor

And have you then experienced that after pasting the code, the Rich Text Editor will produce JavaScript errors:

Java Script Errors

Java Script Errors

Or have you experienced that the Rich Text Editor will remove the <object> tag, leaving the embed tag, making your HTML invalid?

Or is it simply too tricky for the end user to open then HTML and paste in HTML code directly?

There is several solutions to these problems. One of them is to allow the user to write one thing in the editor and display another on the web site. For example, if the user wants to link to a YouTube video, why not just let the user paste a link to the video in the editor. But when the page is rendered, replace the link with the embedded YouTube video player?

I know that there is JavaScript functions to do this, but I would like to create a version that will run without Javascript.

To do this, you need to create a new RenderField processor.

When the replacer is in place you can write an URL to the YouTube video:

Link to YouTube video

Link to YouTube video

And when displayed on the website, the link is replaced with an embedded player:

Displaying embedded YouTube video

Displaying embedded YouTube video

The processor is added to the RenderField pipeline in web.config:

<renderField>
<processor type="Sitecore.Pipelines.RenderField.SetParameters, Sitecore.Kernel" />
...
...
<!-- our processor -->
<processor type="MyCode.Pipelines.RenderField.RenderYouTubeVideo, MyDll"/>
<!-- our processor -->
<processor type="Sitecore.Pipelines.RenderField.AddBeforeAndAfterValues, Sitecore.Kernel" />
<processor type="Sitecore.Pipelines.RenderField.RenderWebEditing, Sitecore.Kernel" />
</renderField>

The YouTube replacer uses the HtmlAgilityPack that is shipped with Sitecore. This pack allows me to traverse the HTML as it were XML.

using System;
using System.Text;
using Sitecore.Text;

namespace Oti.DigiZuite.Renderer.Pipelines.RenderField
{
  public class RenderYouTubeVideo
  {
    public void Process(RenderFieldArgs args)
    {
      Assert.ArgumentNotNull(args, "args");
      Assert.ArgumentNotNull(args.FieldTypeKey, "args.FieldTypeKey");

      // Do not modify output if the field is not a rich text field,
      // or if the page is in page editor mode
      if (args.FieldTypeKey != "rich text" ||
          String.IsNullOrEmpty(args.FieldValue) ||
          Context.PageMode.IsPageEditorEditing)
      {
        return;
      }

      // Load the HTML into the HtmlAgilityPack
      HtmlDocument doc = new HtmlDocument();
      doc.OptionWriteEmptyNodes = true;
      doc.LoadHtml(args.Result.FirstPart);

      // Search for all links
      HtmlNodeCollection aTag = doc.DocumentNode.SelectNodes("//a");
      if (aTag == null || aTag.Count == 0)
        return;

      foreach (HtmlNode node in aTag)
      {
        // Look for links to YouTube
        if (node.Attributes["href"].Value.StartsWith("http://www.youtube.com"))
        {
          // Get the video ID and create an object tag
          UrlString urlString = new UrlString(node.Attributes["href"].Value);
          string videoIdentifier = urlString.Parameters["v"];
          string youtubePlayer = RenderVideo(videoIdentifier, 640, 385);
          node.ParentNode.InnerHtml = node.ParentNode.InnerHtml.Replace(node.OuterHtml, youtubePlayer);
        }
      }
      // Replace the Rich Text content with the modified content
      args.Result.FirstPart = doc.DocumentNode.OuterHtml;
    }

    private string RenderVideo(string videoIdentifier, int width, int height)
    {
      StringBuilder sb = new StringBuilder();
      sb.Append(@"<div class=""Video"">");
      sb.AppendFormat(@"<object width=""{0}"" height=""{1}"">", width, height);
      sb.AppendFormat(@"<param name=""movie"" value=""http://www.youtube.com/v/{0}&color1=0xb1b1b1&color2=0xcfcfcf&hl=en_US&feature=player_embedded&fs=1""></param>", videoIdentifier);
      sb.Append(@"<param name=""allowFullScreen"" value=""true""></param>");
      sb.Append(@"<param name=""allowScriptAccess"" value=""always""></param>");
      sb.AppendFormat(@"<embed src=""http://www.youtube.com/v/{0}&color1=0xb1b1b1&color2=0xcfcfcf&hl=en_US&feature=player_embedded&fs=1"" type=""application/x-shockwave-flash"" allowfullscreen=""true"" allowScriptAccess=""always"" width=""{1}"" height=""{2}""></embed>", videoIdentifier, width, height);
      sb.Append(@"</object>");
      sb.Append(@"</div>");
      return sb.ToString();
    }
  }
}

This is just one example of how to allow users to add one thing and display another. In the upcomming version of DAM for Sitecore, users will, when adding a video to their editor, have an thumbnail image inside the Rich Text editor, but still have a video player on the website. This eliminates the JavaScript errors that pops up when adding <object> tags to the editor.

Posted in c#, Sitecore 6 | Tagged , , , , , | Leave a comment

Missing buttons in Sitecore 6 shell

Have you experienced that the buttons in Sitecore 6 is missing?

Missing buttons in the Sitecore shell

Missing buttons in the Sitecore shell

This can happen if you are running IE9, and you are switching between Sitecore 5 and Sitecore 6 websites on the same domain. The solution is easy. Simply clear your browser cache for the domain, refresh, and you are back to normal.

Clear Browser Cache For This Domain

Press F12 to open the developer toolbar, and clear the browser cache

Posted in Sitecore 5, Sitecore 6 | Tagged , , , | Leave a comment

Sitecore Rocks Really Rocks

In the recent month I have been using the new Sitecore Rocks plugin for Visual Studio. And I must admit that I am very impressed.

The installation is very easy. Open the Extension Manager in Visual Studio and search for Sitecore Rocks.

Hooking into your Sitecore project can be a but more tricky, because in order to use all the cool features of Sitecore Rocks, you need to use the Hard Rock Web Service, which needs to be installed in your website. If you have physical access to the site, it’s no problem. If not, you need to copy the files manually.

Once you are up and running, you are able to browse Sitecore from the Sitecore Explorer:

Sitecore Rocks Sitecore Explorer

Sitecore Rocks Sitecore Explorer

Right click the website and select “Manage” to get a list of all Sites, Pipelines, Temp files, Caches, Lucene Indexes (including ability to browse and search the index = very cool!), statistics and validation:

Sitecore Rocks - View and search in indexes

Sitecore Rocks - View and search in indexes

Or how about the real-time Log Viewer: Right click the web site and select the log viewer. It will show you the latest log file, updating the view each 10 seconds:

Sitecore Rocks - Log Viewer

Sitecore Rocks - Log Viewer

But one of the features I use the most is the ability to copy xml from one website and paste it into another. Sitecore Rocks can display more than one site at a time. So instead of the tedious task of creating packages of content when moving from development to test servers, I simply open the development server, copy the items as XML, open the test server, and paste the items to the appropriate place. Fast and effective:

Sitecore Rocks - Get Item XML

Sitecore Rocks - Get Item XML

Here are some more Rocks! resources:

You can also follo the Sitecore Rocks Development on Twitter:

Posted in Sitecore 6 | Tagged , | 1 Comment

Creating a tree like left menu in Sitecore using UserControls and C#

This post is a follow-up on the article on how to create a tree like left menu in Sitecore using XSLT. This time I will not use XSLT to create my left menu, but instead I will use C# and UserControls.

With Sitecore 5 and Sitecore 6, XSLT’s are becomming deprecated, as it is getting easier and easier to create the same using UserControls.

There are several ways of creating a left menu in C#. In this article I will show how you can dump your left menu into an XML string that can be passed to an asp:XmlDataSource. From the XmlDataSource you can hook the menu into the asp:Menu, the asp:TreeView and even the asp:SiteMap controls. However, none of these controls give you full control of the HTML generated (unless you use these control adaptors), so I will show how to use nested repeaters to create the menu hierachy.

First I must create an XmlLeftMenu class that can dump the current left menu structure into an XML string. This code is an pretty accurate dump of how the XSLT did it:

public class XmlLeftMenu
{
  private Item _root;
  private StringBuilder _sb = new StringBuilder();

  public XmlLeftMenu(Item root)
  {
    _root = root;
  }

  public string LeftMenuXml
  {
    get
    {
      _sb.Append("<menu>");
      RenderMenu(_root);
      _sb.Append("</menu>");
      return _sb.ToString();
    }
  }

  private void RenderMenu(Item root)
  {
    ChildList children = root.GetChildren();
    foreach (Item child in children)
    {
      _sb.AppendFormat(@"<item title=""{0}"" url=""{1}"" haschildren=""{2}"" open=""{3}"" selected=""{4}"">",
      child.DisplayName,
      LinkManager.GetItemUrl(child),
      child.HasChildren,
      child.Axes.IsAncestorOf(Sitecore.Context.Item),
      child.ID == Sitecore.Context.Item.ID);
      if (child.HasChildren && child.Axes.IsAncestorOf(Sitecore.Context.Item))
        RenderMenu(child);
      _sb.Append("</item>");
    }
  }
}

(In this example I simply use the item’s DisplayName as my title, but you should modify the code to select the field containing the page title).

The XML contains the page title and page URL. Haschildren is true if the item has children. Open is true if the current item is a child of the menu item being rendered (indicating that the menu is opened and visible). Selected is true if the menu item is the current item. These last 3 attriubutes can be used to style the menu.

The XmlLeftMenu class can be hooked into a XmlDataSource like this: This is the .aspx page code:

<asp:XmlDataSource ID="xmlMenu" EnableCaching="false" runat="server" XPath="menu/item">
</asp:XmlDataSource>

Please note the EnableCaching=false. This is needed as the control caches the XML by default. If caching is enabled, your menu will not change every time you change page. Here is the codebehind:

protected void Page_Load(object sender, EventArgs e)
{
  XmlLeftMenu leftMenu = new XmlLeftMenu(Sitecore.Context.Database.GetItem(Sitecore.Context.Site.StartPath ));
  xmlMenu.Data = leftMenu.LeftMenuXml;
  DataBind();
}

Now your page has access to the left menu. As said before, you can use the asp:Menu to display the menu directly, but if you would like to keep control of your HTML, you can use a series of nested repeaters like this:

<asp:Repeater ID="repLeftmenu" DataSourceID="xmlMenu" runat="server" EnableViewState="false">
  <HeaderTemplate>
    <ul class="leftMenu">
  </HeaderTemplate>
  <ItemTemplate>
    <li>
      <%-- Rendering level 1 --%>
      <a href="<%# XPath("@url") %>">
        <%# XPath("@title") %>
      </a>
      <%-- Nested repeater rendering level 2 --%>
      <asp:Repeater runat="server" ID="level2" EnableViewState="false" DataSource='<%# XPathSelect("item") %>' Visible='<%# Convert.ToBoolean( XPath("@open") ) %>'>
        <HeaderTemplate>
          <ul>
        </HeaderTemplate>
        <ItemTemplate>
          <li>
            <a href="<%# XPath("@url") %>">
              <%# XPath("@title") %>
            </a>
            <%-- Nested repeater rendering level 3 --%>
            <asp:Repeater runat="server" ID="level3" EnableViewState="false" DataSource='<%# XPathSelect("item") %>' Visible='<%# Convert.ToBoolean( XPath("@open") ) %>'>
              <HeaderTemplate>
                <ul>
              </HeaderTemplate>
              <ItemTemplate>
                <li>
                  <a href="<%# XPath("@url") %>">
                    <%# XPath("@title") %>
                  </a>
                  <%-- Nested repeater rendering level 4
                       You can continue for as many leves as you like...
                  --%>
                  <asp:Repeater runat="server" ID="level4" EnableViewState="false" DataSource='<%# XPathSelect("item") %>' Visible='<%# Convert.ToBoolean( XPath("@open") ) %>'>
                    <HeaderTemplate>
                      <ul>
                    </HeaderTemplate>
                    <ItemTemplate>
                      <li>
                        <a href="<%# XPath("@url") %>">
                          <%# XPath("@title") %>
                        </a>
                      </li>
                    </ItemTemplate>
                    <FooterTemplate>
                      </ul>
                    </FooterTemplate>
                  </asp:Repeater>
                </li>
              </ItemTemplate>
              <FooterTemplate>
                </ul>
              </FooterTemplate>
            </asp:Repeater>
          </li>
        </ItemTemplate>
        <FooterTemplate>
          </ul>
        </FooterTemplate>
      </asp:Repeater>
    </li>
  </ItemTemplate>
  <FooterTemplate>
    </ul>
  </FooterTemplate>
</asp:Repeater>

You can extend the presentation by using the “haschildren”, “open” and “selected” attributes from the XML to create style sheet attributes. Extend each LI tag with the following:

<li class="children<%# XPath(@haschildren") %> open<%# XPath("@open") %> selected<%# XPath("@selected") %>">

This will give you class tags like this childrenTrue and childrenFalse, openTrue and openFalse, selectedTrue and selectedFalse.

Posted in c#, Sitecore 6 | Tagged , , , , , , , , , , | 2 Comments

Run Sitecore scheduled task at the same time every day

Yesterday I helped a user on stackoverflow with this question:

Is it possible to run a Sitecore scheduled task at the exact time every day?

The quick answer is no. But it’s possble to get it close to the same time every day.

Scheduled tasks are run in sequence by the Sitecore scheduler. The scheduler checks within a certain interval (defined in the web.config in the /scheduling/frequency and /scheduling/agent settings) for tasks to be run. If a task is over due, it is run, and the time where the task finished is recorded as the next checkpoint.

It is not possible to define a certain time a day the task needs to run. It’s only possible to determine which days a task needs to run, and the interval.

So how do you make a task run once a day, at a certain time?

Well, instead of configuring the task to run every 23:59:59, I make my task run every minute, but only executing the functionality once a day. Inside the task I check the time, and only if the time is inside a certain interval I execute the functionality.

Here is how to do it:

1) Create yout task and make it run every 1 minute or 5 minutes (or at least twice as often as your interval).

2) Define an interval where the task is allowed to run. For example at night between 01:00 am and 02:00 am.

3) Build your task in the following manner:

public class TaskRunningOnceAday
{
  public void Execute(Item[] itemArray, CommandItem commandItem, ScheduleItem scheduleItem)
  {
    if (!IsDue(scheduleItem))
      return;

    // EXECUTE MY FUNCTIONALITY
  }

  /// <summary>
  /// Determines whether the specified schedule item is due to run.
  /// </summary>
  /// <remarks>
  /// The scheduled item will only run between defined hours (usually at night) to ensure that the
  /// task is run once a day
  /// Make sure you configure the task to run at least double so often than the time span.
  /// </remarks>
  private bool IsDue(ScheduleItem scheduleItem)
  {
    DateTime time;
    DateTime time2;

    DateTime.TryParse("01:00:00", out time);
    DateTime.TryParse("02:00:00", out time2);

    return (CheckTime(DateTime.Now, time, time2) && !CheckTime(scheduleItem.LastRun, time, time2));
  }

  private bool CheckTime(DateTime time, DateTime after, DateTime before)
  {
    return ((time >= after) && (time <= before));
  }

}

The scheduleItem.LastRun property contains the date and time from where the task was last run.

The function “IsDue” checks to see if the task has run within the selected interval. If not, it returns true and the task may execute it’s functionality. If it has already run, it returns false, and the functionality is skipped.

The functionality will not give you dead-on accuracy but will do for most jobs.

Posted in c#, Sitecore 6 | Tagged , , , , | 8 Comments