Code Voyeur
RSS
Languages MVC ORM About Roadmap Contact Site Map RSS Sample Code Presentations Snippets dll Hell .net

A Tag Cloud ViewComponent for MonoRail

Tags are a useful way to organize content on a website. Tag clouds are essentially weighted lists of tags, with each word being rendered as a link. The link's font size increases or decreases based on the weight (or popularity) of the tag. While it is likely that most tags and weighting would come from a count query, the tag cloud component requires only a generic Dictionary. Each key in the dictionary is a tag string, with a integer weight.
public class TagCloudComponent : ViewComponent
{
    private const string TAG_HTML_TEMPLATE = "<span class=\"{0}\"><a href='{1}{2}/{3}'>{2}</a></span> ";
    private const string TAG_CLASSNAME_TEMPLATE = "tag-{0}";

    [ViewComponentParam(Required=true)]
    public Dictionary<string, int> TagsAndCounts { get; set; }
    ...
}
Three additional properties are required. The NumberOfStyleVariations is the count of unique font-size styles with which to render links in the tag cloud. TagLinkBaseUrl and TagLinkAction allow views to customize tag link actions.
[ViewComponentParam(Required=true)]
public int NumberOfStyleVariations { get; set; }

[ViewComponentParam(Required=true)]
public string TagLinkBaseUrl { get; set; }

[ViewComponentParam(Required=true)]
public string TagLinkAction { get; set; }
The Render method begins with three LINQ operations to obtain a sorted set of tags (by tag name) and to find the min and max tag weights. The distribution variable is used to determine the boundaries between tag link font sizes.
public override void Render()
{
    var sorted = TagsAndCounts.OrderBy(x => x.Key);
    double min = sorted.Min(x => x.Value);
    double max = sorted.Max(x => x.Value);
    double distribution = (double)((max - min) / NumberOfStyleVariations);    
    ...        
}
The remaining code in Render will loop over each of the sorted key/value pairs. For each key, the tag's weight will be compared to each of the possible lower and upper bounds. If the tag's weight falls in between a given range, the class assigned to the span surrounding the tag link is named with the value of the second counter, j. The loop will execute at most NumberOfStyleVariations times, meaning j will be a value from 1 to NumberOfStyleVariations (tag-1, tag-2, tag-3, etc.). Doubles are used instead of integers for min, max and distribution to ensure accuracy of the loop. Lost precision will cause the loop to continue at least once beyond max.
foreach(var x in sorted)
{
    for (double i = min, j = 1; i < max; i += distribution, j++)
    {
        if (x.Value >= i && x.Value <= i+distribution)
        {                        
            RenderText(
                string.Format(
                    TAG_HTML_TEMPLATE, 
                    string.Format(TAG_CLASSNAME_TEMPLATE, j), 
                    TagLinkBaseUrl, 
                    x.Key, 
                    TagLinkAction));                        
            break;
        }                    
    }                
}
Tag links are constructed using the TagLinkBaseUrl and TagLinkAction properties. The tag name is added to the URL. The sample project assumes routing would be used to pass the tag name to the requested action.
<routing>
  <rule>
    <pattern>^(/home/)(\w+)/(artist.rails)(.)*$</pattern>
    <replace><![CDATA[ /home/artist.rails?a=$2 ]]></replace>
  </rule>
</routing>
The number-based class name convention requires styles be defined somewhere with the appropriate class names. The styles below demonstrate the required style definitions for a NumberOfStyleVariations set to 6.
.tag-cloud SPAN { margin:.5em;display:inline;}
.tag-cloud .tag-1 { font-size: 1em; }
.tag-cloud .tag-2 { font-size: 1.5em; }
.tag-cloud .tag-3 { font-size: 1.7em; }
.tag-cloud .tag-4 { font-size: 1.9em; }
.tag-cloud .tag-5 { font-size: 2.2em; }
.tag-cloud .tag-6 { font-size: 2.5em; }
The view includes the component and sets the appropriate property values.
<div class="tag-cloud">
    #component(TagCloudComponent with "TagsAndCounts=$TagsAndCounts" "NumberOfStyleVariations=6" "TagLinkBaseUrl=$SiteRoot/Home/" "TagLinkAction=Artist.rails")
</div>
The component requires that a Dictionary<string, int> is assigned to the TagsAndCounts property. The sample project simply creates a dictionary with a hard-coded set of tags and weights. The dictionary is then placed in the PropertyBag.
public void Default()
{
    Dictionary<string, int> tagsAndCounts = new Dictionary<string, int>()
    {
        {"thekillers", 130},
        {"theshins", 175},
        {"radiohead", 220},
        {"chopin", 150},
        {"beethoven", 68},
        {"countingcrows", 90},
        {"ledzeppelin", 130},
        {"elviscostello", 160},
        {"nineinchnails", 103},
        {"sarabarellis", 40},
        {"aliceinchains", 94},
        {"fuddle", 134},
        {"feist", 20},
        {"reginaspektor", 55},
        {"mozart", 35},
        {"queensryche", 65},
        {"nirvana", 23},
        {"jackjohnson", 46},
        {"thebeatles", 99},
        {"rageagainstthemachine", 84},
        {"benfoldsfive", 125},
        {"weirdalyankovic", 50},
        {"weezer", 95},
        {"thirdeyeblind", 43},
        {"queen", 102},
        {"bobdylan", 44},
        {"davematthewsband", 39},
        {"davidgray", 24}
    };

    PropertyBag["TagsAndCounts"] = tagsAndCounts;
}

Download Sample Project

References

Source of the markup for the tag cloud in the sample project
Source code for sample project
Article Posted: Tuesday, June 10, 2008

Leave a Comment


Contact Code Voyeur about this article.