Content Summaries with HtmlAgilityPack

Recently I had a client ask me to modify their Blog page to list only the first paragraph and the first image found in each of their posts instead of the full post on the default Blog Post List.

The site (http://deancambray.com.au) was built with Umbraco and utilises the Blog4Umbraco package along with our Extensions package.  The solution that we came up with was to use the already available HtmlAgilityPack that's included in the Umbraco distribution and write our own Razor Script to list the Blog Posts.

While the entire script also incorporates other features such as a custom numeric pager, I wanted to focus this time on just extracting certain elements out of each post and displaying them in a customised format.

The helper: RenderSummary

First things first: make sure you have a reference to the HtmlAgilityPack library near the top of the script:

@using HtmlAgilityPack;

Our helper looks like this:

@helper RenderSummary(dynamic node) {
    var doc = new HtmlDocument();
    doc.LoadHtml(node.BodyText.ToString());
    var imgNode = doc.DocumentNode.SelectSingleNode("//img[@src]"); 
    if (imgNode != null) {
        var url = imgNode.Attributes["src"].Value;
        string alt = string.Empty;
        string title = string.Empty;
        if (imgNode.Attributes["alt"] != null) { alt = imgNode.Attributes["alt"].Value; }
        if (imgNode.Attributes["title"] != null) { title = imgNode.Attributes["title"].Value; }
<a href="@node.Url" title="Permalink to @node.Name"><img src="@url" alt="@alt" title="@title" /></a>
    }
    var para = doc.DocumentNode.SelectNodes("//p");
    if (para != null) {
        foreach (var p in para) {
            if (string.IsNullOrWhiteSpace(p.InnerText.Replace(" ", ""))) { continue; }
            
            <p>@Html.Raw(p.InnerText)</p>
            break;                                         
        }
    }
}

Our script uses a helper to render the Summary of each post that was found, and instantiates a new HtmlAgilityPack.HtmlDocument for each article by loading the article content using LoadHtml.  Once that's done, we can then use standard xpath queries to select the content that we want.  In this case, we want to find the first image that may be contained in the article and the first non-empty paragraph.

We can check that an image or paragraph exists by the return value of the SelectSingleNode or SelectNodes methods making it very easy to conditionally display the image or a placeholder if desired, for example.

Once we have our image, it's a trivial matter to extract the source url and other attributes using the Attributes collection on the returned HtmlNode and building our custom <img> tag.

Because it is very easy to insert paragraphs through TinyMCE that are empty, we want to find the first paragraph that actually has visible content in it. Otherwise our summary will look very empty indeed.  Once we have found the right paragraph, we can use the InnerText property to extract just the textual elements and ignore things like embedded images, lists and line breaks.  This results in a cleaner display and guarantees that the image (which may be found within the first paragraph) is not shown twice.

Note that you could also use the InnerHtml property instead if you wanted to include the extra format elements and other bits and pieces.

Tying it together

OUr BlogListPosts script is intended to replace the XSLT counterpart provided with Blog4Umbraco, so I've taken the basic structure of that script and tidied it up somewhat for clarity.  I've removed part of it that does the filtering and paging of the list items based on category and/or archive folder.  I wanted to focus on just the Summary rendering, so here's a condensed version of the body of the script featuring the use of the RenderSummary helper defined above:

@{
    var list = Current.DescendantsOrSelf("BlogPost").Items.OrderByDescending(n => n.GetPropertyValue("PostDate"));

    foreach (dynamic post in list)
    {
        <div class="post">
            <h2 class="entry-title"><a href="@post.Url" title="Permalink to @post.Name">@post.Name</a></h2>

            <div class="entry-date">
                <small class="published">@post.PostDate.ToString("dddd, MMM dd, yyyy")</small>
            </div>

            <div class="entry-content summary">
                @RenderSummary(post)
            </div>
            <div class="footer">
                <small class="more"><a href="@post.Url" title="Permalink to @post.Name">Read More...</a></small>
            </div>
        </div>
    }
    
}

Find this post helpful?  Why don't you drop us a line in the comments below...

Retrieving DropDownList Values in Razor or C#

Recently I needed to update the value of an Umbraco DropDownList property in code based on a value instead of the key that's automatically assigned by the Prevalue Editor.  I came across this but it discusses retrieving values for XSLT specifically.  In my scenario I needed to find a specific key.

The simplest way to do this is with XML to Linq.  In the example below I'm using the property's DataTypeDefinition to retrieve the relevant prevalue collection instead of hard-coding the Id of the DropDownList.  This means I have full flexibility in case something changes in the future:

if (p.getProperty("status") != null)
{
    var status = p.getProperty("status");

    status.Value = XElement.Parse(library.GetPreValues(status.PropertyType.DataTypeDefinition.Id).Current.OuterXml)
                           .Descendants("preValue").FirstOrDefault(pv => pv.Value == "On Offer").Attribute("id").Value;

    p.Save();
}

Note I could also have written it like this in Linq notation:

status.Value = (from pv in XElement.Parse(library.GetPreValues(status.PropertyType.DataTypeDefinition.Id).Current.OuterXml).Descendants("preValue")
                       where pv.Value == "On Offer"
                       select pv.Attribute("id").Value).FirstOrDefault();

 That's all there is to it.

Introducing the Umbraco View Counter

Over the last couple of days we've been busy creating an Umbraco package that deals with Content View Counters - it enables the web master to track the number of times content has been viewed on the site.

The Documentation and package has just been uploaded to the Umbraco Project Repository and can be downloaded from here.  This post deals with a few of the features of the package, which was built agains Umbraco 4.7 and dotNet 4.0

Introduction

TheRefactored Content Viewspackage is essentially a content views (number of times  viewed) counter.  The current functionality offered by this package includes:

  • Optional Data Type that allows for configuring view counters with various categories and the ability to instruct Macros etc. to "hide" the view Count yet still increment it.
  • Optional incrementing when displaying the view count (useful when you want to display the view count in a content listing, for example)
  • Example Razor Script and Macro.
  • Library methods to manipulate the counters and retrieve details as an XML fragment for use with XSLT.

Basic Usage.

To simply retrieve and/or increment the counter for a specific content item, call the following library method.  The category and increment parameters are optional, with default values shown initalics:

ViewCount.GetViewCount(nodeId, category: "<empty string>", increment: false);

There is no requirement to configure a DataType; supplying the node id of any valid Content-based node (Member, Document, Media, etc.) will create the Views record in the database if it doesn't exist.  However configuring and using a DataType will allow you to control the advanced features of the counter.

Out of the box

Out of the box you get a default DataType (View Count) and a sample Razor Macro that displays the current View Count of the node being displayed.  If you have set up the Document Type with the View Count DataType, the macro will check whether the View Count should be displayed or not.

Macro Parameters for Page Views:

  • Category (text) - optionally specifies the Category to record the Page Count against.
  • Increment (bool) - set to true to increment the Page Count when the macro is called.

Macro Script Contents:

@inherits umbraco.MacroEngines.DynamicNodeContext
@using umbraco.MacroEngines;
@using umbraco.NodeFactory;
@using Refactored.UmbracoViewCounter;

@if (!ViewCount.HideCounter(Model.Id, category: Parameter.Category)) {
  <span># Views:@ViewCount.GetViewCount(@Model.Id, category: @Parameter.Category, increment: @Parameter.Increment == "1").ToString("N0")</span>
} else {
  ViewCount.Increment(Model.Id, category: Parameter.Category);
}

 Setting up a Data Type

The Data Type has the following Parameters:

View Count DataType

  • Category- Specifyinga different category for multiple DataTypes allows you to differentiate between multiple View Counts in a single content item.  You can then render the content in different views and have a different View Count for each rendering.
  • Hide View Count- Allows you to control (in conjunction with the API and Razor or XSLT macros, for example) whether to hide or show the view count at a Data Type level.
  • Enable View History- Turns on recording of View Count History data including the time the view was incremented.  Also recorded is Reset command events.  This data is stored in the refViewCountHistory table and persists even if the current view count is reset.  This is off by default.
  • Disable Counter Reset- Turning this on disables the Reset action on Content configured with a View Count DataType.

Blog 4 Umbraco Extensions Documentation

Finally, some 8 months after the Blog4Umbraco Extensions library became available, I decided to post it to the package repository on our.umbraco.org and create some actual documentation for it - this is the result...

This document may also be downloaded as a PDF from here.

Introduction

The Refactored Blog4Umbraco Extensions came about because the current version of Blog4Umbraco (currently 2.0.26) had some issues when it came to creating multiple blogs within a single website, and in addition under some circumstances creating a new blog entry would cause a "Yellow Screen of Death" (YSod).

In order to address these shortcomings this package was created, and later extended with other functionality.  The current functionality offered by this package includes:

  • Allowing Comments to be Disabled at the Blog Level
  • Enable setting a Blog-wide Category and having Tags bound to that Category

An additional Datatype called Blog Tags and derived from the built in Tags Datatype is also provided which is the basis upon which the Blog-wide Category is built.

For a more detailed and technical description of the package, the reader is directed to the blog entries found at /blog:

The Future

Work is currently underway to release a version 3 of Blog4Umbraco (B4U) which will address the issues discussed here and add other much needed functionality including Trackbacks and Comment Notifications.

Post-Installation Steps

After installing the package, additional steps are required in order to activate the features.  These involve modifying the Blog-related document types as follows:

Globally Disabling Comments

In order to be able to globally disable comments, edit the Blog Document Type by adding a new property based on the True/False data type as follows:

Blog DisableComments Property 

If the Disable Comments checkbox is checked on a Blog page then the Close comments field will be also become checked when it is saved.

Blog Categories

Updating the Blog Document Type

In order to facilitate Blog Categories, an additional property needs to be added to the Blog Document Type as follows:

BlogCategory 

Coupled with the change to the Blog Post Document type below, this will cause tags added to blog posts to use the category set in this property of the corresponding Blog.

Updating the Blog Post Document Type

Change the Tags property in the Blog Post Document so that it uses the "Blog Tags" type instead of the built-in "Tags" data type:

 blogPost Blog Tags

Enabling Time fields in the Blog Entry Post Date

In the original Blog4Umbraco package, there is no way to enable the Post Date to use time as well as date, which results in all posts being set as being posted at midnight. 

The updated Umlaut.Umb.Blog.dll file included in this package addresses this issue, but you still need to modify the Blog Post document type in order to take advantage of the change.  In order to do so, change the Post Date property type from "Date Picker" to "Date Picker with time":

blogPost PostDate Property 

Other Issues:

Blog for Umbraco generates the following error when attempting to create a new Blog:

Issue # 5612 - http://blog4umbraco.codeplex.com/workitem/5612

Operand type clash: int is incompatible with ntext
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.Data.SqlClient.SqlException: Operand type clash: int is incompatible with ntext

Workaround:

If you encounter this type of error, double-check the Author Picker and make sure that the datatype is set tointinstead ofntext.

Umbraco and JSON-RPC

We recently created a companion iPhone application for a website we released late last year, which needed to retrieve content from the website.  Rather than just retrieve pages of information as html, we wanted to display lists of content, and we also wanted to cache the content on the device for offline viewing.  To do this, it was decided we would use JSON as the transport as it is lightweight.

On the client side, we used the JSON-Framework Cocoa based library created by Stig Brautaset.  On the server side we used JayRock JSON-RPC library for dotNet. Oh, and the website was based on the Umbraco CMS.

Background on JSON-RPC

The JSON-RPC specification is fairly straightforward - it consists of a request, a response, and a notification.  The request sent to the server has 3 parameters as follows:

  • Method Name
  • Parameters, and
  • Id - This is used to associate the request with it's respective reply, and provided it is unique can be used to quarantee that the calling code receives the correct reply for the request - very important if you are sending multiple requests asynchronously.

The response from the server also has 3 parameters:

  • Result object - the result in JSON notation
  • Error object - if an error is thrown by the called method, the details will be contained in this object, including any exceptions thrown, which is very useful for debugging
  • Id - this is the same as the id passed in with the Request.

Creating a JSON-RPC Service

Using the JayRock library, this is all encapsulated, and we can just go about writing our Service Methods like so:

  1. In Visual Studio, or your favourite editor, start by creating a Generic Handler (.ashx type).
  2. Change the class definition so that it derives from the JsonRpcHandler class instead
  3. Create your Method declarations as per normal, but decorate them with the JsonRpcMethod Attribute.

An example is as follows (this is the base class we have created for authenticating user requests to the Umbraco backend):

using System.Web.Security;
using Jayrock.JsonRpc;
using Jayrock.JsonRpc.Web;
using System;
using System.Web;

namespace refactored.WebServices.json
{
    /// <summary>
    /// Provides a base class for JSON-RPC Services and handles authentication.
    /// </summary>
    public class BaseJSONService : JsonRpcHandler
    {
        protected string AuthenticationToken { get; set; }

        [JsonRpcMethod("AuthenticateMember")]
        public string AuthenticateMember(string username, string password)
        {
          ..
..
} [JsonRpcMethod("AuthenticationExpired")] public bool AuthenticationExpired(string authToken) { return (HttpRuntime.Cache[authToken] == null); }
 protected bool IsUserValid() { return (!string.IsNullOrEmpty(AuthenticationToken) && !AuthenticationExpired(AuthenticationToken)); } protected string GetUserName() { if (IsUserValid()) return HttpRuntime.Cache[AuthenticationToken].ToString(); else return string.Empty; } protected MembershipUser GetMember() { if (IsUserValid()) return Membership.GetUser(GetUserName()); else return null; } private bool ValidateUser(string username, string password) { return Membership.ValidateUser(username, password); } } }



Once you compile and load the dll into Umbraco, along with the associated ashx file, you can test the webservice in your favourite web browser:

JSON-RPC Service Discovery

User Authentication and Web Services

One of the features of the iPhone Application was that it allowed users to provide feedback on the content, as well as create "favourite" lists.  This required that users be able to authenticate with the website in order to make changes to their profile and update the lists on the website.

Because Web Services cannot store the users session, we need to provide some way of authenticating the user each call. In order to do this, an AuthenticateMember(string username, string password) method was created on the Service that returned an authentication token to be used in subsequent calls:

        [JsonRpcMethod("AuthenticateMember")]
        public string AuthenticateMember(string username, string password)
        {
            if (string.IsNullOrEmpty(username) || !ValidateUser(username, password))
                throw new Exception("Your Username or Password is incorrect");

            // Create and store the AuthenticatedToken before returning it
            AuthenticationToken = Guid.NewGuid().ToString();
            HttpRuntime.Cache.Add(
                   AuthenticationToken,
                   username,
                  null,
                  System.Web.Caching.Cache.NoAbsoluteExpiration,
                   TimeSpan.FromMinutes(60),
                  System.Web.Caching.CacheItemPriority.NotRemovable,
                  null);

            return AuthenticationToken;
        }

Then, on subsequent calls, we just pass the authentication token in to be validated against the cached version.

Lets test out the method:

JSON-RPC Service Testing with error

Notice wth this screen shot that the returned result was actually the error - it even includes the Exception details along with the erorr message.

Fixing up the parameters results in the following:

JSON-RPC Service Testing with expected result

Note the returned value - this is the authentication token generated by the server - we use this when calling subsequent methods that require validation.

Well, that's it for now, hopefully you can see from this how we can use JSON-RPC to retrieve information from a website and consume it in other applications, or even using AJAX.  Not only can we retrieve information, we can also create new content and otherwise manipulate information on a website.

Embedded YouTube Videos and iPad Rotation

While working on the iPhone/iPad application for the ChickenChannel website, I needed to have the embedded YouTube video resize dynamically to fit properly and in proportion across the screen of the device.  Moreover, the iPad allows for rotating the view, and we needed the video to resize to accomodate both orientations dynamically as you rotate the device.

The Chicken Channel is written in Umbraco, and the iOS application takes advantage of that by presenting existing pages with a customised template.  So while you might look at a recipe on the website and see the nice embedded Youtube video (hidden behind a banner image that prompts you to click it to start playing the video - this was covered in a post last November here.), the iPhone and iPad views are somewhat different.

Step 1: Make the video automatically fill the width of the screen.

This was quite easy:  all I needed to do was clear the width and height the enclosing div and the object tags and set the width to 100% on the embed tag.  I could have set the height as well, but given that there are 3 possible width with the devices, and I'm "veiling" the page until it's loaded anyway, I didn't see any point.  This displays a rather wide but short video on the iPhone:

<div class="youTubePlayer">
  <object class="youTubePlayer">
    <param name="movie" value="http://www.youtube.com/v/rldN0jSBbZQ?fs=1&rel=0" />
    <param name="allowFullScreen" value="true" />
    <param name="allowscriptaccess" value="always" />
    <embed class="youTubePlayer" src="http://www.youtube.com/v/rldN0jSBbZQ?fs=1&rel=0" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="100%" />
  </object>
</div>



Step 2: Use Javascript to add the aspect ratio back in (set the Height).

Using jQuery this is a really simple excercise:  We simply get the width of the window, and, because all our videos are in 16:9 aspect ratio, we use that to derive the height before applying it to the relevant tags.  (Notice the embed and object tags also have the class="youTubePlayer" attribute above?) - you'll also need to add the jQuery core library to your page...

  <script type="text/javascript">
    $(document).ready(function() {
      var newHeight = $(window).width()*9/16;
      if (newHeight > 500)
          newHeight = 500;
      $('.youTubePlayer').attr("height", newHeight);
    });
  </script>      



Right, after testing a little, we notice that the page renders, then the video gets lengthened to the correct ratio and all is good in the world.  However, when rotating the iPad application, the videos dimensions aren't resized along with the rest of the content.  Actually, the video's width is resized, but the height stays where we left it.

Step 3: Use the Resize event to adjust the aspect ratio on Rotation

The final step to this process was to take advantage of the UIWebView's resize javascript event to perform the resize again:

  <script type="text/javascript">
    $(window).resize(function() {
      var newHeight = $(window).width()*9/16;
      var oldHeight = $('div.youTubePlayer:first').attr("height");
      if (newHeight > 500)
          newHeight = 500;
      if (newHeight = oldHeight)
          return;
      $('.youTubePlayer').attr("height", newHeight);
    });
</script>



After a little more testing (ie, me madly waving the iPad around in the air and doing acrobatic contortions in the process) we have established that the video now resizes gracefully when the device is rotated to Portrait or Landscape mode.  All is better in the world.

Side note: While playing around with this, I had a javascript alert(newHeight); line in the resizing code.  on my iPad with the shiny new iOS 4.3.1 installed just last night, this promptly caused the application to crash.  I've submitted a bug report to Apple, and we'll see how it goes.

As always, comments and suggestions are always welcome.

Enabling Alternate Media with the uTube Umbraco Package

The project I'm currently working on requires embedding youtube videos in the page. For that I've settled on the excellent uTube Umbraco package found over here (or visit the Umbraco package page on our.umbraco.org...).

Out of the box, the uTube package provides some excellent functionality - you can upload videos directly from the Media repository, skinnable player as well as the full youtube player, ready made macros for including content...

However, as the site doesn't yet have videos for all the articles, and there's no guarantee that the articles will do so in the future, I wanted to be able to provide an alternative image instead of the video if it was missing.

In addition, some videos were already uploaded to youtube, and uTube currently doesn't have an import datatype for the Media section, so I wanted to be able to provide another alternative to the user to specify a youtube url as well as be able to choose from the Media items.

So I wrapped up the uTube macro's in my own "wrapper" macro:

 

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet [ <!ENTITY nbsp " "> ]>
<xsl:stylesheet
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:msxml="urn:schemas-microsoft-com:xslt"
  xmlns:umbraco.library="urn:umbraco.library" xmlns:Exslt.ExsltCommon="urn:Exslt.ExsltCommon" xmlns:Exslt.ExsltDatesAndTimes="urn:Exslt.ExsltDatesAndTimes" xmlns:Exslt.ExsltMath="urn:Exslt.ExsltMath" xmlns:Exslt.ExsltRegularExpressions="urn:Exslt.ExsltRegularExpressions" xmlns:Exslt.ExsltStrings="urn:Exslt.ExsltStrings" xmlns:Exslt.ExsltSets="urn:Exslt.ExsltSets" xmlns:CWS.Twitter="urn:CWS.Twitter" xmlns:Locator="urn:Locator" xmlns:tagsLib="urn:tagsLib" xmlns:BlogLibrary="urn:BlogLibrary"
  exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets CWS.Twitter Locator tagsLib BlogLibrary ">


<xsl:output method="xml" omit-xml-declaration="yes"/>

<xsl:param name="currentPage"/>
    <xsl:variable name="mainFeatureId" select="/macro/mainFeatureId"/>
    <xsl:variable name="mainFeature" select="umbraco.library:GetXmlNodeById($mainFeatureId)"/>
<xsl:template match="/">

  <xsl:choose>
    <xsl:when test="$mainFeature/uploadedVideo != ''">
      <xsl:variable name="macro">
        <![CDATA[<?UMBRACO_MACRO macroAlias="uTube.ChromelessPlayer.media" mediaID="]]><xsl:value-of select="$mainFeature/uploadedVideo"/><![CDATA[" uTubeWidth="680" uTubeHeight="433"></?UMBRACO_MACRO>]]>
      </xsl:variable>
      
      <!-- Render the chromeless player macro -->
      <xsl:value-of select="umbraco.library:RenderMacroContent($macro, $currentPage/@id)" disable-output-escaping="yes"/>

    </xsl:when>
    <xsl:otherwise>
      <xsl:choose>
        <xsl:when test="$mainFeature/video != ''">
          <xsl:variable name="macro">
            <![CDATA[<?UMBRACO_MACRO macroAlias="uTube.ChromelessPlayer" uTubeVideo="]]><xsl:value-of select="$mainFeature/video"/><![CDATA[" uTubeWidth="680" uTubeHeight="433"></?UMBRACO_MACRO>]]>
          </xsl:variable>
          
          <!-- Render the chromeless player macro -->
          <xsl:value-of select="umbraco.library:RenderMacroContent($macro, $currentPage/@id)" disable-output-escaping="yes"/>
        
        </xsl:when>
        <xsl:otherwise>
          <xsl:if test="count($mainFeature/bannerMedia) > 0">
            <a href="{umbraco.library:NiceUrl($mainFeature/@id)}" title="{$mainFeature/@nodeName}"><img src="{umbraco.library:GetMedia($mainFeature/bannerMedia, 'false')/umbracoFile}" title="{$mainFeature/@nodeName}" alt="{$mainFeature/@nodeName}" /></a>
          </xsl:if>
        </xsl:otherwise>
      </xsl:choose>

    </xsl:otherwise>
  </xsl:choose>

</xsl:template>

</xsl:stylesheet>

Taking a leaf out of the uTube.ChromelessPlayer.media macro's book (which is in itself a wrapper for the uTube.ChromelessPlayer macro), I'm testing for the existence of the uploadedVideo property, which points at the Media Item containing the uTube uploaded media.  If that doesn't exist, I then check for the video property, which is the alternative uTube Single Video Picker (the user simply pastes in a Youtube url).  If that doesn't exist, we fallback on a placeholder image instead.

End result: Graceful media fallback on the web page.

uTube.ChromelessPlayer.media

Broken Protected Pages in Umbraco 4.5?

Today, while implementing the membership functionality for a new site I'm working feverishly on to get go live next week, I came across this error when I tried to access a Protected page while not logged in:


Object reference not set to an instance of an object.

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.

Source Error:

An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.


Stack Trace:

[NullReferenceException: Object reference not set to an instance of an object.]
   System.Web.Security.Membership.GetCurrentUserName() +68
   System.Web.Security.Membership.GetUser() +17
   umbraco.requestHandler..ctor(XmlDocument umbracoContent, String url) +8037
   umbraco.UmbracoDefault.Page_PreInit(Object sender, EventArgs e) +2534
   System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr fp, Object o, Object t, EventArgs e) +24
   System.Web.Util.CalliEventHandlerDelegateProxy.Callback(Object sender, EventArgs e) +41
   System.EventHandler.Invoke(Object sender, EventArgs e) +0
   System.Web.UI.Page.OnPreInit(EventArgs e) +11042957
   System.Web.UI.Page.PerformPreInit() +41
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +1255

As it turns out, it seems you need to add the runAllManagedModulesForAllRequests="true" attribute to the Modules node in your web.config file:

    <modules runAllManagedModulesForAllRequests="true">

Worked like a charm :)  Thank you Lee from Blogg Fodder UK, you saved me heaps of time with this one :)

Blog Categories in Blog4Umbraco - Take Two

So my first attempt at manipulating Tag Groups by using Events didn't go so well.  Sure, it created the tags with the correct group, but it also left the original tags in the csmTags table, and if you went back and edited the blog entry, the tags would be gone because the Tag Data Type was ignoring anything that didn't belong to the default group.

So. Back to the drawing board.

Edit (10/11/2010): The first package download didn't go so well and failed dismally.  I've uploaded a new version of the package and this time double-tested it, so it should be all good.  Leave a comment if things go awry...

This time I decided to try and tackle the problem at a deeper level - what if I introduced a new Data Type that could intercept the tag group and inject the Blog Category instead?

Package Download: You can download an Umbraco Package to install the Dll that will do this for you from here.  It contains a single Dll that has a couple of Extensions in it aimed at Blog 4 Umbraco, but is not dependent on it being installed.

Enter the Blog Tag Data Type.

I didn't want to have to re-create the entire Tag Data Type, it's Editor Control, or it's Prevalue Editor, so I took advantage of the fact that almost everything can be extended in Umbraco, and just derived a new Data Type class:

 

public class DataType : umbraco.editorControls.tags.DataType
    {
        #region IDataType Members

        private IDataEditor _Editor;

        public override IDataEditor DataEditor
        {
            get
            {
                if (_Editor == null)
                {
                    var preValues = ((umbraco.editorControls.tags.PrevalueEditor)PrevalueEditor).Prevalues;
                    int? nodeId = int.Parse(umbraco.helper.Request("id"));
                    if (nodeId.HasValue)
                    {
                        string category = (string)BlogExtensionsLibrary.GetValueRecursively(nodeId.Value, "category");

                        if (!string.IsNullOrEmpty(category))
                        {
                            if (preValues["group"] == null || (string)preValues["group"] == "default")
                                preValues["group"] = category;
                        }
                    }
                    _Editor = new umbraco.editorControls.tags.DataEditor(Data, preValues);
                }
                return _Editor;
            }
        }

        public override string DataTypeName
        {
            get { return "Blog Tags"; }
        }

        public override Guid Id
        {
            get { return new Guid("BD8B240F-0DE3-47E5-A172-2DE212CC30B6"); }
            // this was the core umbraco tags datatype GUID.
            //get { return new Guid("4023e540-92f5-11dd-ad8b-0800200c9a66"); }
        }

        #endregion
    }

 

Basically, I stripped out all the stuff I didn't want to duplicate from the tags DataType class, and overrode the DataEditor property.  A few interesting things to note:

  • I could have left the Id property alone and not overridden it.  If had done so, my DataType would have taken over the core Tags DataType, and you would basically not have to do anything more.  However, I'm trying to be a good Umbraco Citizen and behave in a right neighbourly fashion.
  • The code has been designed so that if the category attribute doesn't exist in the Document Type or one of it's Ancestors, then it will not try to inject anything into the group, so it would be quite safe to replace the Core Tags Control with the new one.
  • BlogExtensionsLibrary.GetValueRecursively() is described in my previous post attempting to deal with Blog Categories, so not going to go into it here.

Ok, once you have compiled your code and copied the dll into the bin directory of your Umbraco installation, the remaining steps are as follows:

Update the Tags Data Type, or alternatively create a new one.

You will need to go and change the Tags Data Type so that it uses the new Blog Tags control instead of the Core Tags control.  Note that if you decided not to play nice, and let the new code override the Core Tags control by using the old GUID, then essentially you shouldn't need to do anything here!

Blog Tags Data Type

Update the Blog Post Document Type

You would only need to change the Blog Post Document Type if you created a new Data Type instead of modifying the Core Tags Data Type.

Altering Xslt Files for Blog Categories with Tags

This is the third post in a series looking at the issue of having Categories at the Blog level with Blog 4 Umbraco.  It builds upon the post dealing with extending the Blog Tags to take make them specific to a particular Blog by taking advantage of the Tag Group feature.  For background, see the following posts:

Blog 4 Umbraco installs a suite of xslt files that among other things aggregate Tags that the posts are associated with.  However, some of the code looks specifically for the default Tag Group, while other parts of it don't care what group the tags are in.  If you have multiple blogs on a site, this leads to tags being displayed that may actually have nothing to do with the Blog you are looking at.

In this article we're going to attempt to address this issue and clean the xslt files up a little.  Starting with the BlogCategories.xslt file, which by default looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet [ <!ENTITY nbsp " "> ]>
<xsl:stylesheet 
  version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
  xmlns:msxml="urn:schemas-microsoft-com:xslt"
  xmlns:umbraco.library="urn:umbraco.library"
  xmlns:tagsLib="urn:tagsLib"
  exclude-result-prefixes="msxml umbraco.library tagsLib">


<xsl:output method="html" omit-xml-declaration="yes"/>

<xsl:param name="currentPage"/>

<xsl:variable name="blogRoot" select="$currentPage/ancestor-or-self::Blog/@id"/>

<xsl:template match="/">
<ul>
  <li class="cat-item"><a href="{umbraco.library:NiceUrl($blogRoot)}">All</a> <span> (<xsl:value-of select="count($currentPage/ancestor-or-self::Blog//BlogPost)"/>)</span></li>
  <xsl:for-each select="tagsLib:getAllTagsInGroup('default')/tags/tag">
        <li class="cat-link">
            <a href="{umbraco.library:NiceUrl($blogRoot)}?filterby={current()}"><xsl:value-of select="current()"/></a> (<xsl:value-of select="@nodesTagged"/>)
        </li>
  </xsl:for-each>
</ul>

</xsl:template>

</xsl:stylesheet>​


Note the line that retrieves all Tags in the default using tagsLib:getAllTagsInGroup().  This is where the problem lies:  We have a blog that has had a category attribute set to "Events" (see the first article listed above), and we now have tags that are associated with the "Events" group, not the "default" group.

In order to fix this, we need to retrieve the category from the Blog node, and use that to retrieve the correct tags.  However, if the blog hasn't implemented the category attribute, or the attribute is left blank, we need to fall back on the current functionality.  So I've made the following adjustments:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet [ <!ENTITY nbsp " "> ]>
<xsl:stylesheet 
  version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
  xmlns:msxml="urn:schemas-microsoft-com:xslt"
  xmlns:umbraco.library="urn:umbraco.library"
  xmlns:tagsLib="urn:tagsLib"
  exclude-result-prefixes="msxml umbraco.library tagsLib">


<xsl:output method="html" omit-xml-declaration="yes"/>

<xsl:param name="currentPage"/>

<xsl:variable name="blogRoot" select="$currentPage/ancestor-or-self::Blog"/>
<xsl:variable name="blogRootId" select="$blogRoot/@id"/>
<xsl:variable name="blogCategory" select="$blogRoot/category"/>

<xsl:template match="/">
  
  <ul>
  <li class="cat-item"><a href="{umbraco.library:NiceUrl($blogRootId)}">All</a> <span> (<xsl:value-of select="count($currentPage/ancestor-or-self::Blog//BlogPost)"/>)</span></li>
  <xsl:if test="$blogCategory = ''">
    <xsl:call-template name="listCategories">
      <xsl:with-param name="category" select="'default'"/>
    </xsl:call-template>
  </xsl:if>
  <xsl:if test="$blogCategory != ''">
    <xsl:call-template name="listCategories">
      <xsl:with-param name="category" select="$blogCategory"/>
    </xsl:call-template>
</xsl:if>

</ul>

</xsl:template>

<xsl:template name="listCategories">
  <xsl:param name="category"/>
  <xsl:for-each select="tagsLib:getAllTagsInGroup($category)/tags/tag">
      <li class="cat-link">
          <a href="{umbraco.library:NiceUrl($blogRootId)}?filterby={current()}"><xsl:value-of select="current()"/></a> (<xsl:value-of select="@nodesTagged"/>)
      </li>
  </xsl:for-each>
</xsl:template>
</xsl:stylesheet>​



Notice that we've split the actual rendering of the categories list into a separate template called listCategories and have conditionally called it with either the category retrieved from the Blog document or the 'default' tag Group if the category doesn't exist.

Next we tackle the Tag Cloud in much the same way.  The original Xslt source:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet [
    <!ENTITY nbsp " ">
]>
<xsl:stylesheet
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:msxml="urn:schemas-microsoft-com:xslt"
  xmlns:umbraco.library="urn:umbraco.library" xmlns:Exslt.ExsltCommon="urn:Exslt.ExsltCommon" xmlns:Exslt.ExsltDatesAndTimes="urn:Exslt.ExsltDatesAndTimes" xmlns:Exslt.ExsltMath="urn:Exslt.ExsltMath" xmlns:Exslt.ExsltRegularExpressions="urn:Exslt.ExsltRegularExpressions" xmlns:Exslt.ExsltStrings="urn:Exslt.ExsltStrings" xmlns:Exslt.ExsltSets="urn:Exslt.ExsltSets" xmlns:tagsLib="urn:tagsLib" xmlns:BlogLibrary="urn:BlogLibrary"
  exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets tagsLib BlogLibrary ">


    <xsl:output method="xml" omit-xml-declaration="yes"/>

    <xsl:param name="currentPage"/>

    <xsl:template match="/">
        <div class="tagcloud">
            <p>

                <xsl:for-each select="tagsLib:getAllTags()/tags/tag [@nodesTagged > 0]">
                    <xsl:sort select="." order="ascending"/>
                    <a href="{umbraco.library:NiceUrl($currentPage/ancestor-or-self::Blog/@id)}?filterby={.}">
                        <xsl:attribute name="class">
                            <xsl:choose>
                                <xsl:when test="@nodesTagged > 5">
                                    <xsl:value-of select="string('tagweight5')"  />
                                </xsl:when>
                                <xsl:otherwise>
                                    <xsl:value-of select="concat('tagweight',@nodesTagged)"/>
                                </xsl:otherwise>
                            </xsl:choose>
                        </xsl:attribute>
                        <xsl:value-of select="."/>
                    </a>
                    <xsl:text> </xsl:text>
                </xsl:for-each>

            </p>
        </div>

    </xsl:template>

</xsl:stylesheet>​

And after our modifications:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet [
    <!ENTITY nbsp " ">
]>
<xsl:stylesheet
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:msxml="urn:schemas-microsoft-com:xslt"
  xmlns:umbraco.library="urn:umbraco.library" xmlns:Exslt.ExsltCommon="urn:Exslt.ExsltCommon" xmlns:Exslt.ExsltDatesAndTimes="urn:Exslt.ExsltDatesAndTimes" xmlns:Exslt.ExsltMath="urn:Exslt.ExsltMath" xmlns:Exslt.ExsltRegularExpressions="urn:Exslt.ExsltRegularExpressions" xmlns:Exslt.ExsltStrings="urn:Exslt.ExsltStrings" xmlns:Exslt.ExsltSets="urn:Exslt.ExsltSets" xmlns:tagsLib="urn:tagsLib" xmlns:BlogLibrary="urn:BlogLibrary"
  exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets tagsLib BlogLibrary ">


    <xsl:output method="xml" omit-xml-declaration="yes"/>

    <xsl:param name="currentPage"/>

    <xsl:variable name="blogRoot" select="$currentPage/ancestor-or-self::Blog"/>
    <xsl:variable name="blogRootId" select="$blogRoot/@id"/>
    <xsl:variable name="blogCategory" select="$blogRoot/category"/>
    
    <xsl:template match="/">
        <div class="tagcloud">
            <p>
              
            <xsl:if test="$blogCategory = ''">
              <xsl:call-template name="createCloud">
                <xsl:with-param name="category" select="'default'"/>
              </xsl:call-template>
            </xsl:if>
            <xsl:if test="$blogCategory != ''">
              <xsl:call-template name="createCloud">
                <xsl:with-param name="category" select="$blogCategory"/>
              </xsl:call-template>
            </xsl:if>
             
            </p>
        </div>

    </xsl:template>
    
  <xsl:template name="createCloud">
    <xsl:param name="category"/>

    <xsl:for-each select="tagsLib:getAllTagsInGroup($category)/tags/tag [@nodesTagged > 0]">
        <xsl:sort select="." order="ascending"/>
        <a href="{umbraco.library:NiceUrl($blogRootId)}?filterby={.}">
            <xsl:attribute name="class">
                <xsl:choose>
                    <xsl:when test="@nodesTagged > 5">
                        <xsl:value-of select="string('tagweight5')"  />
                    </xsl:when>
                    <xsl:otherwise>
                        <xsl:value-of select="concat('tagweight',@nodesTagged)"/>
                    </xsl:otherwise>
                </xsl:choose>
            </xsl:attribute>
            <xsl:value-of select="."/>
        </a>
        <xsl:text> </xsl:text>
    </xsl:for-each>

  </xsl:template>
</xsl:stylesheet>​​​


And there we have it. Both the Tags list and the Tag Cloud now behave nicely when used within the context of a Blog.