OrchardCMS: Advanced Placement.info Matching

Tags: orchardcms, patient

Orchard's Placement.info is a great tool for configuring where and how to display shapes based on certain conditions, out of the box though these conditions are pretty limited (ContentType/DisplayType/ContentPart and Path). At this point i'm going to assume that you know what Placement.info is and how to use the match tags, if not checkout the documentation at http://docs.orchardproject.net/en/latest/Documentation/Understanding-placement-info.

Now for most cases these basic match conditions work fine, but over the years i've come across multiple cases where that is not the case and just worked around it (fudged something). A few weeks ago however in some work i was doing in the day job @patient this problem reared it's head again in such a way a work-around wasn't possible. Basically i needed to specifiy different placement if a certain value on a part was set to true; the current placement match providers were vastly ill equiped in solving this; as was every other out of the box trick i could think of, so i delved into the orchard source to re-acquaint myself with how the current match parsing worked, sadly it all came flooding back to me that it was some horribly non-extendable switch statement. The code can be found here. or below is the code.

public static Func<ShapePlacementContext, bool> BuildPredicate(Func<ShapePlacementContext, bool> predicate, KeyValuePair<string, string> term) {
   var expression = term.Value;
   switch (term.Key) {
                case "ContentPart":
                        return ctx => ctx.Content != null 
                            && ctx.Content.ContentItem.Parts.Any(part => part.PartDefinition.Name == expression) 
                            && predicate(ctx);
                case "ContentType":
                    if (expression.EndsWith("*")) {
                        var prefix = expression.Substring(0, expression.Length - 1);
                        return ctx => ((ctx.ContentType ?? "").StartsWith(prefix) || (ctx.Stereotype ?? "").StartsWith(prefix)) && predicate(ctx);
                    }
                    return ctx => ((ctx.ContentType == expression) || (ctx.Stereotype == expression)) && predicate(ctx);
                case "DisplayType":
                    if (expression.EndsWith("*")) {
                        var prefix = expression.Substring(0, expression.Length - 1);
                        return ctx => (ctx.DisplayType ?? "").StartsWith(prefix) && predicate(ctx);
                    }
                    return ctx => (ctx.DisplayType == expression) && predicate(ctx);
                case "Path":
                    var normalizedPath = VirtualPathUtility.IsAbsolute(expression)
                                             ? VirtualPathUtility.ToAppRelative(expression)
                                             : VirtualPathUtility.Combine("~/", expression);

                    if (normalizedPath.EndsWith("*")) {
                        var prefix = normalizedPath.Substring(0, normalizedPath.Length - 1);
                        return ctx => VirtualPathUtility.ToAppRelative(String.IsNullOrEmpty(ctx.Path) ? "/" : ctx.Path).StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && predicate(ctx);
                    }

                    normalizedPath = VirtualPathUtility.AppendTrailingSlash(normalizedPath);
                    return ctx => (ctx.Path.Equals(normalizedPath, StringComparison.OrdinalIgnoreCase)) && predicate(ctx);
            }
    return predicate;
}

Note: case statements make carl sad.

After staring at this a while i decided the solution was to replace each of these case's with an implementation of a match provider interface, which we'd simply loop through and evaluate if it was a match. Leaving us with the below interface.

namespace Orchard.DisplayManagement.Descriptors.ShapePlacementStrategy {
    public interface IPlacementParseMatchProvider : IDependency {
        string Key { get; }
        bool Match(ShapePlacementContext context, string expression);
    }
}

the new build predicate method

public static Func<ShapePlacementContext, bool> BuildPredicate(Func<ShapePlacementContext, bool> predicate, 
                KeyValuePair<string, string> term, IEnumerable<IPlacementParseMatchProvider> placementMatchProviders) {

            if (placementMatchProviders != null) {
                var providersForTerm = placementMatchProviders.Where(x => x.Key.Equals(term.Key));
                if (providersForTerm.Any()) {
                    var expression = term.Value;
                    return ctx => providersForTerm.Any(x => x.Match(ctx, expression)) && predicate(ctx);
                }
            }
            return predicate;
        }

and an example implementation of the interface; implementing one of the old case statements.

public class DisplayTypePlacementParseMatchProvider : IPlacementParseMatchProvider {
        public string Key { get { return "DisplayType"; } }

        public bool Match(ShapePlacementContext context, string expression) {
            if (expression.EndsWith("*")) {
                var prefix = expression.Substring(0, expression.Length - 1);
                return (context.DisplayType ?? "").StartsWith(prefix);
            }

            return context.DisplayType == expression;
        }
    }

Basically the key is the name of the match attribute <Match [key]> this replaces the "case" and the match statement does what used to occur inside the case. Importantly however anyone can now simply implement their own IPlacementMatchProvider and hook into the placement parsing api and they can create as many different ones as they like! the shackles are off.

For example, in order to solve my original problem of returning a match if a certain value was set on a part we'd have the following implementation.

namespace SomeNamespace.Providers {
    public class PlacementParseStrategyMatchProvider : IPlacementParseStategyMatchProvider {
        public string Key { get { return "CarlsPartPropertyValueCheck"; } }

        public bool IsMatch(ShapePlacementContext context, string expression) {
            var contentItem = context.Content.ContentItem;
            return contentItem.Has<CarlsPart>() && contentItem.As<CarlsPart>().CarlsProperty == expression;
        }
    }
}

and the placement would look like this

<Placement>
<Match CarlsPartPropertyValueCheck="Magic">
....
</Match>
</Placement>

Now if a contentitem has CarlsPart and CarlsProperty is set to "Magic" we'll get a placement match. 

I think this really opens up the placement parsing api, now we can all do crazy placement like per tenant placement ;) the good news is a PR has been submitted and approved for this so it should be in the next release of Orchard! 

Hope you all find it useful.

Comments

comments powered by Disqus