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.