Clay Part 2: Through the Looking Glass

 

In the first tutorial we looked at some of Orchard's built in Clay behaviours, specifically the Zones behaviour. In this part we'll dig a little deeper and actually start writing our own.

The Zone Proxy

Firstly, just take a look at this code:

    public class ZoneShapes : IShapeTableProvider {

        public void Discover(ShapeTableBuilder builder) {
            builder.Describe("Content")
                .OnCreating(creating => {

                        // Add our own behaviour
                        creating.Behaviors.Add(
                            new ZoneProxyBehavior(zoneBehavior)
                            );
                });
        }

    }

You might already be familiar with IShapeTableProvider. This event handler is used all over the place to modify the shape output of Orchard. Commonly this is used for adding Alternates for template picking, or injecting custom values into the shape to display in the view.

Something else you can do is add an OnCreating handler as I'm doing here, and one of the things you can do in this event is add new behaviours to a shape.

Here we're specifically targetting the "Content" shape, which is used to render the container template for all normal content (Content.cshtml). This shape holds the zones which Placement.info can dispatch the different Part shapes to, normally any shape emitted from a Driver.

So the Content shape comes pre-loaded with a ZoneHoldingBehavior. I'm adding a new one. Before I go into the implementation of ZoneProxyBehavior, I'll just detail a scenario which, if you've spent a while working with Orchard you will surely have come up against, which I've seen crop up time and time again in the forums and which there has never been a particularly elegant solution for. Until now.

Placement.info is really handy when you only want to move your parts into different zones on the Content shape. But what if you want to dispatch a piece of your content item off to a completely different part of the overall Layout; e.g. the page Header (for a custom banner per page), or the AsideSecond zone (for "related links"), or to the Navigation zone (for a customised menu on a particular page).  There's no right answer on how to do this, although there is a fairly unpleasant workaround - copy/paste the original driver for the shape you want, and hardcode a push to Layout. The code looks like this:

var shape = shapeHelper.Parts_Some_Part(ContentPart:part);

Services.WorkContext.Layout.AsideSecond.Add(shape,"5");

This technique works; but you have to repeat it every time you want this kind of behaviour, it creates unmaintainable code because you have to copy everything from the driver, and the zone and position are hardcoded.

 There are many forum threads from people asking how to do this; a recent example is here; I've seen this dozens of times.

Well, that all changes here. That initial code snippet was partially incomplete. The constructor of ZoneProxyBehavior actually takes a second parameter, which is an IDictionary<string,Func<dynamic>> - so a keyed dictionary of factories to return arbitrary shapes. This can contain any shapes we want to proxy to. Here's what it should have looked like:

                        creating.Behaviors.Add(
                            new ZoneProxyBehavior(zoneBehavior, 
                                new Dictionary<string, Func<dynamic>> { { "Layout", 
                                    () => _workContextAccessor.GetContext().Layout } })
                            );

Finally I'm going to edit the Placement.info in my theme and add the following:

<Match DisplayType="Detail">
                <Place Parts_Title="Layout@AsideFirst:1"/>
</Match>

And when I run my site, my page title appears in the AsideFirst zone, off to the side of the Content zone.

 If this is something you've ever tried to do, this solution might well seem like sorcery. But there's nothing hugely magical going on here; and if you were following closely in the first tutorial you might well have figured out how this might work. It's actually stunningly simple.

First, let's think about the implementation of ZoneProxyBehavior itself. Here's the constructor:

    public class ZoneProxyBehavior : ClayBehavior {

        public IDictionary<string, Func<dynamic>> Proxies { get; set; }

        public ZoneProxyBehavior(IDictionary<string, Func<dynamic>> proxies) {
            Proxies = proxies;
        }

    }

So we just save the dictionary (I made it public, in case you wanted to add another shape event to add additional proxies). We'll access it shortly.

Let's just recall how ZoneHoldingBehavior works. It overrides the GetMember method and will respond either to "Zones" which it treats as a virtual dictionary of zones; and any other name will be responding to with zone on demand.

Now we'll look at how shapes are added when Placement is used within the display pipeline. This is buried within ContentShapeResult; these objects are created from the base implementation of ContentPartDriver, and there's literally no way to override or modify their creation. The actual placement logic happens in a private method:

        private void ApplyImplementation(BuildShapeContext context, string displayType) {

            if (!string.Equals(context.GroupId ?? "", _groupId ?? "", StringComparison.OrdinalIgnoreCase))
                return;

            var placement = context.FindPlacement(_shapeType, _differentiator, _defaultLocation);

            if (string.IsNullOrEmpty(placement.Location) || placement.Location == "-")
                return;
  
            // ... Then various stuff happens with wrappers and alternates

            // ... And finally the shape is added:

            var delimiterIndex = placement.Location.IndexOf(':');

            if (delimiterIndex < 0) {
                parentShape.Zones[placement.Location].Add(newShape);
            }
            else {
                var zoneName = placement.Location.Substring(0, delimiterIndex);
                var position = placement.Location.Substring(delimiterIndex + 1);
                parentShape.Zones[zoneName].Add(newShape, position);
            }

        }

The code first looks for a placement match on the shape name, and then gives up straight away if it hits a blank or a "-" (the null zone).

Finally it checks for a ":" which indicates Zone:Position, and then pushes the shape off to the right zone with:

                parentShape.Zones[zoneName].Add(newShape, position);

So to follow this in terms of ZoneHoldingBehavior, we can see that it's hitting that special "Zones" property, then accessing the zone by name using indexer access. It's the ZonesBehavior that handles that and returns a ZoneOnDemandBehavior. Then with the Add call, ZoneOnDemandBehavior gets triggered and the zone is actually created.

We can't change or suppress ContentShapeResult in any way. But perhaps we can still change what happens when its code runs!

I'm going to attempt to hijack that special "Zones" property and substitute my own interpretation of what happens from thereon.

So I'll add a GetMember override on my ZoneProxyBehavior. It looks like this:

        public override object GetMember(Func<object> proceed, object self, string name) {

            if (name == "Zones") {
                return ClayActivator.CreateInstance(new IClayBehavior[] {               
                    new InterfaceProxyBehavior(),
                    new ZonesProxyBehavior(()=>proceed(), Proxies, self)
                });

            }
           
            // Otherwise proceed to other behaviours, including the original ZoneHoldingBehavior

            return proceed();

        }

All it does is return a further small set of behaviours. We'll cover them in a second. Otherwise, it just proceed()s onto any other behaviors that might respond - including the original ZoneHoldingBehavior, so any other zone access just happens exactly as normal. We still need the original to work because it knows how to create a zone and we don't want to reimplement that.

ZoneProxyBehavior itself doesn't do anything else; we want to hijack the specific case of parentShape.Zones[zone] - and we'll look in the zone name itself for the proxy sign '@'.

So we're returning a new behaviour of ZonesProxyBehavior; we also pass in a way back out - the proceed() method which this new behavior can call later on to get the ZonesBehavior from ZoneHoldingBehavior. It'll come in useful shortly.

The original ZonesBehavior uses "GetIndex" to implement the indexer access. So here's my implementation of ZonesProxyBehavior overriding GetIndex to check for the "@" proxy symbol and dispatches off to that shape instead:

        public class ZonesProxyBehavior : ClayBehavior {

            private Func<dynamic> _zonesActivator;
            private IDictionary<string, Func<dynamic>> _Proxies;
            private object _parent;

            public ZonesProxyBehavior(Func<dynamic> zonesActivator, 
                    IDictionary<string, Func<dynamic>> Proxies, object self) {
                _zonesActivator = zonesActivator;
                _Proxies = Proxies;
                _parent = self;
            }

            public override object GetIndex(Func<object> proceed, object self,
                    System.Collections.Generic.IEnumerable<object> keys) {

                if (keys.Count() == 1) {
                    // Here's the new bit
                    var key = System.Convert.ToString(keys.Single());

                    // Check for the proxy symbol
                    if (key.Contains("@")) {
                        // Find the proxy!
                        var split = key.Split('@');
                        // Access the proxy shape
                        return _Proxies[split[0]]()
                            // Find the right zone on it
                            .Zones[split[1]];
                    }
                    else {
                        // Otherwise, defer to the ZonesBehavior activator, which we made available
                        // This will always return a ZoneOnDemandBehavior for the local shape
                        return _zonesActivator()[key];
                    }

                }
                return proceed();
            }

    }

It's pretty simple logic: if we're requesting a proxy, look it up in the dictionary and get the zone from there instead. It calls the delegate to get the proxy shape, and access the key from its Zones[] collection. Note that we don't even have to worry about the Add() method, we're returning the whole zone so any further calls will take place on that.

If there isn't an @ sign then we just fire the delegate so we can lookup the zone on the original ZonesBehavior.

The only piece missing here is the GetMember function, because that is normally valid on ZonesBehavior, although I'm not sure if anything much uses it! Still, it's also just a case of delegating to _zonesActivator():

            public override object GetMember(Func<object> proceed, object self, string name) {
                // This is rarely called (shape.Zones.ZoneName - normally you'd just use shape.ZoneName)
                // But we can handle it easily also by deference to the ZonesBehavior activator
                return _zonesActivator()[name];
            }

Wow, that's actually pretty simple! (My first version was about twice as much code but still pretty simply, but it didn't quite work; then I figured out I could pass in the proceed() delegate and it simplified even more). That really is everything, and we've supplemented Placement.info with an impressive new feature, all the time using highly unobtrusive code - this will have zero effect on any layout unless they have an '@' in a Placement node.

It's something I've been wanting to do for quite a while, and I looked at all kinds of methods which were jumping through some extremely complex hoops and involving my own custom implementations of various Orchard services; but I still never quite managed to achieve this seemingly simple goal. Finally, after a short time looking into and understanding behaviours, a very neat solution easily presented itself.

Perhaps in subsequent chapters we'll find even more powerful ways to modify Orchard's behaviour with Clay.

The complete code for ZoneProxyBehavior is available for download here: ZoneProxy.zip (it will also be a feature in the next release of Origami)

If you come up with any cool, interesting, or just plain useful behaviours of your own, let me know - I'd love to spotlight them in this series!

Attachments