Archive

Archive for the ‘Sitecore 6’ Category

Sitecore MVC the first controller.

03/07/2012 1 comment

Sitecore 6.6 is now out, (as a technical preview), with the realse of this version Sitecore now offers full support for MVC. There have all ready been quite a few blog post how to user MVC with Sitecore and some of the new fetures offers a great deal of ease when developing with Sitecore.

So this is the first in a series of how to develop with Sitecore using MVC and some of the benefits you get for free using MVC.

If you haven’t allready read John West blog post on how to setup you Sitecore solution to use MVC you can read them here start with Part 1 and use part 3 to setup a visual studio solution .John also wrote some simple examples on how to use the MVC framework to return JSON serialized result from a controller you read his original post here. For a first post in this Sitecore MVC series we will revisit his example and create our own controller but with a different approach, and with the soon to be released .Net 4.5 Framework which include the new web-api this should be even easier, and given you even more flexibility for free and out of the box, we will look back at this example when the final release is out.

By now you should now have an up an running Sitecore 6.6 solution. My visual studio solution setup is shown below.

Now lets create our first controller Code listed below.


public class ItemController : Controller
 {

public JsonResult Index(string id)
 {
 return Json(CreateObject(ChildRepository.Get(parentItemId)), JsonRequestBehavior.AllowGet);
 }

private ChildItemRepository _childRepository;
 public virtual ChildItemRepository ChildRepository
 {
 get { return _childRepository ?? (_childRepository = new ChildItemRepository()); }

}

private IEnumerable<object> CreateObject(IEnumerable<Item> items)
 {
 foreach (Item item in items)
 yield return new {item.Name};
 }

}

With the controller in place you can change the route to newly created controller John in his example uses a Pipeline in Sitecore to achieve this i like this approache so much more the using the global.asax so we reuse his code for this with some minor alternations. The Id of the node we want to retrieve children for if not specified default to the id of the Home-node. I’ve done this because i don’t like all his assert for empty id, and if you don’t like you can just use the original approach with optional url parameter.You could if liked alsp default the index action. But instead of getting the id trough the url route data you should supply it as a argument to the action method. The key here is that parameter Id must be named the same as the input parameter in index method.


public class SitecoreMVCRoutes
 {

public virtual void Process(Sitecore.Pipelines.PipelineArgs args)
 {
 this.RegisterRoutes(RouteTable.Routes);
 }

public bool MvcIgnoreHomePage { get; set; }

protected virtual void RegisterRoutes(RouteCollection routes)
 {
 if (this.MvcIgnoreHomePage)
 {
 routes.IgnoreRoute(String.Empty);
 }

routes.MapRoute(
 "Default", // Route name
 "Item/{action}/{id}", // URL with parameters
 new { controller = "Item", action = "Index", id = "{110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}" } // Parameter defaults with Id to home node
 );
 }
 }

Now we will implement the busyness layer that will supply the controller with data.

This is done through a ChildRepository the code is listed below.

</pre>
public class ChildItemRepository
 {
 public virtual IEnumerable<Item> Get(string parentItemId)
 {
 ID parentId;
 if(ID.TryParse(parentItemId,out parentId))
 return GetChildNodes(parentId);
return new List<Item>();
 }

private IEnumerable<Item> GetChildNodes(ID parentId)
 {
 Item parentItem = Sitecore.Context.Database.GetItem(parentId);
 return parentItem.GetChildren();
 }
 }
<pre>

Lets test the new controller

First with out supplying a id,

Now with a valid id different form the home-node.

And now with a invalid id.

There we go the code does the same as Johns but with out all the assert tests.

Categories: .Net, C#, MVC, Sitecore 6, Uncategorized Tags: , ,

Running Sitecore Field Editor from a command

21/05/2012 15 comments

When working with Sitecores page editor sometimes you need to edit Hidden fields for examples page metadata ie. “Page Title, Metakeywords, Metadescription”.  You could probably use the editframe control and if in pageedit render out the metadatacontroll through a placeholder. But a more elegant way could be by adding a button in editor Ribbon see screenshot below, and executing  the page editor with a limited set of fields, and is this post we would create a general command tha does exactly that.

To make the command as general as possible in would be tied to a settings item of the type
“/sitecore/templates/System/WebEdit/Field Editor Button”

Which contains fields for displaying

  • Header
  • Editor Icon
  • List with fields that should be shown in the editor separated with “|”

And even better you can use this where you are using the sc:editframe.

Now you can add a button of you liking, you could make a chunk here:

/sitecore/content/Applications/WebEdit/Ribbons/WebEdit/Page Editor/Metadata

Under the chunk add a button
/sitecore/content/Applications/WebEdit/Ribbons/WebEdit/Page Editor/Metadata/Edit

The click event
“command:executefieldeditor(path=/sitecore/content/Applications/WebEdit/Edit Frame Buttons/Metadata/EditMetadata)”

executes the command “command:executefieldeditor” specified in the commands.config file.

Mine looks like this

  <command name="command:executefieldeditor" type="PT.Framework.ExecuteFieldEditorCommand.ExecuteFiledEditor,PT.Framework.ExecuteFieldEditorCommand"/>

Now to the code, by deriving from the “FieldEditorCommand” i get the execution of the page editor for free all i have to do is specifying the correct parameters in the virtual method “GetOptions”  , note that the “PageEditFieldEditorOptions” is located in the Sitecore.Client.dll.

<
public class ExecuteFiledEditor : FieldEditorCommand
{
  protected override PageEditFieldEditorOptions GetOptions(ClientPipelineArgs args, NameValueCollection form)
  {
    EnsureContext(args);
    return new PageEditFieldEditorOptions(form, BuildListWithFieldsToShow()) { Title = SettingsItem[HEADER], Icon = SettingsItem[ICON] };
  }

  private void EnsureContext(ClientPipelineArgs args)
  {
    CurrentItem = Database.GetItem(ItemUri.Parse(args.Parameters[URIPARAMETER]));
    Assert.IsNotNull(CurrentItem, CURRENTITEMISNULL);
    SettingsItem = Client.CoreDatabase.GetItem(args.Parameters[PATHPARAMETER]);
    Assert.IsNotNull(SettingsItem, SETTINGSITEMISNULL);
  }
 private List<FieldDescriptor> BuildListWithFieldsToShow()
  {
    List<FieldDescriptor> fieldList = new List<FieldDescriptor>();
    ListString fieldString = new ListString(SettingsItem[FIELDNAME]);
    foreach (string field in new ListString(fieldString))
     if (CurrentItem.Fields[field] != null)
      fieldList.Add(new FieldDescriptor(CurrentItem, field));

   return fieldList;
  }

 private const string FIELDNAME = "Fields";
 private const string HEADER = "Header";
 private const string ICON = "Icon";

 private const string URIPARAMETER = "uri";
 private const string PATHPARAMETER = "path";
 private const string CURRENTITEMISNULL = "Current item is null";
 private const string SETTINGSITEMISNULL = "Settings item is null";

private Item CurrentItem { get; set; }
 private Item SettingsItem { get; set; }

}

And the final result:

Categories: C#, Sitecore 6 Tags: ,

Queries in Datasource location on Sitecore Layouts

15/05/2012 10 comments

The datasource location specified on layouts is limited to Sitecore paths.

But working with multisite solutions it is sometimes necessary to have some items stored locally under Site.

This could be done fairly easy hooking in to the Sitecore pipeline “getRenderingDatasource”, found in the web.config under
“/configuration/sitecore/pipelines/getRenderingDatasource”

Below I’ve inserted my own

“PT.Framework.Pipelines.GetDatasourceLocation,PT.Framework.Pipelines”

<getRenderingDatasource>
 <processor type="Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceLocation, Sitecore.Kernel" />
 <processor type="PT.Framework.Pipelines.GetDatasourceLocation,PT.Framework.Pipelines" />
 <processor type="Sitecore.Pipelines.GetRenderingDatasource.SetFallbackDatasourceLocations, Sitecore.Kernel" />
 <processor type="Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceTemplate, Sitecore.Kernel" />
 <processor type="Sitecore.Pipelines.GetRenderingDatasource.GetTemplatesForSelection, Sitecore.Kernel" />
 <processor type="Sitecore.Pipelines.GetRenderingDatasource.CheckDialogState, Sitecore.Kernel" />
 <processor type="Sitecore.Pipelines.GetRenderingDatasource.GetDialogUrl, Sitecore.Kernel" />
 </getRenderingDatasource>

The code for this step is really simple:

</pre>
public void Process(GetRenderingDatasourceArgs args)
 {
 Assert.IsNotNull(args, "args");
 DatasourceLocation = args.RenderingItem["Datasource Location"];
 if(QueryInDataSourceLocation())
 ProcessQuery(args);
 }

private void ProcessQuery(GetRenderingDatasourceArgs args)
 {
 ContextItemPath = args.ContextItemPath;
 ContentDataBase = args.ContentDatabase;
 Item datasourceLocation = ResolveDatasourceRootFromQuery();
 if (datasourceLocation != null)
 args.DatasourceRoots.Add(datasourceLocation);
 }

private bool QueryInDataSourceLocation()
 {
 return DatasourceLocation.StartsWith(_query);
 }

private Item ResolveDatasourceRootFromQuery()
 {
 string query = DatasourceLocation.Replace(_query, ContextItemPath);
 return ContentDataBase.SelectSingleItem(query);
 }

private string DatasourceLocation { get; set; }
 private string ContextItemPath { get; set; }
 private Database ContentDataBase { get; set; }
 private const string _query = "query:.";
<pre>

Now you can write queries like “./ancestor-or-self::*[@@templatekey=’site’]/Spots”

Update – Multiple datasource locations

Since one might want to have multiple Datasource Roots i made a change to the code so you can separate queries with the “|” delimiter. Note the normal sitecore path is still working and handled by the
“Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceLocation”.

Now you can write

query:./ancestor-or-self::*[@@templatekey=’main section’]/QueryGlobal|/sitecore/content/GlobalSpots|query:./ancestor-or-self::*[@@templatekey=’site’]/Spots


public class GetDatasourceLocation
 {
 public void Process(GetRenderingDatasourceArgs args)
 {
 Assert.IsNotNull(args, "args");
 DatasourceLocation = args.RenderingItem["Datasource Location"];
 ContextItemPath = args.ContextItemPath;
 ContentDataBase = args.ContentDatabase;
 DatasourceRoots = args.DatasourceRoots;
 if(QueryInDataSourceLocation())
 ProcessQuerys(args);
 }

private void ProcessQuerys(GetRenderingDatasourceArgs args)
 {

ListString possibleQueries = new ListString(DatasourceLocation);
 foreach (string possibleQuery in possibleQueries)
 {
 if (possibleQuery.StartsWith(_query))
 ProcessQuery(possibleQuery);
 }

 }
 private bool QueryInDataSourceLocation()
 {
 return DatasourceLocation.Contains(_query);
 }

private void ProcessQuery(string query)
 {

 Item datasourceLocation = ResolveDatasourceRootFromQuery(query);
 if (datasourceLocation != null)
 DatasourceRoots.Add(datasourceLocation);
 }

private Item ResolveDatasourceRootFromQuery(string query)
 {
 string queryPath = query.Replace(_query, ContextItemPath);
 return ContentDataBase.SelectSingleItem(queryPath);
 }

private string DatasourceLocation { get; set; }
 private string ContextItemPath { get; set; }
 private Database ContentDataBase { get; set; }
 private List<Item> DatasourceRoots { get; set; }
 private const string _query = "query:.";

}

Categories: C#, Sitecore 6 Tags: ,

Unit testing Sitecore with Typemock Isolater.

27/04/2012 5 comments

In my last post I gave a stub example for a Sitecore Item. With the stub item you could test functionality for retrieving field data, which greatly limits the functionality you can test. So in this post I will have a look at the more advanced mocking framework Isolator from Typemock.  With the Isolator framework it is possible to intercept calls for methods that normally wouldn’t be possible, for example static methods. How may this help us? Since Sitecore hasn’t made it any easier to do unit testing with a great deal of static classes and methods alike, you will be able to mock or stub them out with the Isolator framework. The following is a simple example for testing functionality for Item.Children, ie. Sitecores Childlist.

Here we have a simple piece of code that, given a specific Template ID, adds an item to a list and returns the list.

public class SectionFactory
{
   public static IEnumerable<Section> CreateCollection(Item columnItem)
   {
      ID sectionTemplateId = new ID(Constants.Templates.Section);
      ChildList children = columnItem.Children;
      List<Item> sections = new List<Section>();
      foreach (Item child in children)
      {
         if (child.TemplateID == sectionTemplateId)
            sections.Add(new Section(child))

      }

     return sections;
 }
}

Since getting the children for a Item invokes more than a few static methods and classes it is impossible to stub the functionality using most mocking frameworks. But with the Isolator Framework it is possible.

[TestFixture]
 public class SectionFactoryTest
 {
 [Test]
 public void SectionFactoryNoChildrenReturnEmpyList()
 {
 Item stub = Isolate.Fake.Instance<Item>();
 ChildList childlist = Isolate.Fake.Instance<ChildList>();
 Isolate.WhenCalled(() => stub.Children).WillReturn(childlist);
 IEnumerable<Section> sectionList = SectionFactory.CreateCollection(stub);
 Assert.AreEqual(new List<Section>(), sectionList);
 }

[Test]
 public void SectionFactoryOneChildReturnEmpyList()
 {
 Item stub = Isolate.Fake.Instance<Item>();
 Item child = Isolate.Fake.Instance<Item>();
 ChildList childlist = Isolate.Fake.Instance<ChildList>();

ID sectionTemplate = new ID(Constants.Templates.Section);
 Isolate.WhenCalled(() => child.TemplateID).WillReturn(sectionTemplate);
 Isolate.WhenCalled(() => childlist).WillReturnCollectionValuesOf(new List<Item>(){child});
 Isolate.WhenCalled(() => stub.Children).WillReturn(childlist);

ID link = new ID(Constants.Fields.Section.Links);
 Isolate.WhenCalled(() => LinkRepository.GetCollection(child, link)).WillReturn(null);

IEnumerable<Section> sectionList = SectionFactory.CreateCollection(stub);
 Section section = sectionList.FirstOrDefault();
 Assert.That(section, Is.Not.Null);

 }

[Test]
 public void SectionFactoryOneValidSectionChildChildReturnEmpyList()
 {
 Item stub = Isolate.Fake.Instance<Item>();
 Item sectionChild = Isolate.Fake.Instance<Item>();
 Item child = Isolate.Fake.Instance<Item>();
 ChildList childlist = Isolate.Fake.Instance<ChildList>();

ID sectionTemplate = new ID(Constants.Templates.Section);
 Isolate.WhenCalled(() => sectionChild.TemplateID).WillReturn(sectionTemplate);
 Isolate.WhenCalled(() => child.TemplateID).WillReturn(null);
 Isolate.WhenCalled(() => childlist).WillReturnCollectionValuesOf(new List<Item>() { child, sectionChild });
 Isolate.WhenCalled(() => stub.Children).WillReturn(childlist);

ID link = new ID(Constants.Fields.Section.Links);
 Isolate.WhenCalled(() => LinkRepository.GetCollection(child, link)).WillReturn(null);

IEnumerable<Section> sectionList = SectionFactory.CreateCollection(stub);

 Assert.AreEqual(1,sectionList.Count());

}
 }

So with the above test we managed to get a 100% test coverage for the code.
And with the Isolator Framework we could even mock out the Sitecore.Context. The Isolater framework is must a have if you want to do unit testing with Sitecore.

Unit testing with Sitecore Item

07/02/2012 7 comments
This post is about how to do unit testing  with Sitecore. Off course this is not easily done, because Sitecore requires a “context” to work.  There are a couple of blogs out there , that explains how to setup either a custom test runner, where you can xecute tests through a webpage. or alternative setup an app_config file so you can work with Sitecore without a httpcontext. Links to both examples are given in bottom of this posts.
Both are really good, but I would categorize testing this way more as Integrations tests.  Since they both depend on external resources  such as  Sitecore Database or httpcontext, it would be more fair to  call them Integration tests. Not that you shouldn’t do integration test you SHOULD, but theese are more “heavy to run”. A unit is the smallest testable part of an application, and a unit test should not depend on the external things such as a database, file system or a like, and more they should run fast, and by a click of a button. I’ve recently read the book  Working Effectively with Legacy Code by Michael Feathers, and it got me thinking there got a be an alternative way to do unit testing with Sitecore.
Finally I’ve recently had a discussion with one of collegeues, the discussion was about how you could do unit test with Sitecore and what you would win by doing so. He claimed that test would be more complex then the code it test, and you wouldn’t gain anything from such tests. This inspired me to do some extensive testing. To prove him wrong I found some code he wrote for a Sitecore solution see below, the code is taken from a navigation component and finds the navigation title fom a item,  navigation title is only shown if the item if   “show in menu”  checkboxfield is checked. Keep in mind the that I picked this example because it is simple.
public class NavigationTitleFactory
 {
 public static string Create(Item item)
 {
 bool showInNavigation = item.GetCheckBoxField(new ID(Constants.Fields.Navigation.Show));

if (!showInNavigation)
 {
 return string.Empty;
 }

string navigationTitle = item.GetStringField(new ID(Constants.Fields.Navigation.Title));

 if (string.IsNullOrEmpty(navigationTitle))
 navigationTitle = item.Name;

 return navigationTitle;
 }
 }

So the first problem that you meet is that a Sitecore Item is difficult  to mock or stub out for test, it would have been really nice if it had  inherit from a interface or had some virtual methods you could override in  a test implementation of with a mocking framwork.
But it turns out you can actually instantiate a Sitecore Item all  you have to do is passing in a lot of nulls “W.E.W.L.C by  M.Feather page 121” see the result below.
public class TestItem : Item
 {
 public TestItem(FieldList fieldList,string itemName="dummy")
 : base(new ID(new Guid()), new ItemData(new ItemDefinition(new ID(new Guid()), itemName, new ID(new Guid()), new ID(new Guid())), Language.Invariant, new Sitecore.Data.Version(1), fieldList), new Database("web"))
 {

}
 }
 With this test item, writing test should now become a lot easier. The itemName is optional because offen you dont care about the name of your item, but for this show case we do.
If you like ,you could pass in addiational arguments to the test item like database name, language, or version. and would still work. The fieldlist arguments should contain the list of field values you gonna use in your test, the item fields can only be referenced with Sitecore id’s as you will see in the example below. Now back to code challenge from my colleague . I want to test the logic for the  navigation title factory liseted above. Here are the test cases.

  1. An Item with show in navigation not checked – it should return an empty string.
  2. An item with show in navigation checked but no navigation titel should return the Name of the Item.
  3. An item with show in navigation checked and with a navigation title, it should return the value of the navigation title.

The test code i listed below should give 100% test coverage of the code.

[TestFixture]
 public class NavigationTitleFactoryTest
 {
 [Test]
 public void CreateItemWithShowInMenuFalseShouldReturnEmptyString()
 {
 FieldList stubFieldList = new FieldList();
 stubFieldList.Add(new ID(Constants.Fields.Navigation.Show),"0");
 stubFieldList.Add(new ID(Constants.Fields.Navigation.Title),"NavigationTitle");
 Item stub = new TestItem(stubFieldList);

 string navigationTitle = NavigationTitleFactory.Create(stub);

 Assert.IsNullOrEmpty(navigationTitle);
 }

[Test]
 public void CreateItemWithShowInMenuTrueNoNavigationTitleShouldReturnItemName()
 {
 FieldList stubFieldList = new FieldList();
 stubFieldList.Add(new ID(Constants.Fields.Navigation.Show), "1");
 stubFieldList.Add(new ID(Constants.Fields.Navigation.Title), "");
 Item stub = new TestItem(stubFieldList,"myItemName");
 string navigationTitle = NavigationTitleFactory.Create(stub);

Assert.AreEqual("myItemName", navigationTitle);
 }

[Test]
 public void CreateItemWithShowInMenuTrueShouldReturnItemNavigationTitle()
 {
 FieldList stubFieldList = new FieldList();
 stubFieldList.Add(new ID(Constants.Fields.Navigation.Show), "1");
 stubFieldList.Add(new ID(Constants.Fields.Navigation.Title), "NavigationTitle");
 Item stub = new TestItem(stubFieldList);
 string navigationTitle = NavigationTitleFactory.Create(stub);

Assert.AreEqual("NavigationTitle",navigationTitle);
 }
 }
This is  a simple example of how to do some unit testing with sitecore. Off course there are some problems with this approach you can’t access more complex attributes and functions of the Sitecore item, for example Children, path, and  functions in general that needs to communicate with Sitecore. But you can test more complex logic that are based in field values alone.
But what did we win with these tests, lets look at it this way. First of i saved time going in to Sitecore creating items setting field values and publishing, agreed it doesn’t take that long but with test like this i can repeat verification of my code by a click of a button. Also Should the Navigation Title Factory really care about which items it get from sitecore ie  the database  pr which context it runs in ? And it would even be pretty simple to add fallback from navigation title to another field value instead of item name and test this new functionality and ensure that the old functionality still works as excepted. And this is precisely what you want from a unit tests.With a unit test you it should be easy to change/modify ,refactor or add functionality to existing code and easily verify that you didn’t brake anything.
I wouldn’t test pure datacontainers, Items that only holds data to be shown. I would prefer to test these with integrationtest.

Allstair Deneys blog on unit testing with
Sitecore using a custom test runnner can be found here.How to change config files to work with Sitecore without a “context” here. By Mike Edwards

Dropbox integration with Sitecore

16/11/2011 Leave a comment
In this post I will make a simple integration from dropbox into the sitecore media library. This is an early release or more a sneak peak at what I hope in time will be a feature rich sitecore open source project/module  “Dropbox integration with Sitecore” With visioning and two sync, and any request that might come in.
So consider this an Alpha-release, I’ve used this Dropbox Context we build in the last blog post “introduction to the dropbox api“. I’ve changend it a little bit so I can be used to make a static .aspx file where you can build the accestoken file. The changend Dropbox context is shown below
 public class DropboxContext
    {
        private OAuthToken _accessToken;

        private string _applicationKey;

        private string _applicationSecret;
        private string _accessTokenPath;

        private OAuthRestClient _apiRestClient;
        private OAuthRestClient _contentiRestClient;

        private string _version = "1.0";

        public DropboxContext(DropBoxSettings settings)
        {
            _applicationKey = settings.ApplicationKey;
            _applicationSecret = settings.ApplicationSecret;
            _accessTokenPath = settings.AccessTokenPath;
            SetupApiRestClient();
            SetupContentRestClient();
        }

        private void SetupApiRestClient()
        {
            _apiRestClient = new OAuthRestClient("https://api.dropbox.com");
            _apiRestClient.ConsumerKey = _applicationKey;
            _apiRestClient.ConsumerSecret = _applicationSecret;
            _apiRestClient.Version = _version;
        }

        private void SetupContentRestClient()
        {
            _contentiRestClient = new OAuthRestClient("https://api-content.dropbox.com");
            _contentiRestClient.ConsumerKey = _applicationKey;
            _contentiRestClient.ConsumerSecret = _applicationSecret;
            _contentiRestClient.Version = _version;
        }

        public OAuthToken GetRequestToken()
        {

            return new OAuthToken(_apiRestClient.GetRequestToken("1/oauth/request_token"));

        }

        public void AuthorizeToken(OAuthToken token)
        {
            Process.Start("https://www.dropbox.com/1/oauth/authorize?oauth_token=" + token.Token);
        }

        public void SetAccesToken()
        {
            OAuthToken accesToken = GetAccesTokenFromStore();
            if (accesToken == null)
            {
                OAuthToken requestToken = GetRequestToken();
                AuthorizeToken(requestToken);
              //  accesToken = UpgradeRequsetTokenToAccesToken(requestToken);
            }

            _accessToken = accesToken;
        }

        public void UpgradeRequsetTokenToAccesToken(OAuthToken requestToken)
        {
            if (requestToken == null)
                return;

            string tokenString = _apiRestClient.GetAccesToken("1/oauth/access_token", requestToken);
            OAuthToken token = new OAuthToken(tokenString);
            StoreAccessToken(tokenString);

        }

        public void StoreAccessToken(string tokenString)
        {
            FileStream fs = File.Open(_accessTokenPath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
            StreamWriter sw = new StreamWriter(fs);
            sw.WriteLine(tokenString);
            sw.Close();
            fs.Close();

        }

        private OAuthToken GetAccesTokenFromStore()
        {
            OAuthToken token = null;
            string tokenString = TokenStringFromFile();
            if (!string.IsNullOrEmpty(tokenString))
                token = new OAuthToken(tokenString);

            return token;
        }

        private string TokenStringFromFile()
        {
            FileStream fs = File.Open(_accessTokenPath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
            StreamReader sw = new StreamReader(fs);

            string tokenString = sw.ReadToEnd();
            sw.Close();
            fs.Close();
            return tokenString;
        }

        public AccountInfo AccountInfo()
        {
            var response = _apiRestClient.QueryServer<AccountInfo>("1/account/info", GetAccesTokenFromStore());
            return response;
        }

        public FileEntry GetMetadata(string path)
        {
            string requestPath = "1/metadata/dropbox";
            if (!String.IsNullOrEmpty(path))
                requestPath = String.Format("{0}{1}", requestPath, path.ToLower());
            var response = _apiRestClient.QueryServer<FileEntry>(requestPath, GetAccesTokenFromStore());
            return response;
        }

        public byte[] GetFile(string path)
        {
            var response = _contentiRestClient.GetStream("1/files/" + path, GetAccesTokenFromStore());
            return response;
        }
    }

Since this i an early release I’ve build this simple .aspx page that provides the user with a link to allow acces to their dropbox and writes the accesToken to a flat file, the file can we then copy to the sitecore data folder for later use.

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>

    Token : <%# Token.Token %> <br />
    Token Secret  <%# Token.TokenSecret %><br />

    <a href="https://www.dropbox.com/1/oauth/authorize?oauth_token=<%# Token.Token %>" target="_blank">Autherize Dropbox</a>

    <asp:Button ID="Button2" Text="Store Token" runat="server" onclick="Button2_Click" />
    </div>
    </form>
</body>
</html>
 public partial class AutherizeDropBox : System.Web.UI.Page
    {
        private OAuthToken token;
        private DropboxContext context;
        protected void Page_Load(object sender, EventArgs e)
        {
            context = new DropboxContext(SitecoreDropboxSettingsRepository.Get());

            if (!IsPostBack)
            {

                DataBind();
            }
        }

        protected void Page_PreRender(object sender, EventArgs e)
        {

        }

        protected  OAuthToken Token
        {
            get
            {
                if (ViewState["token"] != null)
                {
                    token = new OAuthToken();
                    token.Token = ViewState["token"].ToString();
                    token.TokenSecret = ViewState["tokenSecret"].ToString();
                }
                if (token == null)
                {
                    token = context.GetRequestToken();
                    ViewState["token"] = token.Token;
                    ViewState["tokenSecret"] = token.TokenSecret;
                }
                return token;
            }
        }

        protected void Button2_Click(object sender, EventArgs e)
        {

                OAuthToken t = Token;
                context.UpgradeRequsetTokenToAccesToken(t);

        }
    }

For dropbox context to work we need a valid dropboxcontext setting datacontainer. Information for this i stored as settings in the web.config

   <setting name="ApplicationKey" value="YOURAPPLIKATIONKEY" />
   <setting name="ApplicationSecret" value="YOURAPPLIKATIONSECRET" />
   <setting name="AccessTokenPath" value="PATHTOYOUFILETHATCONTAINSACCESSTOKEN" />

For this simple version of dropbox syncing I’ve build a simple sitecore task to run a “Download all service” from dropbox. It’s start by deleting all old the previously downloaded content from dropbox and the download from all content from dropbox root ie. “dropbox/”.

public class DropboxDownloadTask
    {
        CreateMediaItemService _createMediaItemService;
        public void Execute()
        {
            _createMediaItemService = new CreateMediaItemService();
            GetRoot();
        }

        private void GetRoot()
        {
            MetatadataRepository metatadataRepository = new MetatadataRepository();
            FileEntry root = metatadataRepository.Get();
            IterateOverChildren(root);
        }

        private void  IterateOverChildren(FileEntry folderEntry)
        {

            foreach (FileEntry entry in folderEntry.Contents)
            {
                if (entry.Is_Dir)
                  IterateOverChildren(GetFileEntry(entry));
                else
                  _createMediaItemService.CreateMediaItem(entry);
            }
        }

        private FileEntry GetFileEntry(FileEntry entry)
        {
            MetatadataRepository metatadataRepository = new MetatadataRepository();
            return metatadataRepository.Get(entry);
        }
    }

The task uses some file- and metadata-repositories that communicate with dropboxcontext and a simple settings repository.

 public static class SitecoreDropboxSettingsRepository
    {
        public static DropBoxSettings Get()
        {
            DropBoxSettings settings = new DropBoxSettings();
            settings.ApplicationKey = Settings.GetSetting("ApplicationKey");
            settings.ApplicationSecret = Settings.GetSetting("ApplicationSecret");
            string path = Settings.GetSetting("AccessTokenPath");
            settings.AccessTokenPath = HttpContext.Current.Server.MapPath(path);
            return settings;
        }

    }
public class MetatadataRepository
    {
        private DropboxContext _context;
        public MetatadataRepository()
        {

            _context = new DropboxContext(SitecoreDropboxSettingsRepository.Get());
        }

        public FileEntry Get()
        {
            return _context.GetMetadata(String.Empty);
        }

        public FileEntry Get(FileEntry entry)
        {
          return _context.GetMetadata(entry.Path);
        }
    }
 public class FileDataRepository
    {
        private  DropboxContext _context;
        public FileDataRepository()
        {

            _context = new DropboxContext(SitecoreDropboxSettingsRepository.Get());
        }

        public  byte[] Get(FileEntry entry)
        {
          return _context.GetFile(entry.Root+entry.Path);
        }
    }

Finally the task have a simple service that is responsible for adding a media stream to the sitecore media library.

 public class CreateMediaItemService
    {

        private MediaCreator _creator;
        private MediaCreatorOptions _mediaOptions;

        public void CreateMediaItem(FileEntry folderName)
        {
                 using (new SecurityDisabler())
                          AddStreamToMediaLibrary(folderName);

        }

        private void AddStreamToMediaLibrary(FileEntry folderName)
        {
             if(folderName.Parent == "dropbox")
                 SetMediaOptions(folderName.Parent, folderName.Name);
            else
                SetMediaOptions(String.Format("dropbox/{0}",folderName.Parent), folderName.Name);
            _creator = new MediaCreator();

            FileDataRepository fileDataRepository = new FileDataRepository();
            byte[] bytes = fileDataRepository.Get(folderName);

            MemoryStream theMemStream = new MemoryStream();

            theMemStream.Write(bytes, 0, bytes.Length);
            try
            {

                    _creator.CreateFromStream(theMemStream, folderName.Name, _mediaOptions);
            }
            catch (Exception e)
            {
                throw new Exception(folderName.Name, e);
            }
        }

        private void SetMediaOptions(string sitecorePath, string itemName)
        {
            _mediaOptions = new MediaCreatorOptions();
            _mediaOptions.FileBased = false;
            _mediaOptions.IncludeExtensionInItemName = false;
            _mediaOptions.KeepExisting = false;
            _mediaOptions.Versioned = false;
            _mediaOptions.Destination = String.Format("/sitecore/media library/{0}/{1}", sitecorePath, ItemUtil.ProposeValidItemName(itemName));
            _mediaOptions.Database = Database.GetDatabase("master");

        }

    }

Offcourse there are room for improvement and the code could use some cleaning up to is more “Clean Code” Uncle Bob.  You can download the source code for this project at Pentia. I’m more then interrested in hearing what features should be on the todo-list so let me know.

Categories: .Net, C#, Sitecore 6 Tags: , ,

Running Scheduledtasks with the jobmanager in Sitecore

06/10/2011 5 comments

I’ve recently had the need to execute a scheduled task. The simple way would be to fetch the scheduled task item from Sitecore an instantiate it as a ScheduledItem type found in Sitecore kernel namespace, and the Call the execute method found in the class.


Item item = database.GetItem(taskId);
 ScheduleItem scheduleItem = new ScheduleItem(item);
 scheduleItem.Execute();

This of course Will run the task in the context of the current user. This could lead to some minor issues where the user running task doesn’t have the same rights as the Sitecore build JobManager.The solution is to execute the task via the JobManager, which also is used by scheduler. This is done by providing simple JobOptions. The example belows instantiate a new MyTask and runs the execute method.

 JobOptions options = new JobOptions("Job Runner", "schedule", "scheduler", new MyTask(), "Execute", new object[] { scheduleItem.Items,  scheduleItem.CommandItem, scheduleItem });
 options.SiteName = "scheduler";
 JobManager.Start(options).WaitHandle.WaitOne();

The last parameter passed in as a new object is the parameter list passed on to the Execute Method.

 

Categories: .Net, C#, Sitecore 6 Tags:

Interface Templates

14/12/2010 1 comment

For this post I will look into a simple technique for building easy to extend or reuse features in Sitecore solutions. This simple architectural concept will, for the rest of this blog post, be refereed to as an Interface template. This idea comes from my work in Pentia, where we build website that should be easy to maintain and reuse or extend implemented features. Some of you, that where at Dreamcore 2010, might have been so lucky, that they saw my brilliant colleague Thomas Eldblom in cooperation with Dan Algrove from Hedgehog development, in a session on how to build maintainable Sitecore solutions. You can read more great thoughts from Thomas at his blog Molten Core. Some of the key points from this session and my daily work with Thomas is the base for the blog post, and I will not take any credit for being the inventor for the interface template concept. I’ve just used it a lot recently to make it easier for new templates/pagetypes to reuse implemented code features, and thought I would make for a good post.

We all know that code degrades as time goes by, and that the more “quick fixes – Hey it works don’t change it” where used during implementation the faster the code degrades, and oh yes even the best code will degrade over time, but maybe not on a quite as steep slope.

We have all tried to write simple parts of a website, but even the simple parts may lead to complicated code. Let’s take an simple example as a Menu or Breadcrumb path. Most websites have some sorts of navigation and likewise have breadcrumbs. So to start with the Customer have a simple website where the breadcrumb starts at the current item and works it’s way up the content tree to the root of the site i.e. at the front page. It would be an fairly easy task to implement code that can build this sort of breadcrumb path. This could be done either in an simple xslt or maybe by implementing some sort of breadcrumb provider in c# class.We will traverse every ancestor until the following condition is meet. This could for example be done using a Sitecore query a look something like this.
Note only pseudo code will be used in this post.

ancestor-or-self::item[@tid = ‘frontpage’]


This will work great and is a simple and acceptable solution. So what is the problem ? After an year your customer returns with an wish that they want functionality restart the navigation “breadcrumb” for some node in content tree, this could be anywhere. So the root item ie the first link in the breadcrumbs path is no longer the front page but could potentially be any node.
The question now is, how do you determine where the root of the breadcrumbs path is? You can no longer settle for a simple test against the “frontpage”-template or could we ? A simple way would either be to make an new front page at the given point in the content tree, but it seems stupid that the customer will have to use a front page for this task and might be confusing as well. So okay no problem we will just make an new template, or choose a page type and add the template to the query condition and it might look something like this.


ancestor-or-self;;item[@tid = ‘frontpage’ or @tid = ‘subfrontpage’]


Great it works and was pretty simple too, but now we started on the slippery slope.The key point to notice here is that for this to work we had to make changes to the code.
What will happen the next time the customer returns with a wish to reset the navigation for the all ready existing document template or any template for that matter. And even more the document template should have a check box that enables the reset features. No problem we simply add the check box to the document template and extend the select query.

ancestor-or-self::item[@tid = ‘frontpage’ or @tid = ‘subfrontpage’ or (@tid = ‘document’ and resetmenuField = ‘1’ )]


Wow it worked but now the query is becoming quite complex and keep in mind this is an simple example. And soon the customer will be back with a note that the shoppingItemTemplate needs to reset the breadcrumbs as well and with the same condition as the document template. As the years go bye the query is becoming more and more unreadable.

But how can we avoid this slippery slope ?
We could build a new template and let it inherit from the “frontpage”-template, new template will then have the same fields as the front page. This might lead to a more confused customer because a simple document template will have the same field as front page without using them.

So now we will have a look at an interface template. This could be a simple template with or without fields. The sole purpose of the this interface template, is to define “some functionality”
for another templates that inherits from it. Lets revisit our example with the breadcrumbs.
First we define this template without any fields lets call it _isMenuRoot. And let the frontpage template inherit from this. Now we can write the first query as this.

ancestor-or-self::item[ is derived from template _isMenuRoot]


The query will return the front page as the menu root just as we wanted. Perfect now the beauty of the simple template comes into play. Now the customer wants a simple sub front page that can reset the breadcrumbs. This should be a fairly easy task. Build your normal sub front page a let it inherit from the _isMenuRoot template now look at the query
ancestor-or-self::item[ is derived from template _isMenuRoot]

Wow it’s the same and we didn’t have to change anything in the code, great!
Now let’s extend the template so it has a simple checkbox field that allows back end users to enable/disable the reset functionality. This is done simply by extending the _isMenuRoot Template with an checkbox field. Of course this demands for a simple change to the code, so we change the query accordingly so it reads.
item[ is derived from template _menuRoot and reset menu = 1]
Of course this will force back end users to actively enable/disable this feature for existing items. But this can be done fairly easy on templates that inherit from the interface template. For example, you might want the front page to always have this feature enabled, and as default disabled for a document page or alike. You can achieve this but setting the checkbox value in standard values for all the templates the inherits from our _isMenuRoot interface template. Thank you sitecore for standard values. So without changing any code, future templates can nowgain access to this Menu root functionality by inheriting from our _isMenuRoot template.

The above helped me on project i recently worked on where I had an if statements that had to test for 8 different templates. Everytime someone came to develop new templates/page types they had to go in and actively add another clause to the if statement, if they wanted to acces the code features provided after in if statement. This were change by the use of an interface template so the if statement checked for an interface template instead of 8 different templates .And of course all the template previously in the if statement, now simply had to inherit from the newly created interface template. New templates/pages, that want to reuse the same functionality, simply had to inherit from the interface template,without the need for changing any code at all.

Categories: Sitecore 6 Tags:

Usersessions in sitecore (logout users from backend)

07/06/2010 Leave a comment

The number of usersession that is possible to have open in sitecore depend onj the licens, which is fair enough. But the task for an administrator to end hanging usersession seems somewhat headless. The administrator have to logout, to get a list of active user sessions, and then choose to end hanging or not used sessions. This might be fine in minor solutions but I think this might pose a problem in solutions where a user with the “Is Adminstrator” flag is set to true can be hard to find. He might be working in different office or just generally be hard to get a hold of. It doesn’t require you have the “Is administrator” flag to end session but it is required to get the list of active sessions. You don’t even have to be logged in to sitecore to end active session, if presented with the option to end active session anonymous users could do so. My idea is to help local administrators with functionality to end session with out leaving the backend, or having to log out or anything like that. Since sitecore is one big tool box I will build it as shortcut in the tray of sitecore.
Okay so we start, all our work is done in the core database and it only contains minimal coding. We start with making the tray icon. In the core database we go to content/applications/Desktop/tray and add a item I will call it UserSessions the Data section of the item find the click attribute and add the command name, this will also have to go in the command file we get back to that. I also and icon for the tray I have choosen the standard user icon.
userhelper1
Now with the tray icon shortcut in place you should be able to see it in th start bar just save the item and hit F5

userhelper2

Now we will add the command to the App_Config/Commands.config add the following line

<command name=”Tray:UserSessionViewer” type=”UserHelper.UserhelperTrayCommand,UserHelper” />
I will add this little user feature helper as an application so we need to add an application and layout for the application in the core database. We start with adding the layout “core database” go to sitecore/layout/applications and add a new layout in the data section in path write the physical path to layout you will create in the next step.

suerhelper3
Next we will add the application again this is done in the core database go to sitecore/content/Applications/ and add a new application pull on the the layout we just created make sure you can see it in the layout section for the item. Also note the Id for your application, we will have to use it in the code part.
userhelper4
Okay now we are done with the configuration in sitecore now to code part.
First we will start we the Tray click command which is pretty simple note that is hardcode the id for our application and the database I getting the item from.

Now we have something that handle the click on the Tray command no we will code the popup or application window. First the frontend code for the application. The frontend code shows when the user session started and when the last request was made info need to take into account which user sessions you should end


<div>
<divLiteral">System Time</span>
<span><%# SystemTime %></span>
</div>
<div>
<asp:Repeater ID="taskRepeater" runat="server" DataSource="<%# ScheduleItems %>"
OnItemCommand="taskRepeater_ItemCommand">
<HeaderTemplate>
<table>
<thead>
<tr>
<td>
*
</td>
<td>
Task name
</td>
<td>
Is Due
</td>
<td>
Last run
</td>
<td>
Next run
</td>
<td>
Execute Task
</td>
</tr>
</thead>
<tbody>
</HeaderTemplate>
<ItemTemplate>
<treven" : "odd"   %>">
<td>
<img src="/sitecore/shell/Themes/Standard/<%#((ScheduleItem)Container.DataItem).Icon%>"
alt="<%# ((ScheduleItem)Container.DataItem).DisplayName %>" />
</td>
<td>
<%# ((ScheduleItem)Container.DataItem).DisplayName%>
</td>
<td>
<%# ((ScheduleItem)Container.DataItem).IsDue%>
</td>
<td>
<%# ((ScheduleItem)Container.DataItem).LastRun.ToString("HH:mm dd/MM-yyyy")%>
</td>
<td>
<%# ((ScheduleItem)Container.DataItem).NextRun.ToString("HH:mm dd/MM-yyyy")%>
</td>
<td>
<asp:LinkButton ID="LinkButton1" Text="Execute" CssClass="SortButton" runat="server"
CommandName="Execute" CommandArgument="<%# ((ScheduleItem)Container.DataItem).ID %>" />
</td>
</tr>
</tbody>
</ItemTemplate>
<FooterTemplate>
</tbody> </table>
</FooterTemplate>
</asp:Repeater>
</div>
</div>

Now to the code behind, I am only using standard sitecore functionality nothing fancy here other then the click event for the repeater that end the selected user session.


using Sitecore.Data;
using Sitecore.Security.Accounts;
using Sitecore.Web.Authentication;

namespace Userhelper.Presentation
{
public partial class UserSessionsView : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
DataBind();
}

public IEnumerable UserSessions
{
get
{
return DomainAccessGuard.Sessions;
}
}


protected void userRepeater_ItemCommand(object source, RepeaterCommandEventArgs e)
{
string id = e.CommandArgument.ToString();
switch (e.CommandName)
{
case "Execute":
EndSession(id);
break;
}
}

private void EndSession(string sessionID)
{
DomainAccessGuard.Kick(sessionID);
}

}
}

This is how I looks when we done

userhelperlast

The styling part is left for you. With this little userhelper you can set security on the tray icon so local administrator can see the tray icon and all other users have denied read access so only users with elevate security settings can end user sessions.

System Alert in sitecore

27/04/2010 1 comment

When working with large Sitecore implementation and customers that have a lot of editors maintaining the content of their website, often makes updating the system becomes a hurdle. Because editors may or may not work in on the same floor or even the same building, so even if you have an contact person who’s responsibility is to notify editor about the downtime, it is possible that one or more editors haven’t heart about the downtime and haven’t save the what they where doing the second the system is taken offline, a valuable work may have been lost.
So I thought what would be more smart the implementing a System alert/ notifier system that alert the users about the forthcoming downtime. This Alert could as in this example be a simple javascript alert.
My solution is build around simple Javascript calling a webservice which looks for alert placed inside sitecore and display them as a simple javascript alert. Yes some may argue that I have some hardcoded path string and what have we not, but it is left to you to move these to fx. The web.config. Even more this solution I maybe a little over the edge when looking at the implementation, but I se so many usages for this so I went ALL-IN as implemented with interface and using a provider model. The solution is build and tested against a Sitecore 6.2, but nothing wrong with using on other Sitecore versions.
But here goes first the javascript since Sitecore editors have three different editors Page Edit, Content Editor and the Desktop. So we need to include the javascript in three different files, and because of the we need to ensure that the file is only loaded once so logged into the Desktop and opening the content editor doesn’t give two warnings, hence the cookie check. Now to javascript, it’s all simple stuff.
The javascript should be include in these three files
Webedit:
sitecore\shell\Applications\WebEdit\WebEditRibbon.aspx
Content Editor:
sitecore\shell\Applications\Content Manager\Default.aspx
Desktop:
sitecore\shell\Applications\Startbar\Startbar.xml


/* Function for calling the webservice                             */
function callWebservice() {
var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
xmlhttp.open("GET", "/Components/SystemNotifier/AjaxWebservice/SystemNotifier.asmx/GetAlerts", false);
xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlhttp.send();

if (xmlhttp.status == 200) {
var str = xmlhttp.responseXml.getElementsByTagName("string").item(0).text;
return str;
}
}

/* function that Get system alerts */
/* update timer by calling the webservice    */
function GetSystemAlerts() {
var alertString = callWebservice();
if (alertString != "") {
alert(alertString);
//increase time to next call so we dont get same alert twice
setTimeout("GetSystemAlerts()", 125000);
}
else {
setTimeout("GetSystemAlerts()", 60000);
}
}

var cookieName = "SitecoreSystemNotifier";

function writeCookie() {
document.cookie = cookieName;
}

function cookieExists()
{

if (document.cookie.length >0 )
{
var offset = document.cookie.indexOf(cookieName);
if (offset != -1)
return true;
return false;
}
return false;
}

function init(){
if(!cookieExists()){
writeCookie();
//SetTimeout in ms
setTimeout("GetSystemAlerts()", 60000);
}
}

init();

Okay now that we have the javascript we need the webservice to be called. It’s fairly simple when using the Provider.


namespace SystemNotifier.AjaxWebservice
{
///
/// Summary description for SystemNotifier
///
[WebService(Namespace = "http://pentia.dk/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line.
[System.Web.Script.Services.ScriptService]
public class SystemNotifier : WebService
{

[WebMethod]
public string GetAlerts()
{
ISystemAlert alert = AlertProvider.NextAlert;
if (alert != null)
return alert.Text;
return "";
}

private SystemAlertProvider _provider;
private SystemAlertProvider AlertProvider
{
get
{
if (_provider == null)
_provider = new SystemAlertProvider();
return _provider;
}
}

}
}

And now to the provider implementation.


public class SystemAlertProvider
{
private IEnumerable _alerts;
public IEnumerable Alerts
{
get {
if(_alerts == null)
_alerts = GetAlertsFromSitecore();
return _alerts;
}
}

private TimeSpan timespan = new TimeSpan(0, 1, 0);
private IEnumerable GetAlertsFromSitecore()
{
ChildList childList = AlertRootItem.Children;
foreach(Item child in childList)
{
ISystemAlert alertItem = new SystemAlert(child);
if(alertItem.AlertTime > DateTime.Now.Subtract(timespan))
yield return alertItem;
}
}

private const string sitecoreRootPath = "/sitecore/system/SystemAlertNotifier";
private Item _rootItem;
private Item AlertRootItem
{
get
{
if(_rootItem == null)
_rootItem = Database.GetItem(sitecoreRootPath);
return _rootItem;
}
}

private const string _databaseName = "master";
private Database Database
{
get
{
return Database.GetDatabase(_databaseName);
}
}
public ISystemAlert NextAlert
{
get
{
if(Alerts.Count() > 0)
return Alerts.OrderBy(w => w.AlertTime).First();
return null;
}
}
}

And finally the Alert interface and implementation of the same.

Inteface


public interface ISystemAlert
{
DateTime AlertTime { get; }
String Text { get; }
}

Implementaion


public class SystemAlert : ISystemAlert
{
public SystemAlert(Item item)
{
Item = item;
}

private Item Item
{
get;
set;
}

private const string _alertTimeField = "SystemAlert_AlertTime";
public DateTime AlertTime
{
get
{

DateField dateField = Item.Fields[_alertTimeField];
return dateField.DateTime;
}
}

private const string _textField = "SystemAlert_Text";
public string Text
{
get { return Item[_textField]; }
}
}

Now we got all the code working so now we need to have someway to get the info, let’s use a sitecore item. So here is a snapshot of the how my sitecore item looks.

So this is pretty much everything you need to have a system alert system up and running inside sitecore. Remember to edit hardcode root path to system alert root folder.
You can download the project in the download section link here.

And hope you can see the posiblities in this solution or implementaion, you could scheduled downtown and have email alert, downtime calendar and much much more hope you enjoy,

Categories: C#, Javascript, Sitecore 6 Tags: