Down the Rabbit Hole
After delving deeply into all parts of Orchard, still one area almost completely eluded me: Clay. I finally took the plunge and decided to figure out exactly how it all works. There's some information scattered around but I found very little documentation or explanation about how Clay behaviours can be written, only information on ways you could use existing Clay objects.
Elements of this journey were eerily reminiscent of Lewis Carroll's legendary narrative but be warned; herein lies nothing so mundane as smoking caterpillars or hungry walruses, and there's certainly no white rabbit with bad timekeeping to follow.
Having let that analogy run its course, I should first explain what Clay is. It's actually a very simple little library for manipulating dynamic objects in C#. It's used all over the place in Orchard, in fact almost anywhere you are dealing with a dynamic object it's most likely a Clay instance underneath that; but a lot of the time you won't think you're doing anything more impressive than already provided by ExpandoObject.
But here is the beauty of Clay: sometimes you might be, and you won't even know it.
Clay can certainly behave just like an ExpandoObject ... but then it can behave like a lot of other things as well. It lets you attach new behaviours to it to redefine what happens when you call dynamic properties and methods on it. You can quite literally mould Clay into anything.
A common example of a Clay behaviour can be observed if you've ever implemented a ContentPartDriver. When you return your factory result you invoke a dynamic method on a shape factory:
()=>shapeHelper.Parts_MyPart(Param:"Foo",Param2:"Bar")
That call could be getting dispatched off to theoretically any number of Clay behaviours; but in the of the shapeHelper (which is an injected IShapeFactory) it's quite simple and only has a few behaviours, so it's a good example to start off with. Let's look at the behaviour that handles this particular case:
class ShapeFactoryBehavior : ClayBehavior {
public override object InvokeMember(Func<object> proceed, object target, string name, INamedEnumerable<object> args) {
return ((DefaultShapeFactory)target).Create(name, args);
}
}
The ClayBehavior class has a number of methods to override, and one of them is InvokeMember. This gets fired whenever you attempt to call a dynamic method on a Clay object. What's happening here is quite straightforward; I won't detail the Create method it's calling because it's quite long-winded (it fires the various Shape Table events you might have already seen and constructs a new Clay object for the shape itself) but the parameters of InvokeMember (which are mostly the same as all ClayBehavior overrides) might need some explanation:
proceed. It's important to note that a Clay object can have any number of behaviours. Just because your behaviour hasn't handled a particular call doesn't mean there aren't other behaviours in the queue that might handle it. A delegate is provided for the purpose of carrying on to test other behaviours; usually you'll want to return proceed() at the end of your override if none of your own logic has matched. In this case, the ShapeFactoryBehavior is a "catch all" for any method invoke so it doesn't call proceed.
target. This is the parent object, i.e. the Clay instance itself. Since you never know exactly what parent object your behaviour is attached to (it could be a completely different implementer of IClayBehaviorProvider, for instance) the two safe things to cast this object to are a) IClayBehaviorProvider or b) dynamic. Examples of both will follow.
name. This is simply the name of the method being invoked, the property being accessed, etc. Some behaviours might match specific names (e.g. "Add" for an array behaviour or "Zones" for a zones behaviour), whereas others might look things up in a string dictionary (which is how the ExpandoObject-like behaviour is implemented), and others will may pass the string onto another call, as here with ShapeFactoryBehavior.
args. This is a specialised collection containing the arguments that were passed to the method call. It has two important sub collections: Positional and Named. If I were to call shape.Foo("Bar","Car",Dog:"Woof",Cat:"Meow") then "Bar" and "Car" would be in Positional and Dog and Cat would be key/value pairs in Named.
Most ClayBehavior overrides expose at least proceed, target and name.
The Twilight Zone(s)
Here's something else you're even more likely to have seen; but the mechanism is even more obscured.
In your Layout.cshtml you may very well have seen statements such as these:
@Zone(Model.Content)
At the beginning of Layout.cshtml, this "Zone" call is actually defined as:
Func<dynamic, dynamic> Zone = x => Display(x);
So really it's just a shortcut (for visual clarity) to a perfectly normal Display(Model.Content) call.
What's so special about these shapes on the model that they are Zones and not ordinary shapes? Actually it's something special about the model itself. The model is a shape (the Layout shape, although you'll see the same thing in other templates such as Content or Widgets). These shapes have a special behaviour added to them when they're being constructed: the ZoneHoldingBehavior.
Now, this is a somewhat more complicated beast than the ShapeFactoryBehavior so I'll first explain loosely what it does. When you define a destination in a Placement file, or set a widget's target zone, the shape ends up being added to the parent shape (i.e. Layout or Content) with a call like this:
shape.Zones["MyCustomZoneName"].Add(newShape,"1");
This call could easily be rewritten as the following:
shape.MyCustomZoneName.Add(newShape,"1");
The purpose of the Zones[] accessor is so the zone can be accessed via a string variable instead of a dynamic call, in all other respects both these versions do exactly the same thing. What they'll do is create a zone if it doesn't already exist, and add the shape at the desired position.
I should just point out a behavioural examples; assuming the zone "CustomZone" does not exist yet:
if (shape.CustomZone == null) { // returns true
Zone zone = shape.CustomZone; // throws an Exception!
shape.CustomZone.Add(myShape,"1");
Zone zone = shape.CustomZone; // Will now work!
}
What is going on here and how does it all work? It's all in ZoneHoldingBehavior. I'll go through some parts of it:
public class ZoneHoldingBehavior : ClayBehavior {
private readonly Func<dynamic> _zoneFactory;
public ZoneHoldingBehavior(Func<dynamic> zoneFactory) {
_zoneFactory = zoneFactory;
}
public override object GetMember(Func<object> proceed, object self, string name) {
if (name == "Zones") {
// provide a robot for zone manipulation on parent object
return ClayActivator.CreateInstance(new IClayBehavior[] {
new InterfaceProxyBehavior(),
new ZonesBehavior(_zoneFactory, self)
});
}
var result = proceed();
if (((dynamic)result) == null) {
// substitute nil results with a robot that turns adds a zone on
// the parent when .Add is invoked
return ClayActivator.CreateInstance(new IClayBehavior[] {
new InterfaceProxyBehavior(),
new NilBehavior(),
new ZoneOnDemandBehavior(_zoneFactory, self, name)
});
}
return result;
}
}
Firstly, it takes as a constructor a delegate function which can be used to produce a zone shape. This is because different regions of the page have different types of zone shape which render slightly differently (DocumentZone, ContentZone). When the behaviour is created a factory must be passed in to generate the correct shape.
The behaviour only overrides a single operation: GetMember. First it checks for a single named member "Zones" - this is how out that string-keyed Zone collection works. This is provided via a ZonesBehavior that works almost identically to the ZoneBehavior itself.
Failing that, it checks the result of the proceed() delegate. This means that any other behaviours existing in the Clay object have a chance to return a result; this checks both if the named Zone already exists, and also if any other properties of the same name exist. Finally, having found nothing, a "virtual" zone behaviour is returned. The zone hasn't been created yet - instead we have a ZoneOnDemand that will be instance a zone as soon as any of its operations are called. The ClayActivator.CreateInstance method is a quick way to return a set of Clay behaviours for shaping the behaviour of a return value.
First let's look at the ZonesBehavior:
public class ZonesBehavior : ClayBehavior {
private readonly Func<dynamic> _zoneFactory;
private readonly object _parent;
public ZonesBehavior(Func<dynamic> zoneFactory, object parent) {
_zoneFactory = zoneFactory;
_parent = parent;
}
public override object GetMember(Func<object> proceed, object self, string name) {
var parentMember = ((dynamic)_parent)[name];
if (parentMember == null) {
return ClayActivator.CreateInstance(new IClayBehavior[] {
new InterfaceProxyBehavior(),
new NilBehavior(),
new ZoneOnDemandBehavior(_zoneFactory, _parent, name)
});
}
return parentMember;
}
public override object GetIndex(Func<object> proceed, object self, System.Collections.Generic.IEnumerable<object> keys) {
if (keys.Count() == 1) {
return GetMember(proceed, null, System.Convert.ToString(keys.Single()));
}
return proceed();
}
}
As you see, this will return the same ZoneOnDemandBehavior if any if its members are called. So Model.Zones.MyZone is the same as saying Model.MyZone ... except that there's no check for proceed() ... this wells us we can use the Zones property to force creation of a zone when a property already exists.
This behaviour also overrides GetIndex. This is the dictionary/indexer override for []. This is how the Zones["Foo"] call is mapped to a GetMember call so Zones["Foo"] is the same as Zones.Foo. The keys parameter is an enumerable because we might have written Zones["Foo","Bar"] (and note that this won't be handled by the behaviour because it checks explicitly for a keys length of 1).
Finally the really interesting behaviour is ZoneOnDemandBehavior:
public class ZoneOnDemandBehavior : ClayBehavior {
private readonly Func<dynamic> _zoneFactory;
private readonly object _parent;
private readonly string _potentialZoneName;
public ZoneOnDemandBehavior(Func<dynamic> zoneFactory, object parent, string potentialZoneName) {
_zoneFactory = zoneFactory;
_parent = parent;
_potentialZoneName = potentialZoneName;
}
public override object InvokeMember(Func<object> proceed, object self, string name, INamedEnumerable<object> args) {
var argsCount = args.Count();
if (name == "Add" && (argsCount == 1 || argsCount == 2)) {
// pszmyd: Ignore null shapes
if (args.First() == null)
return _parent;
dynamic parent = _parent;
dynamic zone = _zoneFactory();
zone.Parent = _parent;
zone.ZoneName = _potentialZoneName;
parent[_potentialZoneName] = zone;
if (argsCount == 1)
return zone.Add(args.Single());
return zone.Add(args.First(), (string)args.Last());
}
return proceed();
}
}
Here's where we check for an invocation of the "Add" method, and if so (with the correct number of arguments) we invoke the zone factory, and actually add the zone to the parent, using the name and parent referenced which were passed down when the ZoneOnDemandBehavior was constructed.
And this is why I mentioned that:
Zone zone = shape.CustomZone; // throws an *Exception*!
In this case a zone hasn't been created yet. We have only reached as far as the ZoneOnDemand behaviour and, well, haven't demanded anything of it which would trigger creation. Clay attempts to perform a cast to a Zone and there simply isn't one there.
Summary
I hope this has been a good introduction to the inner workings of Clay. In a future article I'll explore some of the concepts a bit further and provide some brand new examples of behaviors.