torsdag den 27. februar 2014

Rendering controls from a placeholder on another item

From time to time, it makes sense to render the content that is defined on another item, as if it was defined on the item the user is viewing.

To do this, we have created an custom placeholder, that can be inserted like any other ASP.NET control, and then configured to get the renderings from somewhere else.

First we need to define a base class for all user controls inserted on a control, to both help getting the datasource, and being able to change it from outside of the control itself.

This can also be done for webcontrols, but I'll leave that to you to figure out how to do that :)

Here is how the base class looks:

public abstract class BaseSublayoutUserControl : UserControl
{
    private Item _datasourceItem;

    private NameValueCollection _renderingParameters;

    protected Item Datasource
    {
        get
        {
            if (_datasourceItem != null)
            {
                return _datasourceItem;
            }

            try
            {
                Sublayout sublayout = (Sublayout)Parent;
                if (!string.IsNullOrEmpty(sublayout.DataSource))
                {
                    _datasourceItem = Sitecore.Context.Database.GetItem(sublayout.DataSource);
                }
            }
            catch (NullReferenceException)
            {
                _datasourceItem = null;    
            }
            catch (InvalidCastException)
            {
                _datasourceItem = null;    
            }
            catch (InvalidOperationException)
            {
                _datasourceItem = null;
            }

            return _datasourceItem;
        }

        set
        {
            _datasourceItem = value;
        }
    }

    protected NameValueCollection RenderingParameters
    {
        get
        {
            if (_renderingParameters != null)
            {
                return _renderingParameters;
            }

            _renderingParameters = string.IsNullOrEmpty(Attributes["sc_parameters"]) ? new NameValueCollection() : HttpUtility.ParseQueryString(Attributes["sc_parameters"]);

            return _renderingParameters;
        }
    }

    internal void ForceDatasourceItem(Item newDatasource)
    {
        _datasourceItem = newDatasource;
    }

    internal void ForceParameters(string parameters)
    {
        Attributes["sc_parameters"] = parameters;
    }
}

Here both the parameters and the datasource is exposed, so it is easy to get them from the control.
Then every usercontrol that is to be inserted using the page editor should just inherit this class instead of the normal UserControl class, and all is fine.

Now that we have the base class done, we need to implement the placeholder - this requires two classes, the placeholder itself, and what is known as a ControlBuilder, which makes it possible to configure how the control behave when inserted.

Here is the code for the control builder class:

public sealed class RemoteLayoutRendererControlBuilder : ControlBuilder
{
    public override bool AllowWhitespaceLiterals()
    {
        return false;
    }
}

Then, we just need to control that does all the work:

[ControlBuilder(typeof(RemoteLayoutRendererControlBuilder))]
[ToolboxData("<{0}:RemoteLayoutRenderer runat=\"server\" />")]
public class RemoteLayoutRenderer : Control
{
    public RemoteLayoutRenderer()
    {
        Device = Sitecore.Context.Device;
    }

    public Item Item { get; set; }

    public string PlaceholderKey { get; set; }

    public DeviceItem Device { get; set; }

    protected override void CreateChildControls()
    {
        base.CreateChildControls();

        if (Item == null || string.IsNullOrEmpty(PlaceholderKey))
        {
            return;
        }

        if (string.IsNullOrEmpty(Item[FieldIDs.LayoutField]))
        {
            return;
        }

        IEnumerable<RenderingReference> renderings = from rendering in Item.Visualization.GetRenderings(Device, false)
                                                        where rendering.Placeholder.EndsWith(PlaceholderKey, StringComparison.InvariantCultureIgnoreCase)
                                                        select rendering;

        int i = 0;

        foreach (RenderingReference rendering in renderings)
        {
            Control control;

            try
            {
                switch (rendering.RenderingItem.InnerItem.TemplateName)
                {
                    case "Sublayout":
                        control = GetSublayout(rendering, i++);
                        break;
                    // Hint - here might be a good place to handle webcontrols ;-)
                    default:
                        continue;
                }
            }
            catch (TargetInvocationException)
            {
                continue;
            }
            catch (InvalidCastException)
            {
                continue;
            }

            if (control != null)
            {
                Controls.Add(control);
            }
        }
    }

    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);

        Load += RemoteLayoutRendererLoad;
    }

    private void RemoteLayoutRendererLoad(object sender, EventArgs e)
    {
        EnsureChildControls();
    }

    private BaseSublayoutUserControl GetSublayout(RenderingReference rendering, int controlIndex)
    {
        if (string.IsNullOrEmpty(rendering.RenderingItem.InnerItem["Path"]))
        {
            return null;
        }

        BaseSublayoutUserControl control = Page.LoadControl(rendering.RenderingItem.InnerItem["Path"]) as BaseSublayoutUserControl;

        if (control == null)
        {
            return null;
        }

        if (!string.IsNullOrEmpty(rendering.Settings.DataSource))
        {
            control.ForceDatasourceItem(Sitecore.Context.Database.GetItem(rendering.Settings.DataSource));
        }

        if (!string.IsNullOrEmpty(rendering.Settings.Parameters))
        {
            control.ForceParameters(rendering.Settings.Parameters);
        }

        control.ID = ID + "_dynamic_" + controlIndex;

        return control;
    }
}

Now we can just insert this like any other ASP.NET control, and define which placeholder to load the controls from, and then set the Item property to the item they are defined on.

As long as the user controls inherit from our base class, we can configure their datasource and parameters, so they don't know that they have been pulled in somewhere else.

onsdag den 19. februar 2014

Create a custom Sitecore cache

I have been experimenting with creating a custom Sitecore cache, to be able to cache the result of some calculations that happens all the time.

In this case, we happen to quite often lookup which of the sites defined in Sitecore, that matches a certain criteria (has certain sub items).

In solutions with many sites, this quickly starts to slow the solution down - but there is a simple solution, which is caching the result :)

So first, we have to implement the cache, which requires two classes - the cache (called SiteCache) and a cache manager (called SiteCacheManager).

Here is the code for the SiteCache:

internal class SiteCache : CustomCache
{
    public SiteCache(string name, long maxSize) : base(name, maxSize)
    {
    }

    public bool IsSiteInCache(string siteName)
    {
        object obj = GetObject(siteName);

        return obj != null;
    }

    public void AddSiteToCache(string siteName)
    {
        SetObject(siteName, true, sizeof(bool));
    }
}

And here is the code for the SiteCacheManager

internal static class SiteCacheManager
{
    private static readonly SiteCache Cache = new SiteCache("SiteCache", StringUtil.ParseSizeString("100KB"));

    public static bool IsSiteInCache(string siteName)
    {
        return Cache.IsSiteInCache(siteName);
    }

    public static void AddSiteToCache(string siteName)
    {
        Cache.AddSiteToCache(siteName);
    }

    public static void Clear()
    {
        Cache.Clear();
    }
}

The way it works is pretty simple, since the manager is static, as soon as Sitecore starts, it creates a new instance of the SiteCache cache.
Then, to use this, we create a simple function to test if sites are valid:

public bool IsSiteValid(string siteName)
{
    if (SiteCacheManager.IsSiteInCache(siteName))
    {
        return true;
    }

    bool siteValid = RunSiteTest(siteName);

    if (siteValid)
    {
        SiteCacheManager.AddSiteToCache(siteName);
    }

    return siteValid;
}

This way, we always look in the cache first, and if the site isn't there, we do the normal tests, and if the result is that the site is valid, we add it to the cache, so we can avoid the test the next time.

The reason we do this, is because the cache might have been flushed, so we need to always do the check if the site is missing from the cache.

So this works best, if most of the things you test on is valid - if most of the sites in the solution for this is returning false from the test, the amount of time saved will be minimal.

mandag den 13. januar 2014

Presenting at the Danish Sitecore Developer Group meeting

Hey,

Next wednesday, I'll be presenting how we at the company I work at (DIS/PLAY A/S), have streamlined our Sitecore development process.

So in case you happen to be in Copenhagen, and want to hear about this, please drop by :-)

You can read more about it here:
http://www.meetup.com/Danish-Sitecore-Developer-Group/events/134529942/

This is also why there hasn't been any updates on this blog for a little while, since I've been busy preparing.

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.