Tales from the Evil Empire

Bertrand Le Roy's blog

News


Bertrand Le Roy

BoudinFatal's Gamercard

Tales from the Evil Empire - Blogged

Blogs I read

My other stuff

Archives

June 2011 - Posts

So what are zones really?

(c) Bertrand Le Roy 2010There is a (not so) particular kind of shape in Orchard: zones. Functionally, zones are places where other shapes can render. There are top-level zones, the ones defined on Layout, where widgets typically go, and there are local zones that can be defined anywhere. These local zones are what you target in placement.info.

Creating a zone is easy because it really is just an empty shape. Most themes include a helper for it:

Func<dynamic, dynamic> Zone = x => Display(x);

With this helper, you can create a zone by simply writing:

@Zone(Model.Header)

Let's deconstruct what's happening here with that weird Lambda. In the Layout template where we are working, the Model is the Layout shape itself, so Model.Header is really creating a new Header shape under Layout, or getting a reference to it if it already exists. The Zone function is then called on that object, which is equivalent to calling Display. In other words, you could have just written the following to get the exact same effect:

@Display(Model.Header)

The Zone helper function only exists to make the intent very explicit. Now here's something interesting: while this works in the Layout template, you can also make it work from any deeper-nested template and still create top-level zones. The difference is that wherever you are, Model is not the layout anymore so you need to access it in a different way:

@Display(WorkContext.Layout.Header)

This is still doing the exact same thing as above.

One thing to know is that for top-level zones to be usable from the widget editing UI, you need one more thing, which is to specify it in the theme's manifest:

Name: Contoso
Author: The Orchard Team
Description: A subtle and simple CMS theme
Version: 1.1 Tags: business, cms, modern, simple, subtle, product, service Website: http://www.orchardproject.net Zones: Header, Navigation, HomeFeaturedImage, HomeFeaturedHeadline,
Messages, Content, ContentAside, TripelFirst, TripelSecond,
TripelThird, Footer

Local zones are just ordinary shapes like global zones, the only difference being that they are created on a deeper shape than layout. For example, in Content.cshtml, you can find our good old code fro creating a header zone:

@Display(Model.Header)

The difference here is that Model is no longer the Layout shape, so that zone will be local. The name of that local zone is what you specify in placement.info, for example:

<Place Parts_Common_Metadata_Summary="Header:1"/>

Now here's the really interesting part: zones do not even know that they are zones, and in fact any shape can be substituted. That means that if you want to add new shapes to the shape that some part has been emitting from its driver for example, you can absolutely do that. And because zones are so barebones as shapes go, they can be created the first time they are accessed. This is what enables us to add shapes into a zone before the code that you would think creates it has even run. For example, in the Layout.cshtml template in TheThemeMachine, the BadgeOfHonor shape is being injected into the Footer zone on line 47, even though that zone will really be "created" on line 168.

Creating shapes on the fly

(c) Bertrand Le Roy 2011Most Orchard shapes get created from part drivers, but they are a lot more versatile than that. They can actually be created from pretty much anywhere, including from templates. One example can be found in the Layout.cshtml file of the ThemeMachine theme:

WorkContext.Layout.Footer
.Add(New.BadgeOfHonor(), "5");

What this is really doing is create a new shape called BadgeOfHonor and injecting it into the Footer global zone (that has not yet been defined, which in itself is quite awesome) with an ordering rank of "5".

We can actually come up with something simpler, if we want to render the shape inline instead of sending it into a zone:

@Display(New.BadgeOfHonor())

Now let's try something a little more elaborate and create a new shape for displaying a date and time:

@Display(New.DateTime(date: DateTime.Now, format: "d/M/yyyy"))

For an even shorter syntax, we can even use Display directly to create the shape:

@Display.DateTime(date: DateTime.Now, format: "d/M/yyyy")

For the moment, this throws a "Shape type DateTime not found" exception because the system has no clue how to render a shape called "DateTime" yet. The BadgeOfHonor shape above was rendering something because there is a template for it in the theme: Themes/ThethemeMachine/Views/BadgeOfHonor.cshtml. We need to provide a template for our new shape to get rendered. Let's add a DateTime.cshtml file into our theme's Views folder in order to make the exception go away:

Hi, I'm a date time shape.

Now we're just missing one thing. Instead of displaying some static text, which is not very interesting, we can display the actual time that got passed into the shape's dynamic constructor. Those parameters will get added to the template's Model, so they are easy to retrieve:

@(((DateTime)Model.date).ToString(Model.format))

Now that may remind you a little of WebForm's user controls. That's a fair comparison, except that these shapes are much more flexible (you can add properties on the fly as necessary), and that the actual rendering is decoupled from the "control". For example, any theme can override the template for a shape, you can use alternates, wrappers, etc.

Most importantly, there is no lifecycle and protocol abstraction like there was in WebForms. I think this is a real improvement over previous attempts at similar things.

Adding RSS to tags in Orchard

Water lenses (c) Bertrand Le Roy 2005A year ago, I wrote a scary post about RSS in Orchard. RSS was one of the first features we implemented in our CMS, and it has stood the test of time rather well, but the post was explaining things at a level that was probably too abstract whereas my readers were expecting something a little more practical.

Well, this post is going to correct this by showing how I built a module that adds RSS feeds for each tag on the site. Hopefully it will show that it's not very complicated in practice, and also that the infrastructure is pretty well thought out.

In order to provide RSS, we need to do two things: generate the XML for the feed, and inject the address of that feed into the existing tag listing page, in order to make the feed discoverable.

Let's start with the discoverability part. One might be tempted to replace the controller or the view that are responsible for the listing of the items under a tag. Fortunately, there is no need to do any of that, and we can be a lot less obtrusive. Instead, we can implement a filter:

public class TagRssFilter : FilterProvider, IResultFilter

On this filter, we can implement the OnResultExecuting method and simply check whether the current request is targeting the list of items under a tag. If that is the case, we can just register our new feed:

public void OnResultExecuting(ResultExecutingContext filterContext) {
    var routeValues = filterContext.RouteData.Values;
    if (routeValues["area"] as string == "Orchard.Tags" &&
        routeValues["controller"] as string == "Home" &&
        routeValues["action"] as string == "Search") {

        var tag = routeValues["tagName"] as string;
        if (! string.IsNullOrWhiteSpace(tag)) {
            var workContext = _wca.GetContext();
            _feedManager.Register(
                workContext.CurrentSite + " – " + tag,
                "rss",
                new RouteValueDictionary { { "tag", tag } } );
        }
    }
}

The registration of the new feed is just specifying the title of the feed, its format (RSS) and the parameters that it will need (the tag). _wca and _feedManager are just instances of IWorkContextAccessor and IFeedManager that Orchard injected for us. That is all that's needed to get the following tag to be added to the head of our page, without touching an existing controller or view:

<link rel="alternate" type="application/rss+xml"
title="VuLu - Science" href="/rss?tag=Science"/>

Nifty.

Of course, if we navigate to the URL of that feed, we'll get a 404. This is because no implementation of IFeedQueryProvider knows about the tag parameter yet. Let's build one that does:

public class TagFeedQuery : IFeedQueryProvider, IFeedQuery

IFeedQueryProvider has one method, Match, that we can implement to take over any feed request that has a tag parameter:

public FeedQueryMatch Match(FeedContext context) {
    var tagName = context.ValueProvider.GetValue("tag");
    if (tagName == null) return null;

    return new FeedQueryMatch { FeedQuery = this, Priority = -5 };
}

This is just saying that if there is a tag parameter, we will handle it.

All that remains to be done is the actual building of the feed now that we have accepted to handle it. This is done by implementing the Execute method of the IFeedQuery interface:

public void Execute(FeedContext context) {
    var tagValue = context.ValueProvider.GetValue("tag");
    if (tagValue == null) return;

    var tagName = (string)tagValue.ConvertTo(typeof (string));
    var tag = _tagService.GetTagByName(tagName);
    if (tag == null) return;

    var site = _services.WorkContext.CurrentSite;

    var link = new XElement("link");
    context.Response.Element.SetElementValue("title",
site.SiteName + " - " + tagName); context.Response.Element.Add(link); context.Response.Element.SetElementValue("description",
site.SiteName + " - " + tagName); context.Response.Contextualize(requestContext =>
link.Add(GetTagUrl(tagName, requestContext))); var items = _tagService.GetTaggedContentItems(tag.Id, 0, 20); foreach (var item in items) { context.Builder.AddItem(context, item.ContentItem); } }

This code is resolving the tag content item from its name and then gets content items tagged with it, using the tag services provided by the Orchard.Tags module. Then we add those items to the feed.

And that is it. To summarize, we handled the request unobtrusively in order to inject the feed's link, then handled requests for feeds with a tag parameter and generated the list of items for that tag. It remains fairly simple and still it is able to handle arbitrary content types. That makes me quite happy about our little piece of over-engineered code from last year.

The full code for this can be found in the Vandelay.TagCloud module:
http://orchardproject.net/gallery/List/Modules/
Orchard.Module.Vandelay.TagCloud/1.2

UPDATE: I just got a question about how to use this if you are not a developer. Well, if all you want is RSS for tags, just install the Vandelay.TagClouds module and enable the Tag RSS feature. That's it.

Getting started with custom themes in Orchard
More Posts