« 2007-07 | HomePage | 2007-12 »

Wednesday, 24 October 2007

Sitecores abstract data layer

This is an area that I know for sure most people have not been working with at all in Sitecore. It’s the abstract data layer which lies between the physical layer (data providers), and the API.

 

The traditional description of the abstract data layer is that it handles Sitecore caching. However, it does a bit more than that; it parses the actual requests from the API, - figures out which data provider the current branch uses, and then passes the request to the data provider.

f5ed4d1d714b27843076cbdb8a2d968e.png

 

Usually developers would not hook into the abstract data layer, - partly because it’s a very complex layer, - and normally there’s no reason to do so. However, a better understanding of this layer comes in handy, and in very rare occasions hooking into the layer may be a good way to tamper with data on a overall level.

The abstract data manager holds an interesting class, - ItemManager which in passes all request from the API to an ItemProvider object. Methods such as GetParent, GetChildren and GetItem can be found in this object. The ItemProviders job is to pass the requests on to the physical layer, - the data providers.

If you override the existing provider, you can hook into the data stream and tamper with it, - and ultimately the data after it’s retrieved from the physical layer. Modifying the data is not depending on an individual database or database provider, but will happen across all databases.

An example of such modification would be adding a filter to the abstract data layer which may remove unwanted items, such as items that does not have a specific value or data specified; and this will be across all database providers (e.g. filter data regardless of it coming from SQL server, SharePoint or any other database).

To create such a hook, create a class that inherits from ItemProvider; and the override appropriate methods (for example, GetChildren). Register the class by setting the ItemManager.provider property to your custom class. This, - however, is not easy as the provider is registered automatically when Sitecore initializes, - and it’s not an entry in the web configuration file. This can be done, however, by creating a piece of code that too is run when Sitecore initializes, and from this point setting the property.

 

Example: Filter children

The following example will filter all child items of a parent in the master database starting with the value provided by a field in the parent item (this field is conveniently named filter). Here’s some before/after screenshots:

Before filter has been applied:
12b3046aad0f5066a1146d9bee60212d.png

 

A screenshot of the content editor after the filter has been applied: Notice that the two printers starting with BL has disappeared.
b06df3b03b747bea7780e9d8e8c6a5b3.png

 

In order to implement this code, we need to write (or rather, override) the item provider itself. We also need to write a bit of code which, - upon initialization, sets the ItemManager provider property to our custom class; to do so, we use the initialization pipeline (which can be found in the web configuration file).

To hook into the initialization pipeline, we must create a small processor; let’s name it ReplaceItemProvider: We add this provider to the end of our initialization pipeline:

 

<pipelines>
  <initialize>
     :
    <processor type="Sitecore.DynamicXslExtensions.Loader,
      Sitecore.DynamicXslExtensions"
/>
    <processor type="Sitecore.Testing.ReplaceItemProvider,
      ItemProviderExtender"
/>
  </initialize>
  :

 

I have added the processor to the end of the initialize processor, in order to ensure that all existing classes and objects has been created and registered. The following code dynamically sets the property of the ItemManager to our custom ItemProvider:

 

using Sitecore.Data.Managers;
using Sitecore.Pipelines;

namespace Sitecore.Testing {
  public class ReplaceItemProvider {
    public void Process(PipelineArgs args) {
      ItemManager.Provider = new FilterItemProvider();
    }
  }
}

 

The FilterItemProvider class filters the actual data:

 

using System;
using Sitecore.Collections;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Diagnostics;
using Sitecore.SecurityModel;

namespace Sitecore.Testing {
  public class FilterItemProvider : ItemProvider{

    public override ChildList GetChildren(Item item,
      SecurityCheck securityCheck) {
      Assert.ArgumentNotNull(item, "item");
      if (item.Database.Name!="master"){
        return base.GetChildren(item, securityCheck);
      }
      ChildList children = base.GetChildren(item, securityCheck);
      if(children == null){
        return null;
      }
      return Filter(children, item);
    }

    ChildList Filter(ChildList children, Item parent) {
      string filter = parent["filter"];
      if(filter.Length <= 0) {
        return children;
      }
      ItemList filtered = new ItemList();
      foreach(Item child in children) {
        if(!child.Name.StartsWith(filter, 
            StringComparison.OrdinalIgnoreCase)) {
          filtered.Add(child);
        }
      }
      return new ChildList(parent, filtered);
    }
  }
}

 

The code is rather self-explaining, but there are a few interesting parts here:
I override the GetChildren method only, and call’s the base method: The base method returns all the items from the data provider layer; filtering is done after by creating a new (filtered) list:

 

public override ChildList GetChildren(
     Item item, SecurityCheck securityCheck) {
  :
  ChildList children =
       base.GetChildren(item, securityCheck);
  :
  return Filter(children, item);
  :

 

I apply the filter by calling the internal Filter method: If the parent item has a “filter” field, and it’s not blank, items will be added to a new list, if they match the criteria:

 

:
foreach(Item child in children) {
  if(!child.Name.StartsWith(filter,
      StringComparison.OrdinalIgnoreCase)) {
    filtered.Add(child);
  }
}

13:55 Posted in Sitecore | Permalink | Comments (1) | Email this

Wednesday, 17 October 2007

Modifying the HttpRequest pipeline, custom 404 handler

Today, during level 2 training I got an idea when describing the Sitecore HttpRequest pipeline. This pipeline is executed every time a request is made: If it’s a web page, if it’s web services or if it’s the AJAX layer.

The purpose of the request pipeline is to populate the Sitecore context with information on the current request, - such as visiting device, security settings and the item requested.

 

I decided to do a small example on pipeline usage, while the participants were working hard to complete a lab. In this example I’m inserting a class right after the pipeline has checked if the current requested item exists. If not, - I will redirect to another Sitecore item which is the “page not found” page. As a small addition, I will parse the requested document and search the content structure for matching alternative items and display a description, something like:

 

Page not found.

The page you were looking for “xyz” was not found. However, you may want to visit the following topics:

  • Topic1
  • :

This example is nowhere near perfect: You should rather use a real search engine to find the item, - and this is also something I will recommend that you do if you decide to use this approach.

 

HttpRequest pipeline

As described above, the HttpRequest pipeline is executed on every single request for Sitecore, - and it populates the Sitecore.Context objects with information ranging from the current user, security, devices, databases and the requested item. The following entry in the pipeline finds the requested item, and if the item does not exists, instead of returning the normal 404 page (404 page not found), my class will set the new item:


<httpRequestBegin>
  :
  <processor type="Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel" />       
  <processor type="Sitecore.Examples.PageNotFound, PageNotFound" method="ProcessCustomPageNotFound" />
  :
</httpRequestBegin>

 

The Sitecore.Examples.PageNotFound class contains the following code:

 

using Sitecore.Data;
using Sitecore.Data.Items;

namespace Sitecore.Examples {
  public class PageNotFound {
    public void ProcessCustomPageNotFound(
      Pipelines.HttpRequest.HttpRequestArgs args) {

      if (Context.Item == null) {

        if (Context.Database.Name == "web") {         

          Item PageNotFoundPage =
            Context.Database.GetItem(
            "/sitecore/content/home/global/pagenotfound");

          if (PageNotFoundPage!=null){
            Context.Item = PageNotFoundPage;

          }
        }
      }
    }
  }
}

 

As you can see, I simply check if Sitecore has found an item (if the context item is null), and if not, do a quick check if the database is the web database (we don’t want to show this page for internal Sitecore items). I also try to identify the page not found item in the Sitecore content structure, - that’s the page that actually displays the error code: Make sure you do have a Sitecore item (with an assigned layout) in the structure. The item is, in this example, located under /home/global/pagenotfound.

 

As a small treat, - I have also created a sub layout that searches the Sitecore data structure for items that may match the URL the user has entered. Now, I realize this way of searching may be nowhere optimal, - and I would therefore recommend you to refine it should you choose this approach and use this code.

 

The sublayout contains a single label, looking something like this:

 

<%@ Control Language="C#" AutoEventWireup="true"
    CodeBehind="PageNotFoundDetails.ascx.cs"
    Inherits="PageNotFound.layouts.WebUserControl1" %>

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

<asp:Label ID="lblAbstract" CssClass="normal"
    runat="server">
</asp:Label>

 

With this code-behind:

 

using System;
using System.Text;
using Sitecore.Data.Items;
using Sitecore.Web;

namespace PageNotFound.layouts {

  public partial class WebUserControl1 :
    System.Web.UI.UserControl {

    protected void Page_Load(object sender, EventArgs e) {

      Response.StatusCode = 404;
      Response.Status = "404 Page not found";

      string searchedfor = WebUtil.GetUrlName(0);

      string sPath =
        "/sitecore/content/home//*[startswith(@@key,'" +
        searchedfor.Substring(0, 2) + "')]";

      Item[] found =
        Sitecore.Context.Database.SelectItems(sPath);

      StringBuilder stringBuilder = new StringBuilder();

      stringBuilder.Append(
        "<h1>The requested page, '" +
        searchedfor +
        "' could not be found.</h1>");

      if (found.Length>0){
        stringBuilder.Append(
          "<h3>However, you might find these interesting:</h3>");

        stringBuilder.Append("<ul>");

        foreach (Item item in found) {
          stringBuilder.Append("<li><a href='"+
            item.Paths.GetFriendlyUrl()
            + "'>" + item.Name + "</a></li>");

        }

        stringBuilder.Append("</ul>");

      }

      lblAbstract.Text = stringBuilder.ToString();

    }
  }
}

 

A single comment to this code:

 

You may notice that I set the Response.StatusCode and Response.Status to 404 and a matching description.

      Response.StatusCode = 404;
      Response.Status = "404 Page not found";

 

This is naturally because is a visiting spider identifies a link to a non-matching page, - and are redirected to this page, - it should be mapped as a page that does not exists, -and therefore should be removed from the index.
The following line extracts the last part of the requested URL:

      string searchedfor = WebUtil.GetUrlName(0);

 

For example, it would return apple from this URL request: http://localhost/products/apple.aspx.

 

I also utilize the Sitecore path search to find a matching document in the entire content structure, by extracting the first 2 letters from the above request part, and attempts matching items with that name:

      string sPath =
        "/sitecore/content/home//*[startswith(@@key,'" +
        searchedfor.Substring(0, 2) + "')]";

 

Download file (Sitecore package with project file and installation instructions):

PageNotFound-v.1.zip.

12:52 Posted in Sitecore | Permalink | Comments (3) | Email this

Tuesday, 02 October 2007

Trigger workflow engines from Sitecore

Very often I’m asked if it’s possible to hook in, - or trigger remote workflow engines from within Sitecore. There are several of approaches to do so. This is the two most common methods:

  1. Workflow action
    Build a .NET class and hook them into the workflow as an action. The workflow event, e.g. when entering the “editing state” of a workflow, will trigger the remote workflow engine, such as a BizTalk orchestration, K2 or WWF.

    The Sitecore workflow API also allow you to change the state of the content item, e.g. move it to a new state, the remote engine can set new states in Sitecore such as the “done state”: To do so, you can use either the base .NET API or the Web API (for example Web Services)
     
    1. Advantages:
      1. Simple to implement: Cause workflow to invoke an action.
      2. Still maintains visualization in Sitecore. For example, you can use the workbox: The Sitecore workbox.

    2. Disadvantages:
      1. Involves a cross-workflow configuration as you will need to set up at least two steps in the Sitecore visualization (state that activates remote engine, and state that sets the item in “ready for publishing” state

  2. Workflow provider
    You can build your custom workflow provider, fully exchanging the Sitecore workflow class. To do so, implement the methods of the workflow class, and exchange the default workflow provider specified in the web.config file with your custom class.

    This is what you could consider the “real integration”.

    1. Advantages:
      1. Fully bypasses the Sitecore visualization state.
      2. May require fewer steps for binding Sitecore document types to a workflow (as you can configure the “bind document type to workflow” in the remote workflow engine).

    2. Disadvantages:
      1. Requires some complex coding as you should hook in features to hook remote workflow to a Sitecore item.
      2. Requires some coding to hook remote engine workflow state visualizations into the Sitecore shell (however, this is very often solved by loading the item as a frame, if  you can solve the challenge of authorization and authentication).

To be honest, i cannot recommend one approach for another. It's all about the level of integration you wish to execute

09:37 Posted in Sitecore | Permalink | Comments (1) | Email this