Share

LinkedIn

Presentation Inversion of Control (Part 2)

In my previous post, I introduced the concept of presentation inversion of control.  In this post, I will show a few ways that this concept can be implemented.

I will cover three different implementations of presentation inversion of control.  However, all of the implementations follow the same basic procedure.  The three implementations only differ in how and when they execute these steps.

  1. Get the list of content items that need to be rendered on the page.
  2. Look up the presentation settings on those items.
  3. Add the rendering controls specified in the presentation settings to the page.

Step one will be pretty much the same for all three implementations.  For the callouts example that I used in part 1, the content author selects the items that they want to add to the page in a multilist or treelist.  So, step one would just consist of getting the list of items selected in that field.  In other scenarios, you might get the list of child items or items in a particular folder.  In the sample code in this post I will abstract the detail of step one through the use of a custom item property.

Sublayout/WebControl

When I first came up with the concept of presentation inversion of control, this is how I implemented it.  This was back in 2007 when we were using Sitecore 5.3.  The basic idea here is that your logic is contained within a sublayout or web control.  You look up the presentation settings on each of the items that you want to render on the page, get the control responsible for rendering and add it as a child control of your sublayout or web control.

01.using Sitecore.Data.Items;
02.using Sitecore.Layouts;
03.using Sitecore.Pipelines.RenderLayout;
04.namespace BlogPosts.Pioc
05.    public partial class Callouts: System.Web.UI.UserControl
06.    {
07.        protected void Page_Load(object sender, EventArgs e)
08.        {
09.            PageItem page = Sitecore.Context.Item;
10.            foreach (Item itemCallout in page.RightCallouts)
11.           {
12.                string strDataSource = itemCallout.ID.ToString();
13.                RenderingReference[] renderings =
14.                    itemCallout.Visualization.GetRenderings(Sitecore.Context.Device, false);
15.                foreach (RenderingReference rendering in renderings)
16.                {
17.                    rendering.Settings.DataSource = strDataSource;
18.                    this.Controls.Add(
19.                        rendering.RenderingItem.GetControl(rendering.Settings));
20.                }
21.            }
22.        }
23.    }

The code above iterates through the list of items and for each item, it iterates through the list of renderings that are assigned in its presentation settings.  For each rendering, it sets the data source to the item ID, gets the control associated with the rendering and adds it to the controls collection.

This implementation does the job.  All of the content items are rendered as expected.  You can use all of the standard Sitecore caching settings.  It even still works with the Page Editor (Web Edit, if you’re still on version 5.x).

One downside to this approach is that the rendering controls are added very late in the pipeline.  It would be nice if we could add them around the same time as the rest of the renderings so that they would be available for any other processing that you wanted to do on them.

RenderLayout Processor

The next implementation uses a custom proccessor for the RenderLayout pipeline.  The code is very similar to the first implementation except that it adds the RenderingReference objects directly to the page context rather than using them to get the actual controls.

01.using Sitecore.Data.Items;
02.using Sitecore.Layouts;
03.using Sitecore.Pipelines.RenderLayout;
04.       
05.namespace BlogPosts.Pioc
06.{
07.    class InsertCalloutRenderings : RenderLayoutProcessor
08.    {
09.        public override void Process(RenderLayoutArgs args)
10.        {
11.            PageItem page = Sitecore.Context.Item;
12.            foreach (Item itemCallout in page.RightCallouts)
13.            {
14.                string strDataSource = itemCallout.ID.ToString();
15.                RenderingReference[] renderings =
16.                    itemCallout.Visualization.GetRenderings(Sitecore.Context.Device, false);
17.                foreach (RenderingReference rendering in renderings)
18.                {
19.                    rendering.Settings.DataSource = strDataSource;
20.                    Sitecore.Context.Page.AddRendering(rendering);
21.                }
22.            }
23.        }
24.    }
25.}

This is then inserted into the RenderLayout pipeline, usually right after the InsertRenderings processor.

1.<renderLayout>
2.  ...
3.  <processor type="Sitecore.Pipelines.RenderLayout.InsertRenderings, Sitecore.Kernel" />
4.  <processor type="BlogPosts.Pioc.AddCallouts, BlogPosts.Pioc" />
5.  <processor type="Sitecore.Pipelines.RenderLayout.PageExtenders, Sitecore.Kernel" />
6.  ...
7.</renderLayout>

This is an improvement over the first implementation because the renderings are added much earlier in the pipeline.  One disadvantage of this implementation is that it runs on every request.  Therefore, you need to make sure that your code is efficient.  It is a good idea to start out with some conditional statements so that you can exit immediately, if your processor doesn’t need to run.

Conditional Rendering

In Sitecore 6.1 and later, we have another implementation option: conditional rendering.  A conditional rendering rule associates a condition with an action.  The rule can then be selected in the Personalization section of the Control Properties dialog when editing presentation details.

Depending on the requirements, it may be possible to use out-of-the-box conditions.  For the callouts example, I created a custom action that determines whether the specified multilist or treelist field is empty, taking into account the permissions of the user.

01.using Sitecore.Data.Fields;
02.using Sitecore.Diagnostics;
03.using Sitecore.Rules;
04.using Sitecore.Rules.Conditions;
05.       
06.namespace BlogPosts.Pioc
07.{
08.    class WhenMultilistEmpty<T> : WhenCondition<T> where T : RuleContext
09.    {
10.        public string FieldName { get; set; }
11.   
12.        protected override bool Execute(T ruleContext)
13.        {
14.            Assert.ArgumentNotNull(ruleContext, "ruleContext");
15.            Assert.IsNotNullOrEmpty(this.FieldName, "FieldName is not set");
16.       
17.            MultilistField fld = ruleContext.Item.Fields[this.FieldName];
18.            Assert.IsNotNull(fld, string.Format("Field {0} does not exist", this.FieldName));
19.            return (fld.GetItems().Length == 0);
20.        }
21.    }
22.}

The condition definition item then has the following Text property.  The part in square brackets tells the system to let the user enter a string for the name of the field that should be checked.  This is how the FieldName property above is set.

image

The action is then very similar to the previous two implementations.  This time we add the rendering references to the rule context.  For the sake of reuse, I created an abstract base class for this action. This makes it a bit easier to create actions that get the list of non-page items in some other way.

01.using System.Collections.Generic;
02.using Sitecore.Data.Items;
03.using Sitecore.Layouts;
04.using Sitecore.Rules.Actions;
05.using Sitecore.Rules.ConditionalRenderings;
06.       
07.namespace BlogPosts.Pioc
08.{
09.    public abstract class AddSelfRenderingItemsActionBase<T>
10.        : RuleAction<T> where T : ConditionalRenderingsRuleContext
11.    {
12.        public abstract IEnumerable<Item> GetItems(T ruleContext);
13.   
14.        public override void Apply(T ruleContext)
15.        {
16.            foreach (Item item in this.GetItems(ruleContext))
17.            {
18.                string strDataSource = item.ID.ToString();
19.       
20.                RenderingReference[] calloutRenderings =
21.                    item.Visualization.GetRenderings(Sitecore.Context.Device, true);
22.                foreach (RenderingReference rendering in calloutRenderings)
23.                {
24.                    rendering.Settings.DataSource = strDataSource;
25.                    ruleContext.References.Add(rendering);
26.                }
27.            }
28.        }
29.    }
30.}

An implementation of the abstract class gets the items selected in the specified multilist or treelist field.

01.using Sitecore.Data.Items;
02.using Sitecore.Diagnostics;
03.using Sitecore.Rules.ConditionalRenderings;
04.       
05.namespace BlogPosts.Pioc
06.{
07.    public class AddSelfRenderingItemsFromMultilistAction<T>
08.        : AddSelfRenderingItemsActionBase<T> where T : ConditionalRenderingsRuleContext
09.    {
10.        public string FieldName { get; set; }
11.       
12.        public override IEnumerable<Item> GetItems(T ruleContext)
13.        {
14.            Assert.IsNotNullOrEmpty(this.FieldName, "Field name not set");
15.            Item pageCurrent = ruleContext.Item;
16.            MultilistField fld = pageCurrent.Fields[FieldName];
17.            return fld.GetItems();
18.        }
19.    }
20.}

The action definition item allows the user to enter the name of the field.  This is how the FieldName property above is set.

image

A rule definition item then puts the condition together with the action.

image

Finally, the rule is selected in the Control Properties dialog of a sublayout.

image

Obviously, there is a bit more effort required for this implementation.  At least, the first time around.  But this is composed of pieces that could be reused individually.  This implementation also has advantages over the other two.  First, the renderings will be added during the InsertRenderings step of the RenderLayout pipeline at the time that renderings are normally added.  Also, since you can select this rule to be evaluated on another sublayout or rendering, it won’t necessarily be executed on every request.

In my opinion, the conditional rendering implementation gives the cleanest, most efficient and most maintainable result.  If you are still working with an older version of Sitecore, one of the first two implementations will still be a big improvement over the traditional approaches that I discussed in part 1.

Sitecore development, Sitecore code, Sitecore architecture

Comments

Add a Comment

*
*

Please confirm you are human by typing the text you see in this image:

Corey Burnett said: 9/26/2011 at 3:35 PM

Excellent post! I am trying to implement the first method that you mentioned. For some reason it seems like the individual sublayouts are not getting their data source set correctly at run time. They are trying to display fields from the main item, not the individual items from the multilist. Any idea what I am doing wrong?

Ben said: 9/28/2011 at 10:18 AM

The standard boilerplate code in an XSL rendering sets the "current" item to the one specified as the datasource. Sublayouts, however, do not have any such boilerplate code. When you access Sitecore.Context.Item, you get the item that corresponds to the requested URL. You have to use code like the following to get the data source item.

private Item _dataSource;
public Item DataSource
{
get
{
if (null == _dataSource)
{
var sublayout = Parent as Sublayout;
if (null == sublayout) return null;
_dataSource = Sitecore.Context.Database.GetItem(Sitecore.Data.ID.Parse(sublayout.DataSource));
}
return _dataSource;
}
}

Tony said: 2/25/2012 at 10:09 AM

This is great and I've used it in a few implementations, but I have a problem now that I'm using CMS 6.5/DMS 2.0... It doesn't seem to pay attention to DMS personalization. I have a folder of callouts that is the datasource of the multilist. If I take one of those callouts, modify the presentation details so that the sublayout uses a simple persona that has the rule "hide rendering," it still displays. I think that the AddSelfRenderingItemFromMultilist ignores the personalization and just grabs the rendering and adds it to the placeholder. Has any one else seen this?

Nishant Agarwal said: 6/7/2012 at 4:52 AM

I was able successfully implement this using WebControl.
But I have one problem. If I add any control which has any postback event (like button click event), that event is not being triggered as controls are added at runtime.

Is there any way to resolve this issue ?

Ben said: 6/7/2012 at 10:55 AM

Nishant-

The behavior should be the same as you would see with any dynamically created controls. As long as you are adding the controls on every page load and postback, there should not be a problem. Theoretically, adding the controls during the Load phase should work. But if you continue to experience problems, you could try moving it up to the Init phase.