Writing a custom Condition for Orchard - checking the tags on a page

I've previously written about how I've added a standardised warning banner to certain pages in Orchard, so that I can always show something like the text in the image below when I've written a post where I've completley ignored best-practice so that I can get on with doing/explaining/showing something.

The problem that I had with this was that I had to write a long, and ever growing Layer Rule to assign this warning banner to the pages that I wanted it on, like:

url("~/blog/creating-your-own-certificates-for-development-test-environments-part1") or url("~/blog/creating-your-own-certificates-for-development-test-environments-part-2")

What I really wanted was the ability to display the banner to be dependent on the tags present on the blog post, so I could add a "not best practice" tag to all the relevant entries and have the banner auto-magically appear. As I was running on Orchard 1.9.x up until a few days ago, I'd been leaving this as it looks like the way layer rules have been implemented has changed, as the release notes say:

Layer rules have been moved to Orchard.Conditions for reusability

Now that I've upgraded to 1.10.x I've implemented a Tag based condition which I'm moving to using.

Implementing a Condition that checks tags

To do this I followed the instructions for creating a module as far as step 11 in "Command line scaffolding with orchard.exe" to get the basic project layout I needed, along with all the appropriate references, etc, to allow my new module to work. The next thing to do was to create a class that implemented Orchard.Conditions.Services.IConditionProvider, which is the interface that the built-in conditions (for checking against the authenticated state of the user, or the url of the current request) implement.

To check the tags that the current page/item has associated with it, there's one key piece of information I need: What is the current content item? I found some code on Codeplex (yes, it's still there, for now!) that shows how to achieve that which I distilled down to the following method:

private IContent GetContentItemForCurrentRequest()
{
    var requestUrl = _workContextAccessor.GetContext().HttpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(1).Trim('/');
    var requestItemRoute = _aliasService.Get(requestUrl);

    return requestItemRoute == null ? _contentManager.New("Dummy") : _contentManager.Get(Convert.ToInt32(requestItemRoute["Id"]));
}

This works by retrieving the URL of the current request and then asking the Orchard AliasService to resolve it back to a "real" Orchard URL/Route - you can see what these are by looking in the "Aliases" module in /Admin, for example the URL http://robertwray.co.uk/blog/a-standardised-warning-banner-in-orchard gets mapped down to http://robertwray.co.uk/Contents/Item/Display/3434 and both will show you the same content.

This is necessary so that I can ask the Orchard ContentManager (the last line of that method) for the content item associated with that id (it only allows you to retrieve items by id). Once that's done, it's a relatively trivial exercise to get the tags associated with the item and check to see whether the tag that has been passed into the Condition matches any of the tags associated with the content item:

if (_currentContent.Has<TagsPart>())
{
    var tagPart = _currentContent.Get<TagsPart>();
    evaluationContext.Result = tagPart.CurrentTags != null && tagPart.CurrentTags.Any(tag => tag.Equals(tagName, StringComparison.OrdinalIgnoreCase));
}
else
{
    evaluationContext.Result = false;
}

A little bit of defensive coding there, just on the off-chance that this gets applied to something that doesn't have any tags (though I'm not sure if .Get<TagPart> would just return an empty / null set in that instance) because I don't want pages in my Orchard site blowing up in that case.

Putting it all together gives a class in a module that can be loaded into Orchard and used with layer rules like "tag("not best practice")":

using Orchard;
using Orchard.Alias;
using Orchard.Conditions.Services;
using Orchard.ContentManagement;
using Orchard.Tags.Models;
using System;
using System.Linq;

namespace RobertWray.Conditions.Providers
{
    public sealed class TagCondition : IConditionProvider
    {
        private readonly IAliasService _aliasService;
        private readonly IWorkContextAccessor _workContextAccessor;
        private readonly IContentManager _contentManager;
        public TagCondition(IAliasService aliasService, IWorkContextAccessor workContextAccessor, IContentManager contentManager)
        {
            _aliasService = aliasService;
            _workContextAccessor = workContextAccessor;
            _contentManager = contentManager;
        }
        public void Evaluate(ConditionEvaluationContext evaluationContext)
        {
            if (!string.Equals(evaluationContext.FunctionName, "tag", StringComparison.OrdinalIgnoreCase))
            {
                return;
            }

            var tagName = Convert.ToString(evaluationContext.Arguments[0]);

            var _currentContent = GetContentItemForCurrentRequest();

            if (_currentContent.Has())
            {
                var tagPart = _currentContent.Get();
                evaluationContext.Result = tagPart.CurrentTags != null && tagPart.CurrentTags.Any(tag => tag.Equals(tagName, StringComparison.OrdinalIgnoreCase));
            }
            else
            {
                evaluationContext.Result = false;
            }
        }

        private IContent GetContentItemForCurrentRequest()
        {
            var requestUrl = _workContextAccessor.GetContext().HttpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(1).Trim('/');
            var requestItemRoute = _aliasService.Get(requestUrl);

            return requestItemRoute == null ? _contentManager.New("Dummy") : _contentManager.Get(Convert.ToInt32(requestItemRoute["Id"]));
        }
    }
}

I'll add another post when I've got this all tidied up and uploaded to GitHub / the Orchard gallery so that others can use it!

About Rob

I've been interested in computing since the day my Dad purchased his first business PC (an Amstrad PC 1640 for anyone interested) which introduced me to MS-DOS batch programming and BASIC.

My skillset has matured somewhat since then, which you'll probably see from the posts here. You can read a bit more about me on the about page of the site, or check out some of the other posts on my areas of interest.

1 Comment

Add a Comment