Query ContentItems by Taxonomy - using Tags

Topics: Customizing Orchard, Writing modules
Feb 14, 2015 at 6:09 AM
I've been hacking with Orchard for a while, but the sheer size and complexity of Orchard has been a bit of a whipping. I get punished by things that I think should be easy that end up having complex solutions, but am totally drunk on the power and possibility that Orchard provides. I bang my head against the wall for a couple of days but once I solve the problem, my mind opens up to a bunch of new possibilities and a better understanding of why Orchard is the way it is.

I have an interesting use case and would like to get some feedback on my code to see if I'm doing things in an optimal fashion thinking in terms of site speed as well as doing it the "Orchard way". I'm also posting because I'm using the TagService and TaxonomyService and since I pulled my code from a dozen different partial examples I found online, maybe someone else will benefit from my code and thought process.

Here's the scenario
  • I have custom ContentType called Article
  • The Article ContentType has a custom Taxonomy called "Article Type"
  • I have Pages that I use URL Alternates to override and create some static page content
  • I am to creating a "Related Content" widget to show a list of Article Content Items relating to a Page
  • I want to use the "Related Content" widget on an Article ContentItem as well to show related articles
Here's what I've done:
  • When I created the Page content item, I tag the Page with a tag that happens to have the same name.
  • I've created a Zone where my "Related Content" widget should live on my URL Alternate for the specified page.
When the Display driver is invoked for the "Related Content" widget, I query the Tags first to see if I'm currently on a Page with Tags specified. If no Tags are found, I query the ContentItem for any defined Terms. Whatever terms/tags I find are then queries against the "Article Type" Taxonomy to find any Article Content Items showing those terms/tags.

Here's my code:
protected override DriverResult Display(RelatedContentWidgetPart part, string displayType, dynamic shapeHelper)
        {
            var contentId = GetCurrentContentItemId();
            
            ContentItem currentContent = null;
            IEnumerable<string> tags = null;            

            if (contentId != null)
            {
                // find any tags associated with the ContentItem
                currentContent = _contentManager.Get(contentId.Value);
                tags = currentContent.As<TagsPart>().CurrentTags;
            }

            List<TermPart> taxonomies = new List<TermPart>();
            List<string> pageTags = tags.ToList<string>();
            if ((pageTags.Count == 0) && (contentId != null))
            {
                // no tags are associated with the contentItem - look for taxonomies instead
                IEnumerable<TermPart> taxonomyTags = _taxonomyService.GetTermsForContentItem(contentId.Value);
                foreach (TermPart term in taxonomyTags)
                {
                    pageTags.Add(term.ToString());
                }
            }
            else
            {
                // we found tags - convert them to TermParts in order to match an article
                TaxonomyPart taxonomyPart = _taxonomyService.GetTaxonomyByName("Article Type");
                foreach (string sTag in pageTags)
                {
                    TermPart term = _taxonomyService.GetTermByName(taxonomyPart.Id, sTag);
                    taxonomies.Add(term);
                }
            }

            if (taxonomies.Count == 0)
            {
                // return an empty widget - there are no tags or taxonomies to match
                return ContentShape("Parts_RelatedContentWidget",
                        () => shapeHelper.Parts_RelatedContentWidget(
                                ContentItems: shapeHelper.List()
                                ));
            }

            List<ContentItem> contentItems = new List<ContentItem>();
            foreach (TermPart term in taxonomies)
            {
                // go find the content that matches the taxonomy
                IEnumerable<IContent> matchedContentItems = _taxonomyService.GetContentItems(term);
                foreach (IContent content in matchedContentItems)
                {
                    contentItems.Add(content.ContentItem);
                }
            }
            
            // Create a list and push our display content items in
            var list = shapeHelper.List();
            list.AddRange(contentItems.Select(p => _contentManager.BuildDisplay(p, "Summary")));
     
            return ContentShape("Parts_RelatedContentWidget",
                () => shapeHelper.Parts_RelatedContentWidget(
                        ContentItems : list
                        ));
        }

        private int? GetCurrentContentItemId()
        {
            object id;
            if (_requestContext.RouteData.Values.TryGetValue("id", out id))
            {
                int contentId;
                if (int.TryParse(id as string, out contentId))
                    return contentId;
            }

            return null;
        }
Feb 17, 2015 at 5:58 AM
There were a few bugs in that previous implementation that wouldn't give you the correct result, so I've correct them here and commented inline.

To summarize, this code queries for any associated Tags with a Content Item, if none are found queries for any associated Taxonomies, and then finds all of the Content that has been tagged with the specified tags/taxonomies within the "Article Type" taxonomy.

The goal was to be able to take an existing Page Content Item, give it a Tag, and then query for any Article (a custom Content Type) Content Items that might have been created with that should be displayed on the Page Content Item.

If you don't want the current Content Item listed in the results, you could always check for it before adding it to the contentItems List.
protected override DriverResult Display(RelatedContentWidgetPart part, string displayType, dynamic shapeHelper)
        {
            // get the ContentItem, find associated tags/taxonomies, and 
            var contentId = GetCurrentContentItemId();
            
            ContentItem currentContent = null;
            IEnumerable<string> tags = null;            

            if (contentId != null)
            {
                // find any tags associated with the ContentItem
                currentContent = _contentManager.Get(contentId.Value);

                try
                {
                    tags = currentContent.As<TagsPart>().CurrentTags;
                }
                catch (Exception e)
                {
                    // if the content doesn't have a Tags part, the previous cast will throw an exception - just let it fall through
                    string sError = e.Message;
                }
            }

            List<TermPart> taxonomies = new List<TermPart>();
            List<string> pageTags = new List<string>();
            if (tags != null)
            {
                pageTags = tags.ToList<string>();
            }

            if ((pageTags.Count == 0) && (contentId != null))
            {
                // no tags are associated with the contentItem - look to see if there are any taxonomies instead
                IEnumerable<TermPart> taxonomyTags = _taxonomyService.GetTermsForContentItem(contentId.Value);
                foreach (TermPart term in taxonomyTags)
                {                    
                    pageTags.Add(term.Name);
                }
            }

            if (pageTags.Count > 0)
            {
                // we found tags - convert them to TermParts in order to match an article
                TaxonomyPart taxonomyPart = _taxonomyService.GetTaxonomyByName("Article Type");
                foreach (string sTag in pageTags)
                {
                    TermPart term = _taxonomyService.GetTermByName(taxonomyPart.Id, sTag);
                    taxonomies.Add(term);
                }
            }

            if (taxonomies.Count == 0)
            {
                // return an empty widget - there are no tags or taxonomies to query for
                return ContentShape("Parts_RelatedContentWidget",
                        () => shapeHelper.Parts_RelatedContentWidget(
                                ContentItems: shapeHelper.List()
                                ));
            }

            List<ContentItem> contentItems = new List<ContentItem>();
            foreach (TermPart term in taxonomies)
            {
                // go find the ContentItems that match the specified taxonomy
                IEnumerable<IContent> matchedContentItems = _taxonomyService.GetContentItems(term);
                foreach (IContent content in matchedContentItems)
                {
                    contentItems.Add(content.ContentItem);
                }
            }
            
            // Create a list and push our display content items in
            var list = shapeHelper.List();
            list.AddRange(contentItems.Select(p => _contentManager.BuildDisplay(p, "SummaryListing")));
     
            return ContentShape("Parts_RelatedContentWidget",
                () => shapeHelper.Parts_RelatedContentWidget(
                        ContentItems : list
                        ));
        }

        // see if there's an ID value in the request - this will be the ContentItem ID
        private int? GetCurrentContentItemId()
        {
            object id;
            if (_requestContext.RouteData.Values.TryGetValue("id", out id))
            {
                int contentId;
                if (int.TryParse(id as string, out contentId))
                    return contentId;
            }

            return null;
        }