Sitecore links in multisite solutions – SiteResolving

One of the key features in Sitecore is that you can have multiple sites in the same installation. This is not just about saving money on licenses. The feature allows you to, for example, operate one main website and a multitude of subsites each having their own URL. Because they share the same Sitecore, you can share resources like the media library or templates, layouts, users etc.

Another key benefit is that it allows you to create internal links between sites. These links are maintained by the LinkManager so links are updated automatically. And when they are rendered they will still point to the correct host.

Imagine this simplified scenario where you have 2 websites called SITE1 and SITE2. In Sitecore you create the 2 websites:

/sitecore/

/content/

/site1/

/home/

/subitems…

/site2/

/home/

/subitems…

To give each website its own hostname you need to set up 2 new sites in the web.config:

<site name="website_1" hostName="www.site1.com" language="en" cacheHtml="false" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content/site1" startItem="/home" database="web" domain="extranet" allowDebug="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
<site name="website_2" hostName="www.site2.com" language="en" cacheHtml="false" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content/site2" startItem="/home" database="web" domain="extranet" allowDebug="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />

Now when accessing www.site1.com you are given the home page at /sitecore/content/site1/home, and www.site2.com returns the home page at /sitecore/content/site2/home.

Imagine you create a link on the page at /site1/home/subitem to /site2/home/subitem. Internally in Sitecore this is stored as a GUID. But when rendered, the LinkManager ensures that the link is rendered as www.site2.com/subitem.aspx.

It does so because in the web.config, SiteResolving is enabled by default:

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

When SiteResolving is true, the LinkManager will resolve the correct site from the <sites> section in web.config. It makes a best match and finds the site that the page belongs to, and resolves the hostname and URL based on the context of the site it found.

If you disable the SiteResoving the LinkManager stays in the current context. The link would then have been www.site1.com/sitecore/content/site2/home/subitem.aspx.

LIMITATIONS IN THE SITERESOLVER

UPDATE: Please note that these limitations have been adressed from Sitecore 6.4.1 Update 6. Read more about it here.

There are limitations to siteresolving. Imagine you have a setup where you not only have multiple sites, but also multiple languages per site. And that each of the languages have their own host.

So if your SITE1 has 2 languages (english and danish) you have www.site1.com and www.site1.dk. And SITE2 also has 2 languages, www.site2.com and www.site2.dk.

If you enable SiteResolving, all links linking to the same website (all links from SITE1 to SITE2) will be resolved as links to the first site content found in the web.config.

What does this mean? It means that if you in the web.config have the following order of sites:

All links from www.site1.dk will point to www.site1.com/dk/subitem.aspx. The SiteResolver does this because the algorithm resolves URLS as the first path that is the shortest is the one used. And since www.site1.com/dk/subitem.aspx has just as short a path as www.site1.dk/dk/subitem.aspx it’s a match, and voila, problem solved.

MORE READING

Posted in Sitecore 6 | Tagged , , , | 5 Comments

Sitecore DMS uses master database per default

This one was identified by my colleague Thomas Stern. The Sitecore Digital Marketing System (formerly known as Sitecore OMS, affectionally known as Sitecore Analytics) uses the master database per default as its lookup database. This is great for people who doesn’t like to publish their contents, bad for people who uses staged environments where the front-end server have no access to the master database.

If you have DMS enabled on a front end server with no access to the master database, you will get the following errors in your log:

Exception: System.InvalidOperationException Message: Could not find configuration node: databases/database[@id='master']
Source: Sitecore.Kernel 

The problem is easily resolved using the hidden feature “Analytics.DefaultDefinitionDatabase”. Create a .include file (or add the setting directly to the web.config) and set the Default Definition Database to “web”:

<configuration>
   <sitecore>
     <settings>
       <setting name=“Analytics.DefaultDefinitionDatabase“ value=“web“ />
     </settings>
   </sitecore>
</configuration>

Remember to instruct your Analytics people to publish their changes.

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

Sitecore poor database performance

If you experience decreasing performance in your Sitecore solution, it’s not always your fault. And it’s not always Sitecore’s fault either.

The database is usually the last place I would look for performance issues. Most performance issues comes from poor XSLT design (which is why I quit using XSLT’s). Other issues revolve around selecting too many items at once (remember to use the Lucene index). And a few issues revovle around the security settings (Is Siteore security slowing you down?).

However, in certain situations, the SQL server indexes may decrease in performance. This is usually due to index fragmentation which is why you need to rebuild them.

This script will rebuild all indexes with a fill factor of 80 (Fillfactor determines the percentage of the space on each leaf-level page are filled with data):

DECLARE @TableName VARCHAR(255)
DECLARE @sql NVARCHAR(500)
DECLARE @fillfactor INT
SET @fillfactor = 80
DECLARE TableCursor CURSOR FOR
SELECT OBJECT_SCHEMA_NAME([object_id])+'.'+name AS TableName
FROM sys.tables
OPEN TableCursor
FETCH NEXT FROM TableCursor INTO @TableName
WHILE @@FETCH_STATUS = 0
BEGIN
SET @sql = 'ALTER INDEX ALL ON ' + @TableName + ' REBUILD WITH (FILLFACTOR = ' + CONVERT(VARCHAR(3),@fillfactor) + ')'
EXEC (@sql)
FETCH NEXT FROM TableCursor INTO @TableName
END
CLOSE TableCursor
DEALLOCATE TableCursor
GO

You can find the original script here:

http://blog.sqlauthority.com/2009/01/30/sql-server-2008-2005-rebuild-every-index-of-all-tables-of-database-rebuild-index-with-fillfactor/

Other resources:

Thanks to Michael Sundstrøm for the tip.

Posted in Sitecore 6 | Tagged , , , , | 1 Comment

Using Sitecore Jobs

Sitecore contains a job system, allowing you to start and run jobs in the background. Jobs are perfect for long running operations. Scheduled tasks is jobs that is started at certain time.

Jobs have no HttpContext and, depending on how they are started, may run in a different context that you expect. For example, jobs started from the Scheuled tasks run in the “scheduler” context.

This is a madeup example on how to start a job that will traverse through Sitecore and do stuff to every item, starting from a certain root.

using System;
using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Jobs;
using Sitecore.Pipelines;
using Sitecore.SecurityModel;

namespace MyCode
{
  public class JobService
  {
    private string _JOBNAME = "MyJob";

    public Job Job
    {
      get { return JobManager.GetJob(_JOBNAME); }
    }

    public string StartJob(Item root)
    {
      JobOptions options = new JobOptions(_JOBNAME,
                                          "MyCode",
                                          Context.Site.Name,
                                          this,
                                          "ConvertData",
                                          new object[] {root});
      JobManager.Start(options);
      return _JOBNAME;
    }

    public void ConvertData(Item root)
    {
      ProcessAllItems(root);
      if (Job != null)
      {
        Job.Status.State = JobState.Finished;
      }
    }

    private void ProcessAllItems(Item item)
    {
      ProcessItem(item, pipeline);
      foreach (Item childItem in item.Children)
      {
        ProcessAllItems(childItem);
      }
    }

    private void ProcessItem(Item item)
    {
      if (Job != null)
      {
        Job.Status.Processed++;
        Job.Status.State = JobState.Running;
      }
      // Do something to my item
    }
  }
}

You can call the code like this:

JobService service = new JobService();
service.StartJob(Sitecore.Context.Item);

The job is created using the JobOptions class. This class create a Job with a job name and a category. The parameter “methodName” takes the name of a function to execute in the background. You include parameters using the last parameter.

When calling JobManager.Start() you execute the job in the background.

You can then use the Sitecore.Context.Job to get the current job for the current context. Or you can ask for a specific job (as I do in the “Job” property) using JobManager.GetJob().

A job have limited reporting capabilities, but you can use the Job.Status.Processed to indicate progress. You can even use the Job.Status.Total if you know beforehand how many operations the job is processing, and Sitecore Rocks will even convert the numbers displayed and show a percentage progress instead of a count. Use the Job.Status.State to indicate the status of your job (Initializing, Queued, Running, Finished).

Here is a simple example on how to check the status of a job:

protected void Page_Load(object sender, EventArgs e)
{
  JobService service = new JobService();
  if (service.Job != null)
  {
    Response.Write(string.Format("Job Status: {0}. Count: {1}", service.Job.Status.State, service.Job.Status.Processed));
  }
}

You can read more about jobs here:

Posted in .net, c#, Sitecore 6 | Tagged , , | 4 Comments

Sitecore.Diagnostics.Assert statements

Recently I was corrected by my colleague Alan Coates about my use of Assert statements in my code, which gave me the opportunity to write this article.

Sitecore has a fundamental Assert (Sitecore.Diagnostics.Assert) namespace which differs fundamentally from the .NET Debug.Assert that it actually thows an exception if the asserted condition is false. Asserting your code is a fast way of checking that conditions are met in a function before executing it:

void Page_Load(object sender, System.EventArgs e)
{
  DoStuff(null);
}

private void DoStuff(Item item)
{
  Sitecore.Diagnostics.Assert.ArgumentNotNull(item, "item"); // Assertion
  Response.Write(item.Paths.FullPath);
}

The example above throws an ArgumentNullException if the item parameter is null, and it even formats the exception message like this:

Value cannot be null.
Parameter name: item

Sitecore contains different Assert statements which thows different exceptions. In the example above I could have used the IsNotNull statement instead:

private void DoStuff(Item item)
{
  Sitecore.Diagnostics.Assert.IsNotNull(item, "item");
  Response.Write(item.Paths.FullPath);
}

But I would then return an InvalidOperationException and the exception message would not have been formatted for me, and I would have to write the complete exception message myself:

private void DoStuff(Item item)
{
  Sitecore.Diagnostics.Assert.IsNotNull(item, "The parameter 'item' is null");
  Response.Write(item.Paths.FullPath);
}

There are several function to choose from, each of thise returns different exceptions when the condition is not met:

  • AreEqual return an InvalidValueException. You have to write the exception message yourself (for example “a is not equal to b”)
  • ArgumentCondition, ArgumentNotNull, ArgumentNotNullOrEmpty return an ArgumentException, pre-formatted for you (see the examples above).
  • CanRunApplication return an AccessDeniedException if the user does not have access to the application stated in the parameter (this is probably mostly used internally)
  • HasAccess also returns an AccessDeniedException if the condition is not met.
  • IsEditing returns an EditingNotAllowedException if the item in the parameter is not in editing mode.
  • IsFalse, IsNotNull, IsNotNullOrEmpty, IsNull and IsTrue return an InvalidOperationException if the condition is not met. You have to format the exception message yourself.
  • ReflectionObjectCreated returns an UnknownTypeException if the condition is not met (also most an internal function I guess).
  • Required returns an RequiredObjectIsNullException when condition is not met. You have to format the message yourself.
  • ResultNotNull is basically a generic version of ArgumentNotNull returning the message “Post condition failed” when condition is not met.

if you look in the Sitecore source code you will notice that basically every Sitecore function asserts all parameters before executing the function, and so should you.

But remember the exception handling as well. Asserting parameters is good practise, but crashing a page because a simple condition is not met is not. For example, if you use a dictionary to look for text strings in Sitecore, you should assert that the dictionaty item is present in Sitecore before returning a value. But you should handle the exception nicely in your domain code so the entire page does not crash just because a dictionary text is missing.

Posted in c#, General .NET, Sitecore 6 | Tagged , , , , , | 2 Comments

XElement get default values using extension methods

I often work with dynamic XML documents which might or might not contain the element, field or attribute that I am looking for. The Standard .NET XElement class is pretty strict (as it should be) and returns a null reference execption if you ask for the value of an element that is not there:

<employees>
 <employee id="bp">
   <name>Brian</name>
 </employee>
 <employee id="ap">
   <name>Anita</name>
   <position>CEO</position>
 </employee>
</employees>

In the example above I can look for the position element for employee AP, but not for employee BP. Instead of building my exception handling every time I ask for the position element, I have built a set of extension methods that take care of this.

Extension methods 1 and 2: SafeValue and SafeAttribute

The following methods allows me to return a default value if the value of either an element or an attribute is not present:

using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Linq;

namespace MyCode.Extensions
{
  public static class XElementExtension
  {
     public static string SafeValue(this XElement element, string defaultValue)
     {
       if (element == null)
         return defaultValue;
       if (element.Value == null)
         return defaultValue;
       return element.Value;
     }

     public static string SafeAttribute(this XElement element, XName name, string defaultValue)
     {
       if (element == null)
         return defaultValue;
       if (element.Attribute(name) == null)
         return defaultValue;
       if (element.Attribute(name).Value == null)
         return defaultValue;
       return element.Attribute(name).Value;
     }
  }
}

Extensions 3 and 4: GetElementFromPath and GetValueFrompath

These extensions are more exotic. They allow me to ask for an element or a value down a path from the current element, returning a default value if the element or value is not present:

public static XElement GetElementFromPath(this XElement element, params string[] path)
{
  if (element == null)
    return null;

  XElement current = element;
  foreach (string s in path)
  {
    if (current != null && current.Element(s) != null)
      current = current.Element(s);
  }
  return current;
}

public static string GetValueFromPath(this XElement element, params string[] path)
{
  if (element == null)
    return string.Empty;

  XElement current = element;
  foreach (string s in path)
  {
    if (current != null && current.Element(s) != null)
      current = current.Element(s);
  }
  return current != null ? current.Value : "";
}

The functions might seem strange, but it’s cool to be able to do this:

element.GetValueFromPath("employee", "position");

Extension 5: Convert XElement to XmlElement

The last extension method is a old goodie. Converting from the LINQ based XElement to the old-fashioned XmlElement class is handy when working with web services or other methods that requires the old XmlElement.

    public static XmlElement ToXmlElement(this XElement element)
    {
      return new XmlService(element).CreateXmlElement();
    }

    #region internal conversion class

    internal class XmlService
    {
      private readonly StringBuilder _sb;
      private readonly XmlWriter _writer;

      private XmlService()
      {
        _sb = new StringBuilder();
        _writer = new XmlTextWriter(new StringWriter(_sb))
        {
          Formatting = Formatting.Indented,
          Indentation = 2
        };
      }

      public XmlService(XNode e) : this()
      {
        e.WriteTo(_writer);
      }

      public XmlService(XmlNode e) : this()
      {
        e.WriteTo(_writer);
      }

      public XElement CreateXElement()
      {
        return XElement.Load(new StringReader(_sb.ToString()));
      }

      public XDocument CreateXDocument()
      {
        return XDocument.Load(new StringReader(_sb.ToString()));
      }

      public XmlElement CreateXmlElement()
      {
        return CreateXmlDocument().DocumentElement;
      }

      public XmlDocument CreateXmlDocument()
      {
        var doc = new XmlDocument();
        doc.Load(new XmlTextReader(new StringReader(_sb.ToString())));
        return doc;
      }
    }
    #endregion
Posted in .net, c#, General .NET | Tagged , , , , | 1 Comment

Is Sitecore security slowing you down?

Recently, several blogs have posted about hidden settings that allows you to disable certain security related features in Sitecore. Sitecore can contain many users or many groups. This will sometimes lead to slow performance in the Sitecore shell or in the Sitecore page editor.

One of the reasons that Sitecore becomes slow, is that some security-releated lookups is done using the Sitecore Fast Query. Fast queries are not always fast, as these queries are non-cached SQL select statements done directly on the database:

Database.SelectItems("fast://*[@__lock='%\"" + Context.User.Name + "\"%']");

Some of the lookups are done merely to enhance the user experience – not to enforce security schemes. So some of the lookups can be disabled using web.config settings. Here are a few of them:

CheckSecurityOnLanguages

This settings enforces security on languages. The settings is TRUE per default and allows you to allow/disallow access to languages. Read this article on how to set up security on languages.

The check is used by Sitecore.Globalization.GetLanguages(), Sitecore.Security.AccessControl.ItemAccess.CanReadLanguage() and Sitecore.Security.AccessControl.ItemAccess.CanWriteLanguage(), and it is also  used by the GetContentEditorWarnings.CanReadLanguage processor.

If your website is not a multilanguage site, or you allow all editors to access all languages, you can disable this check.

ContentEditor.CheckSecurityOnTreeNodes

This settings is also TRUE by default and it will read the security settings on each node to ensure that you have read acces. If not, the node is hidden. If FALSE, every node is displayed in the tree view, and security is enforced when clicking the node. If you don’t care that every editor can see every node in the tree, you may disable this. Security is still enforced when the editor clicks an item.

WebEdit.ShowNumberOfLockedItemsOnButton

This settings applies to the page editor, especially the “My Items” button, which will display how many items you have locked. This lookup is done using Sitecore Fast Query, and can be painfully slow when having lots of users and groups.

Read more about the ShowNumberOfLockedItemsOnButton feature here.

Other resources:

If you are interested in Sitecore performance tweaking, these blogs are worth a visit:

Posted in Sitecore 6 | Tagged , , , , , , , , | 3 Comments

Using Sitecore EditFrame in PageEdit

The sc:EditFrame is a Sitecore Page Editor (called Content Editing in Sitecore marketing terminology) feature that allows front end access to fields that are not directly accessible, either because they are not visible or they are of a type that are not directly front end editable.

Basically, Text, Rich Text, Image, Link  and fields like that are directly editable. But MultiList fields, checkbox fields and other types are not. This is where the EditFrame comes in.

This is a list of links placed on my website:

List of Links

List of Links

The list of links is a TreeList field:

TreeList Field

TreeList Field

I would like to give the user acces to edit this field from the Page Editor. But using the sc:FieldRenderer only allows the user to edit any field that the treelist points to. So to give access to the TreeList field itself, I need to do the following:

First, I need to create a new Edit Frame Button in the core database.

Go to /sitecore/content/Applications/WebEdit/Edit Frame Buttons and create a new Edit Frame Button Folder. In this folder, create a new Edit Frame Button. In the field “Fields” you type in te names of the fields that this button should give edit acces to:

EditFrame Buttons

EditFrame Buttons

Next step is to modify the user control that renders my list of links:

In the rendering that renders the list of links, I must add an EditFrame surrounding my list:

<sc:EditFrame id="editLinks" runat="server" Buttons="/sitecore/content/Applications/WebEdit/Edit Frame Buttons/DocumentLinks">

  // Inside this edit frame is the HTML that renders the list of links.
  <h2>
    Links
  </h2>
  <asp:ListView ID="lvLinks" runat="server" DataSource="<%# Links %>" EnableViewState="false">
    <LayoutTemplate>
      <ul class="LinkList">
        <asp:PlaceHolder runat="server" ID="itemPlaceholder" />
      </ul>
      </LayoutTemplate>
        <ItemTemplate>
          <li><a href="<%# Sitecore.Links.LinkManager.GetItemUrl(Container.DataItem as Sitecore.Data.Items.Item) %>">
            <sc:FieldRenderer ID="frItemTitle" FieldName="NavigationTitle" runat="server" Item="<%# Container.DataItem as Sitecore.Data.Items.Item %>" />
          </a></li>
        </ItemTemplate>
      </asp:ListView>
  </asp:Panel>

</sc:EditFrame>

The EditFrame has an attribute called “Buttons” that points to the EditFrame Buttons Folder that I created earlier.

The code is now finished.

When the user hover above the Links in Page Edit mode, he will see the edit frame with the “Edit” button:

EditFrame

EditFrame

And clicking the button will open the TreeList field in a seperate window:
Open EditFrame For Editing

Open EditFrame For Editing

Please Note:

In Sitecore 6.5 (maybe also other versions of Sitecore) you need to save the page before the selected fields becomes visible. It is not enough to press the “OK” button in the dialog window.

Other usages

You can also use the EditFrame for fields that are only visible if they are not empty, and for fields that are not visible at all. You can use the Sitecore.Context.PageMode.IsPageEditor attribute to determine if the current page is in the Page Edit mode, and when so, display a text inside the edit frame:

<sc:EditFrame id="editLinks" runat="server" Buttons="/sitecore/content/Applications/WebEdit/Edit Frame Buttons/DocumentLinks">
  <asp:Panel ID="panNoLinksAndPageEditing" Visible="<%# Sitecore.Context.PageMode.IsPageEditor %>" runat="server">
    Edit Links
  </asp:Panel>
</sc:EditFrame>

The original code with the links list actually uses the same technique, as the list is hidden if there is no links selected. In this scenario, I add a panel that is only visible if the list is empty and the page is in editing mode:

<sc:EditFrame id="editLinks" runat="server" Buttons="/sitecore/content/Applications/WebEdit/Edit Frame Buttons/DocumentLinks">
  <asp:Panel ID="panNoLinksAndPageEditing" Visible="<%# !HasLinks && Sitecore.Context.PageMode.IsPageEditor  %>" runat="server" CssClass="Spot SpotLinks">
    <dictionary:literal runat="server" path="/Document/Links" defaulttext="Links" />
  </asp:Panel>
  <asp:Panel ID="panLinks" Visible="<%# HasLinks %>" runat="server" CssClass="Spot SpotLinks">
    <h2>
      <dictionary:literal runat="server" path="/Document/Links" defaulttext="Links" />
    </h2>
      <asp:ListView ID="lvLinks" runat="server" DataSource="<%# Links %>" EnableViewState="false">
        <LayoutTemplate>
          <ul class="LinkList">
            <asp:PlaceHolder runat="server" ID="itemPlaceholder" />
          </ul>
        </LayoutTemplate>
        <ItemTemplate>
          <li><a href="<%# Sitecore.Links.LinkManager.GetItemUrl(Container.DataItem as Sitecore.Data.Items.Item) %>">
            <sc:FieldRenderer ID="frItemTitle" FieldName="NavigationTitle" runat="server" Item="<%# Container.DataItem as Sitecore.Data.Items.Item %>" />
          </a></li>
        </ItemTemplate>
      </asp:ListView>
  </asp:Panel>
</sc:EditFrame>

The EditFrame is also your shourtcut to upgrade websites that are not Page Editable from the beginning. Although I would recommend that you rewrite your website using the sc:FieldRenderer.

Posted in .net, c#, General .NET, Sitecore 6 | Tagged , , , , , | 8 Comments

Parse namevalue collection with quotation marks handling

Parsing of namevalue collections is a common pattern. .NET have build in parsing of Query Strings, and other bloggers have solutions on how to do this.

This is Yet Another NameValueCollection parser. It will parse not only query strings, but also other collections. This query string can be parsed:

  • id=2&title=hello&subtitle=world
    • id = 2
    • title = hello
    • subtitle = world

But also more complex strings, where your seperators are inside quotation marks can be parsed. For example:

  • id=2|title=’hello=world‘|subtitle=’sub|title
    • id = 2
    • title = hello=world
    • subtitle = sub|title

Ignoring seperators inside quotes (” and “”) requires more complex code than just calling the Split() function on a string, but the code is not that complex.
This class parses any name/value collection string, ignoring seperators inside quotes (” and “”):

using System;
using System.Collections;
using System.Collections.Generic;

namespace PT.MyCode
{
  internal class NameValueCollectionParser
  {
    public NameValueCollectionParser(char innerSeperator, char outerSeperator)
    {
      InnerSeperator = innerSeperator;
      OuterSeperator = outerSeperator;
    }

    public char InnerSeperator { get; private set; }
    public char OuterSeperator { get; private set; }

    public void GetNameValueSet(string nameValueColection, ref Dictionary<string, string> output)
    {
      foreach (string nameValueSet in Split(nameValueColection, OuterSeperator))
      {
        string[] nameValueSetSplitted = Split(nameValueSet, InnerSeperator);
        if (nameValueSetSplitted.Length != 2)
          return;
        output.Add(nameValueSetSplitted[0], nameValueSetSplitted[1]);
      }
    }

    private string[] Split(string values, char seperator)
    {
      var list = new ArrayList();
      bool insidePlings = false;
      int ac = 0;
      int begin = 0;
      CharEnumerator en = values.GetEnumerator();
      while (en.MoveNext())
      {
        if (en.Current == '\'' || en.Current == '\"')
        {
          if (insidePlings)
            insidePlings = false;
          else
            insidePlings = true;
        }
        else if (en.Current == seperator && !insidePlings)
        {
          string s = Trim(values.Substring(begin, ac - begin));
          list.Add(s);
          begin = ac + 1;
        }
        ac++;
      }
      list.Add(Trim(values.Substring(begin, values.Length - begin)));

      return list.ToArray(typeof (string)) as string[];
    }

    private string Trim(string s)
    {
      s = s.TrimStart(',');
      s = s.TrimStart('\'', '\"');
      s = s.TrimEnd('\'', '\"');
      s = s.Trim();
      return s;
    }
  }
}

The class is prepared to be used in an Extension method on the Dictionary class:

using System.Collections.Generic;

namespace PT.MyCode
{
  internal static class DictionaryExtension
  {
    public static void AddNameValueCollection(this Dictionary<string, string> dictionary, char outerSeperator, char innerSeperator, string value)
    {
      NameValueCollectionParser parser = new NameValueCollectionParser(innerSeperator, outerSeperator);
      parser.GetNameValueSet(value, ref dictionary);
    }

  }
}

The extension method is now ready to be used:

Dictionary<string, string> attributes = new Dictionary<string, string>();
attributes.AddNameValueCollection('|', '=', "id=2|title=\"hello=world\"|text=\"tell me how|you are doing\"");
Posted in .net, c#, General .NET | Tagged , , , , , , | Leave a comment

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