fredag den 13. december 2013

Server.MapPath inside Sitecore tasks

From time to time, when developing tasks for Sitecore, you need to map a virtual path to a physical file in the file system.

The way to do this, is to call the function MapPath on the Server object located on the current HttpContext object.

This will not work in tasks, since there is no HttpContext (since we are running on a separate thread) - so we have to come up with some other way to map this, in case the HttpContext is missing.

This is the solution we have come up with:

public static string MapFilePath(string filePath)
{
    if (string.IsNullOrEmpty(filePath))
    {
        throw new ArgumentException("filePath must not be empty", "filePath");
    }

    if (HttpContext.Current != null)
    {
        return HttpContext.Current.Server.MapPath(filePath);
    }

    return HttpRuntime.AppDomainAppPath + filePath.Replace('/', '\\');
}

If there is a HttpContext, we just do like we normally do, since that is prettier - but if there is no HttpContext, we have to do some string manipulation to get the desired result.

Since the URL has to use back slashes instead of forward slashes, we have to replace those.

This results in usable URL's being returned, no matter if you are in a task or not :-)

WCF, Sitecore, JSON and .NET 4.0 vs. .NET 4.5.1

I have been having an interresting problem the last few days, that took quite a while to debug.

This doesn't only relate to Sitecore, since every WCF service created in .NET that is consumed be javascript can run into this issue.

A little while ago, we upgraded our development machines to Windows 8.1, which includes .NET 4.5.1 - which seems to make some small, but important changes to WCF.

It turns out, that Microsoft has changed how objects gets wrapped, when they are outputted as JSON.

In .NET 4.0, it seems like objects was getting wrapped in an element called "d" - this has been removed in .NET 4.5.1 (it's gone on our machines at least).

The server the website is running on, only had .NET 4.0 installed, so we got permission to upgrade it to .NET 4.5.1 , which after a little too long spinning at "Preparing to configure updates" in Windows Update, we had the server up and running with .NET 4.5.1, which fixed the problem, so the JSON nolonger is wrapped in the "d" element.

mandag den 2. december 2013

Upgrading Sitecore, Integer fields and the page editor

Just had an interresting issue with a customers solution, that we upgraded last week.

It seems, that if you have a template with a field, that is of the field type "Integer", something weird can happen.

To trigger this, you have to have three things:

  1. The field on the current page you are at.
  2. You have to be inside the page editor
  3. The field must be rendered using a <sc:text> tag.
If you do this, and then try and save the page without entering anything into the field (it might be optional for the user to enter something here, which was the case for our customer), you would get an error saying: " '' is not a valid integer.".

This happens, because Sitecore sometime between 6.5-111230 and the 6.6-130529 changed now the save event works, so it will give an error on Integer fields, even if there is no validators added to the field.

The solution we went for, was setting the field to have the default value of 0, which makes sure the field never is empty, which solved the problem.

mandag den 25. november 2013

To assert or not to assert, thats a good question

It's no secret among people that work with me, that I really don't like the way Sitecore is misusing the Assert word.

The problem is, that Sitecore's Assert class, and the standard .NET Debug.Assert works fundamentally different, which can become a big problem, if people starts to learn the way Sitecore uses it, and then later on gets to work with other things than Sitecore.

So, when you in Sitecore have a function, that takes a parameter, and validates it like this:

public string IsItemMediaItem(Item item)
{
    Assert.ArgumentNotNull(item);

    return item.Paths.IsMediaItem;
}

I know this is a bad example, but it illustrates the problem, which is, that now we are using Assert to validate input parameters, which works just fine... in Sitecore.

Now lets say that the same code was written without Sitecore (Ignore the classes is called the same):

public string IsItemMediaItem(Item item)
{
    Debug.Assert(item != null);

    return item.Paths.IsMediaItem;
}

Now the problem starts to show, since normal .NET Assert only gets executed in Debug builds, so when you build your code as a Release build, the Debug.Assert line is removed, which now means your code no longer validates it's input!

It gets worse, once you start using this with input from clients, since you should always validate this.

That's why I always recommend people to do input validation this way, since that doesn't give them and other people bad habits.

This, on the other hand, is the "right" way to it:

public string IsItemMediaItem(Item item)
{
    if (item == null)
    {
        throw new ArgumentNullException("item", "item must not be null");
    }

    return item.Paths.IsMediaItem;
}

This works every time, on every system.

So Sitecore, if you read this, please mark your entire Assert class as obsolete, and start teaching people how to do real input validation, instead of something that will make them write bad code everywhere else.

Ensuring a child item exists

Sometimes, we need to get an item if it exists, and if it doesn't, create it instead.

This normally happens when we have something that needs to be placed inside folders, to ensure there aren't too many items in the same folder.

Lets say, that we have a task, that imports employees from an external system, and for each of these create an item - this can quickly result in problems if there are more than a few hundred items.

The normal way to solve this, is to divide the users into folders based on the first character in their name (or what else is used to differentiate them).

If we take the employee "A1D2", we would create a structure like this, for the item: "Employees/A/1/A1D2" .

Here comes the problem - to create the item, we need to make sure the folder "1" exists, but to ensure that, we need to ensure that the folder "A" exists, and so forth..

To do this, we have created a helper function, that returns a child item if it exists, and if it doesn't, we create the item, and return the new item - here is the code for the function:

public static Item EnsureChildItem(Item parentItem, string childName, TemplateID templateID, Action<Item> createdAction)
{
    if (parentItem == null)
    {
        throw new ArgumentNullException("parentItem", "parentItem must not be null");
    }

    if (string.IsNullOrEmpty(childName))
    {
        throw new ArgumentException("childName must not be empty", "childName");
    }

    string itemNameError = ItemUtil.GetItemNameError(childName);

    if (!string.IsNullOrEmpty(itemNameError))
    {
        throw new ArgumentException("childName is invalid - message: " + itemNameError, "childName");
    }

    Item childItem = parentItem.Children[childName];

    if (childItem != null)
    {
        return childItem;
    }

    childItem = parentItem.Add(childName, templateID);

    if (createdAction != null)
    {
        createdAction(childItem);
    }

    return childItem;
}

First we check if the parameters is valid, and if they are, we check if the item exist - if it does, we just return it.

If it doesn't we create it, and run the passed Action if it is defined - this allows the called to pass a function to be called if the item was created.
This is pretty clever, if I should say so, since it allows you to do anything to the new item, such as publish it (which is what we would normally do), so you're always sure the item is published.

It's a pretty small function, but it really makes it easy, to be able to do this:

string employeeName = GetEmployeeName();
Item employeeFolder = GetEmployeeFolder();

// Get the first level folder.
employeeFolder = HelperFunctions.EnsureChildItem(employeeFolder, employeeName[0].ToString(), new TemplateID(TEMPLATE_ID), item => HandleNewItem(item));

// Get the second level folder.
employeeFolder = HelperFunctions.EnsureChildItem(employeeFolder, employeeName[1].ToString(), new TemplateID(TEMPLATE_ID), item => HandleNewItem(item));

This way, we know that employeeFolder is containing the folder where the new employee item is to be created, and we know for sure, that it exists.

Nice :-)

tirsdag den 19. november 2013

Creating timebased tasks in Sitecore

From time to time, our customers would like to run taks at a certain time each day.

To be able to do this, we have created a new concept, called Timebased Tasks, which works like normal tasks, but instead of a frequency has a timestamp defining when the task should run.

To do this, you need 6 things, some "dummy" items to define the days, a template for the tasks, a folder for the tasks, two classes and an include file adding the agent.

Dummy items:

Lets start with the dummy items, create this folder in the master database: "/sitecore/system/Modules/Timebased Tasks".

Then create 7 items below it (based on the "Standard Template" template, since we don't need any special fields on them), and call them: "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" and "Sunday".

Template:

We need a template to define a Timebased Task - create a new template called "Timebased Task" somewhere where it makes sense to you, and add the following fields to it (all should be marked shared):

Name: Type: Source:
Command Droplink /sitecore/system/tasks/commands
Items Treelist /sitecore
Start Date Date  
End Date Date  
Frequency Checklist /sitecore/system/Modules/Timebased Tasks
Time Of Day Single-Line Text  
Asynchronous Checkbox  
Auto Remove Checkbox  
Last Run Datetime  

Folder:

Now we have to create the folder where the tasks is to be placed, so create this folder: "/sitecore/system/Tasks/Timebased Schedules"

Once the folder has been created, replace the insert options on the folder, so you can create items based on the template you just created.

The code:

Finally we get to the fun part - the code :-)

First we need a class called TimebasedScheduleItem - this class represents the schedule item, and extends the normal schedule item (to be allowed to be passed to a task, as if it was a normal ScheduleItem object):

using System;
using System.Globalization;
using System.Linq;

using Sitecore;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Jobs;
using Sitecore.SecurityModel;
using Sitecore.Tasks;

public class TimebasedScheduleItem : ScheduleItem
{
    public TimebasedScheduleItem(Item innerItem) : base(innerItem)
    {
    }

    public new bool Asynchronous
    {
        get
        {
            return InnerItem["Asynchronous"] == "1";
        }
    }

    public new bool AutoRemove
    {
        get
        {
            return InnerItem["Auto Remove"] == "1";
        }
    }

    public new CommandItem CommandItem
    {
        get
        {
            ReferenceField field = InnerItem.Fields["Command"];

            if (field == null || field.TargetItem == null)
            {
                return null;
            }

            return field.TargetItem;
        }
    }

    public new bool Expired
    {
        get
        {
            // First we check if the end date is before today (meaning that we are expired).
            if (EndDate < DateTime.Today)
            {
                // It is, so we return true.
                return true;
            }

            // If the end date is today.
            if (EndDate == DateTime.Today)
            {
                TimeSpan timeOfDay;

                // We try and parse the time of day.
                if (!TryParseTimeOfDay(out timeOfDay))
                {
                    // We failed, so we considder the task expired.
                    return true;
                }

                // Then we take the end date, and adds the time of day to it.
                // This is because the last possible time it can run would be the time of day on the end date.
                DateTime totalEndDate = EndDate.Add(timeOfDay);

                // Finally we check if the total end date is before now, and that the task has been run today.
                // This is to make sure that task gets run the last day if needed.
                return totalEndDate < DateTime.Now && LastRun.Date == DateTime.Today;
            }

            // If we gets here, end date is in the future, so we are not expired yet.
            return false;
        }
    }

    public new bool IsCompleted
    {
        get
        {
            // A task is completed (never going to run again) when the NextRun is the minimum possible DateTime value.
            return NextRun == DateTime.MinValue;
        }
    }

    public new bool IsDue
    {
        get
        {
            // If the task has been run today already, we return false.
            if (LastRun.Date == DateTime.Today)
            {
                return false;
            }

            // If the start date is in the future, or the task is expired, we return false.
            if (StartDate > DateTime.Today || Expired)
            {
                return false;
            }

            // If the day of the week it is today is not checked in the checklist, we return false;
            if (!Frequency.Contains(DateTime.Now.DayOfWeek.ToString()))
            {
                return false;
            }

            TimeSpan timeOfDay;

            // Then finally we try and parse the time of day field, and figures out if it is before the current time.
            // This makes sure that task does not get run too early - since if we get here, the task is supposed to run today.
            return TryParseTimeOfDay(out timeOfDay) && timeOfDay <= DateTime.Now.TimeOfDay;
        }
    }

    public new Item[] Items
    {
        get
        {
            MultilistField field = InnerItem.Fields["Items"];

            return (from id in field.TargetIDs
                    select InnerItem.Database.GetItem(id)).ToArray();
        }
    }

    public new DateTime LastRun
    {
        get
        {
            DateField field = InnerItem.Fields["Last Run"];

            return field.Value.Length == 0 ? DateTime.MinValue : field.DateTime;
        }

        set
        {
            using (new SecurityDisabler())
            {
                InnerItem.Editing.BeginEdit();
                InnerItem["Last Run"] = DateUtil.ToIsoDate(value);
                InnerItem.Editing.EndEdit();
            }
        }
    }

    public new DateTime NextRun
    {
        get
        {
            TimeSpan timeOfDay;

            // First we test if the task is due, in which case the next run is now, so we return DateTime.Now .
            if (IsDue)
            {
                return DateTime.Now;
            }

            // If the task is expired, no days is selected in the checklist or we are unable to parse the time of day field.
            // We return DateTime.MinValue, which indicates that there is no next run.
            if (Expired || Frequency.Length == 0 || !TryParseTimeOfDay(out timeOfDay))
            {
                return DateTime.MinValue;
            }

            // If the checklist contains the day of the week that today is, and the time of day is before now, we take today,
            // and add the time of day to it, and return it.
            if (Frequency.Contains(DateTime.Now.DayOfWeek.ToString()) && timeOfDay >= DateTime.Now.TimeOfDay)
            {
                return DateTime.Today.Add(timeOfDay);
            }

            DateTime tmpDate = DateTime.Today.Add(timeOfDay);

            // If we get here, the task is not expired, there is at least one day selected in the checklist, so we try adding a day to the
            // temporary date variable above, untill the day of the week that it represents is checked in the checklist - thats the next run :-)
            // The reason for 7 is because there is 7 days in a week, so we should in theory never hit the 7th run, since that would be back to
            // the weekday of today.
            for (int i = 0; i < 7; i++)
            {
                tmpDate = tmpDate.AddDays(1);

                if (Frequency.Contains(tmpDate.DayOfWeek.ToString()) && tmpDate.Date <= EndDate)
                {
                    return tmpDate;
                }
            }

            // If we get here, the task is not able to be run, so we just return DateTime.MinValue to make sure it never gets run.
            return DateTime.MinValue;
        }
    }

    public new ScheduleField Schedule
    {
        get
        {
            // We don't use the ScheduleField here, since we have our own field for this, so we overwrite it and return null instead.
            // This also prevents the normal DatabaseAgent from working with the TimebasedScheduleItem objects (like it is ever to get its hands on one).
            return null;
        }
    }

    private DateTime StartDate
    {
        get
        {
            DateField field = InnerItem.Fields["Start Date"];

            if (field == null)
            {
                return DateTime.MinValue;
            }

            return field.Value.Length == 0 ? DateTime.MinValue : field.DateTime.Date;
        }
    }

    private DateTime EndDate
    {
        get
        {
            DateField field = InnerItem.Fields["End Date"];

            if (field == null)
            {
                return DateTime.MinValue;
            }

            return field.Value.Length == 0 ? DateTime.MinValue : field.DateTime.Date;
        }
    }

    private string[] Frequency
    {
        get
        {
            MultilistField field = InnerItem.Fields["Frequency"];

            if (field == null)
            {
                return new string[0];
            }

            // We take all the selected weekdays, and return the item names of those that are selected.
            return (from id in field.TargetIDs
                    select InnerItem.Database.GetItem(id).Name).ToArray();
        }
    }

    public new void Execute()
    {
        try
        {
            // If the commanditem is null, we cannot run the task.
            if (CommandItem == null)
            {
                return;
            }

            // We execute the task (async or sync depending on what is checked.)
            if (Asynchronous)
            {
                ExecuteAsynchronously(CommandItem);
            }
            else
            {
                ExecuteSynchronously(CommandItem);
            }

            // Then we set the LastRun to now.
            LastRun = DateTime.Now;

            // And if the task is complete (not to ever run again), and is set to be auto removed, we remove it.
            if (IsCompleted && AutoRemove)
            {
                Log.Info("Schedule is completed. Auto removing schedule item: " + Name, this);
                InnerItem.Delete();
            }
        }
        catch (Exception e)
        {
            Log.Error("Error executing schedule item: " + Name, e, this);
            throw;
        }
    }

    public new string GetJobName(CommandItem command)
    {
        if (command == null)
        {
            throw new ArgumentNullException("command", "command must not be null.");
        }

        // We return the combined name of the task, which is unique to the command
        return "TimebasedScheduleCommand '" + command.InnerItem.ID + "'";
    }

    private void ExecuteAsynchronously(CommandItem command)
    {
        // Here we run the task asynchronously, so we create a Job, and start it.
        string jobName = GetJobName(command);
        JobManager.Start(new JobOptions(
            jobName,
            "timebasedschedule",
            "scheduler",
            command,
            "Execute",
            new object[] { Items, this }) { AtomicExecution = true });
    }

    private void ExecuteSynchronously(CommandItem command)
    {
        // Here we run the task synchronously, so we just call execute on it.
        command.Execute(Items, this);
    }

    private bool TryParseTimeOfDay(out TimeSpan timeOfDay)
    {
        DateTime result;

        // We try and parse the time of day field - it is supposed to be in the format "HH:mm".
        if (!DateTime.TryParseExact(InnerItem["Time Of Day"], "HH:mm", null, DateTimeStyles.None, out result))
        {
            // We failed, so we set timeOfDay to an empty TimeSpan and return false.
            timeOfDay = new TimeSpan();
            return false;
        }

        // We succeded, so we set timeOfDay to the parsed value, and return true.
        timeOfDay = result.TimeOfDay;
        return true;
    }
}

Then we need another class, which is our database agent, which has the job of finding the tasks, and starting them when needed - the class is called TimebasedDatabaseAgent, and the code looks like this:

using System;
using System.Linq;

using Sitecore;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Jobs;

public class TimebasedDatabaseAgent
{
    public TimebasedDatabaseAgent(string databaseName, string scheduleRoot)
    {
        Error.AssertString(databaseName, "databaseName", false);
        Error.AssertString(scheduleRoot, "scheduleRoot", false);

        Database = Factory.GetDatabaseNames().Contains(databaseName) ? Factory.GetDatabase(databaseName) : null;
        ScheduleRoot = scheduleRoot;
        LogActivity = true;

        if (Database == null)
        {
            LogInfo("TimebasedDatabaseAgent skipping, database is null");
        }
    }

    public Database Database { get; private set; }

    public bool LogActivity { get; set; }

    public string ScheduleRoot { get; private set; }

    public void Run()
    {
        // If we have no database (like running on a content delivery server without the master database), we return.
        if (Database == null)
        {
            return;
        }

        LogInfo("TimebasedDatabaseAgent started. Database: " + Database.Name);

        // First we find the current job, and the schedules.
        Job job = Context.Job;
        TimebasedScheduleItem[] schedules = GetSchedules();

        LogInfo("Examining schedules (count: " + schedules.Length + ")");

        // Then we test if the job is valid, and if it is, we set the status on it.
        if (IsValidJob(job))
        {
            job.Status.Total = schedules.Length;
        }

        // For each found task
        foreach (TimebasedScheduleItem scheduleItem in schedules)
        {
            try
            {
                // If the schedule is due, we execute it.
                if (scheduleItem.IsDue)
                {
                    LogInfo("Starting: " + scheduleItem.Name + (scheduleItem.Asynchronous ? " (asynchronously)" : string.Empty));
                    scheduleItem.Execute();
                    LogInfo("Ended: " + scheduleItem.Name);
                }
                else
                {
                    LogInfo("Not due: " + scheduleItem.Name);
                }

                // If the schedule is expired and set to be auto removed, we remove it.
                if (scheduleItem.AutoRemove && scheduleItem.Expired)
                {
                    LogInfo("Schedule is expired. Auto removing schedule item: " + scheduleItem.Name);
                    scheduleItem.Remove();
                }
            }
            catch (Exception e)
            {
                LogInfo("Failed to run tasks, error: " + e.Message);
            }

            // If the job is valid, we increase the processed counter.
            if (IsValidJob(job))
            {
                job.Status.Processed += 1;
            }
        }
    }

    private static bool IsValidJob(Job job)
    {
        // We check if the job is in the right category
        return job != null && job.Category == "schedule";
    }

    private TimebasedScheduleItem[] GetSchedules()
    {
        // First we find the root folder.
        Item item = Database.Items[ScheduleRoot];

        // If we found it.
        if (item != null)
        {
            // Then we take all descendants from it, which is based on the template "Timebased Schedule", and return them as an array.
            return (from foundItem in item.Axes.GetDescendants()
                    where foundItem.TemplateName == "Timebased Schedule"
                    select new TimebasedScheduleItem(foundItem)).ToArray();
        }

        // We did not find the root folder, so we return an empty array.
        return new TimebasedScheduleItem[0];
    }

    private void LogInfo(string message)
    {
        // If logging is enabled, we log the message.
        if (LogActivity)
        {
            Log.Info(message, this);
        }
    }
}

Thats all the code needed, now we just need to create an include file, to configure Sitecore to use our new task system.

Include file:

The include file looks like this, and assumes you have placed the code in an assembly called TimebasedTasks:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <scheduling>
      <frequency>00:00:30</frequency>
      <!-- Agent to process timebased schedules embedded as items in a database -->
      <agent type="TimebasedTasks.TimebasedDatabaseAgent, TimebasedTasks" method="Run" interval="00:00:30" patch:after="frequency">
        <param desc="database">master</param>
        <param desc="schedule root">/sitecore/system/Tasks/Timebased Schedules</param>
        <LogActivity>true</LogActivity>
      </agent>
    </scheduling>
  </sitecore>
</configuration>

Note that we have to change the frequency to 30 seconds to ensure, that our tasks gets executed the right minute (The value 30 is found by experimentation, anything higher have been causing tasks not to get run when they should - your milage may vary).

Summary:

That really all there is to it - now you are able to create tasks in the folder you created, and select the command to run, which days to run, and the time to run, in the format "HH:mm".

Simple, right?

tirsdag den 12. november 2013

A PublishQueue for publishing several different items in the background

From time to time, we all happen to have to create some import task in Sitecore, that imports alot of content.

The problem with this content is often, that you have to publish it once it has been imported, which can take quite some time of you are importing several hundred items (and maybe deleting some old items too).

That is why we have created a PublishQueue (not to be mistaken for the normal publish queue in Sitecore), that you can create, and fill with items - and then finally publish in one go.

Enough talking - here we go :-)

This solution consists of three classes: the PublishQueue class, which implements the queue, the QueueProcessor, which does the publishinh, and finally PublishingHelper, which has two helper functions the developer calls once he is ready to publish.

First, we have the PublishQueue class:

using System;
using System.Collections.Generic;

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

public sealed class PublishQueue
{
    private readonly List<PublishOptions> publishQueue = new List<PublishOptions>();
    private readonly Database targetDatabase;

    public PublishQueue(Database targetDatabase)
    {
        this.targetDatabase = targetDatabase;
    }

    internal int Count
    {
        get
        {
            return publishQueue.Count;
        }
    }

    internal PublishOptions[] QueueContent
    {
        get
        {
            return publishQueue.ToArray();
        }
    }

    public void Clear()
    {
        publishQueue.Clear();
    }

    public void Enqueue(Item item, bool includeChildren, bool allLanguages, PublishMode publishMode)
    {
        if (item == null)
        {
            throw new ArgumentNullException("item", "item must not be null");
        }

        switch (publishMode)
        {
            case PublishMode.Full:
            case PublishMode.Smart:
                break;
            case PublishMode.SingleItem:
            case PublishMode.Unknown:
            case PublishMode.Incremental:
                throw new ArgumentException("The passed PublishMode is invalid", "publishMode");
            default:
                throw new ArgumentOutOfRangeException("publishMode");
        }

        PublishOptions options = new PublishOptions(item.Database, targetDatabase, publishMode, item.Language, DateTime.Now)
        {
            Deep = includeChildren,
            RepublishAll = allLanguages,
            RootItem = item
        };

        publishQueue.Add(options);
    }
}

The next class we need, is the QueueProcessor class:
using System;

using Sitecore.Data;
using Sitecore.Globalization;
using Sitecore.Publishing;

private class QueueProcessor
{
    private readonly PublishQueue queue;

    public QueueProcessor(PublishQueue queue)
    {
        if (queue == null)
        {
            throw new ArgumentNullException("queue", "queue must not be null");
        }

        this.queue = queue;
    }

    public void ProcessQueue()
    {
        foreach (PublishOptions options in queue.QueueContent)
        {
            Language[] languages = options.RepublishAll ? options.RootItem.Languages : new[] { options.RootItem.Language };
            Database[] databases = new[] { options.TargetDatabase };

            PublishManager.PublishItem(options.RootItem, databases, languages, options.Deep, options.Mode == PublishMode.Smart);
        }
    }
}

Finally we need these the helper class, called PublishingHelper:
using System;

using Sitecore;
using Sitecore.Jobs;

public static class PublishingHelper
{
    public static void ProcessPublishQueue(PublishQueue queue)
    {
        QueueProcessor processor = new QueueProcessor(queue);

        processor.ProcessQueue();
    }

    public static Job ProcessPublishQueueAsync(PublishQueue queue)
    {
        string jobName = GetJobName(queue);

        JobOptions jobOptions = new JobOptions(jobName, "publish", "publisher", new QueueProcessor(queue), "ProcessQueue")
        {
            ContextUser = Context.User,
            AfterLife = TimeSpan.FromMinutes(1.0),
            AtomicExecution = true
        };

        if (Context.Job != null)
        {
            jobOptions.ClientLanguage = Context.Job.Options.ClientLanguage;
        }

        return JobManager.Start(jobOptions);
    }

    private static string GetJobName(PublishQueue queue)
    {
        return string.Format("Publish queue containing {0} items.", queue.Count);
    }
}

Then we are done - the only thing needed is to use it - here is an example of how it can be used:
public static void ImportItems(IEnumerable<XElement> dataFromExternalSystem)
{
    PublishQueue queue = new PublishQueue(Factory.GetDatabase("web"));

    IEnumerable<Item> oldItems = GetOldItemsToBeRemoved();

    using (new SecurityDisabler())
    {
        foreach (Item oldItem in oldItems)
        {
            oldItem.Delete();
            queue.Enqueue(oldItem, false, true, PublishMode.Full);
        }
    }

    foreach (XElement element in dataFromExternalSystem)
    {
        Item item = CreateItemFromData(element);
        queue.Enqueue(item, false, true, PublishMode.Full);
    }

    PublishingHelper.ProcessPublishQueueAsync(queue);
}

First we create our queue, defining that the items in it, should be published to the web database.

Then we gets all the old items that we want to remove, and both remove them and add them to the queue, so they get removed from the web database too.
And then we create all the new items, which also gets put into the queue.

Finally we starts an async publish of the entire queue in the background - we don't save the return value, since we don't care about when it gets done, it should just run in the background untill it is done.

Thats really all there is to it - we have been using this for a while now, and it really both simplifies and increase the stability of our import tasks, so thats a win-win :-)

mandag den 4. november 2013

Links to media items on external sitecore sites and Sitecore 6.5

Just ran into an interresting problem a while ago, with a Sitecore 6.5 solution we made a while back.

It seems like there is a bug in Sitecore 6.5, that has been fixed in later versions of Sitecore 6.5

I'm writing here, so people having the same problem knows that an upgrade fixes it.

Now, lets image we have created the website http://www.mywebsite.org for a customer, and they want to link to latest price list from one of their suppliers, who is also running Sitecore.

The supplier has the website http://www.supplier.org , and their pricelist is located at http://www.supplier.org/~/media/Files/PriceList.pdf .

Our editor at mywebsite would like to link to this, so he creates a new article, and inserts a link to this file using the Rich Text editor in Sitecore.

Once he saves the link, it is saved correctly to the database - however, when the frontend renders the link, it tries to expand it, which results in the following link getting rendered: http://www.supplier.org/http://www.mywebsite.org/~/media/Files/PriceList.pdf , which ofcause does not work.

The good news is, that the link manager we rewritten late in Sitecore 6.5 , so upgrading to the newest version of Sitecore 6.5, or Sitecore 6.6 would solve this problem.

So, if you are creating a new solution, there is nothing to worry about, since you really should be using the newest version of Sitecore anyway - but if you have some old solutions, where people hits this bug, the easy fix is to upgrade. (There is a workaround which replaces parts of the link manager, which I'm not gonna go into details with).

tirsdag den 29. oktober 2013

Leaving Page Editor mode resulting in a 404 error.

There is a bug in Sitecore, that causes it to generate an excess of cookies when leaving Page Editor mode (clicking the Close button in the ribbon).

This causes browsers to behave weird, and Internet Explorer just displays a 404 error page.

The problem happens, if there is code on the page calling SiteContextFactory.GetSiteContext , which is used to get a SiteContext for a certain site.

For every time this function is called, a new cookies is set, so if you have logic in your solution that uses, you must cache the SiteContext objects you get from GetSiteContext, to work around this problem.

Due to a bug in one of our controls on a solution we where designing, a menu was looking up the SiteContext for every element in a menu, which caused it to fail.

If you want to see the proble, here is a step-by-step guide to how to trigger it - I haven't tested it on anything newer than Sitecore 6.5, but to my knowledge, the bug is still there.

How to trigger:

  1. Create an empty aspx page, and add the following code below to the Page_Load event.
  2. Add this aspx page as a layout in Sitecore, and assign it to an item.
  3. Open the page editor, go to the item, and then click Close on the ribbon.
Here is the code needed to trigger the bug - you would not normally do this, but it trigger the creation of way too many cookies:

            for (int i = 0; i < 300; i++)
            {
                foreach (string siteName in Sitecore.Sites.SiteContextFactory.GetSiteNames())
                {
                    Sitecore.Sites.SiteContextFactory.GetSiteContext(siteName);
                }
            }

Workaround:

Since quite alot of Sitecore's API uses SiteContext objects as parameters, we have to deal with them, which poses these problems when using solutions with multiple sites, that needs to be able to get links across.

However, here is a small code snippet that we are using, that works around this bug:

        internal class SiteContextWrapper : SiteContext
        {
            public SiteContextWrapper(SiteInfo siteInfo) : base(siteInfo, false)
            {
            }
        }

It simply extends the SiteContext class, causing it to be created without setting the cookie.
To get the SiteContext object, you simply class the GetSiteInfo function in SiteContextFactory, and use that to create a new SiteContextWrapper object, which can be cast to a SiteContext object.

mandag den 28. oktober 2013

Using Axes.GetDescendant vs. Axes.SelectSingleItem

I've come across an interresting problem, where people are using the SelectSingleItem function, when they could be using GetDescendant.

Here is some sample code to test the difference between those two performance wise:

            Stopwatch stopWatch = new Stopwatch();
            stopWatch.Start();
            
            for (int i = 0; i < 10; i++)
            {
                rootItem.Axes.GetDescendant("Test Article 2/Test Subarticle 1/Test Subsubarticle 1");
                rootItem.Axes.GetDescendant("Test Article 2/Test Subarticle 1/Test Subsubarticle 2");
                rootItem.Axes.GetDescendant("Test Article 2/Test Subarticle 1/Test Subsubarticle 3");
            }

            stopWatch.Stop();
            TimeSpan ts1 = stopWatch.Elapsed;

            stopWatch = new Stopwatch();
            stopWatch.Start();

            for (int i = 0; i < 10; i++)
            {
                rootItem.Axes.SelectSingleItem("Test Article 2/Test Subarticle 1/Test Subsubarticle 1");
                rootItem.Axes.SelectSingleItem("Test Article 2/Test Subarticle 1/Test Subsubarticle 2");
                rootItem.Axes.SelectSingleItem("Test Article 2/Test Subarticle 1/Test Subsubarticle 3");
            }

            stopWatch.Stop();
            TimeSpan ts2 = stopWatch.Elapsed;

            litResult.Text = string.Format("GetDescendant: {0} ms, SelectSingleItem: {1} ms.", ts1.TotalMilliseconds, ts2.TotalMilliseconds);

This results in:
GetDescendant: 9,2628 ms, SelectSingleItem: 22,5741 ms.
The numbers vary a bit from each run, but it averages out at SelectSingleItem being about 2.5x times slower than GetDescendant - now imagine using SelectSingleItem in a sublayout loaded on every page view, that cannot be cached...

So, why is there even a SelectSingleItem function on the Axes class, one might ask?

SelectSingleItem uses XPath to lookup the item, which also explains why it is slower than GetDescendant (which just crawles down the tree).

So, what to take away from this?

If you just need to look up an item below the current item, use GetDescendant - and if you need to use XPath, use SelectSingleItem.