Creating a Tag Cloud using ASP.NET MVC and the Entity Framework

A Tag Cloud is a visual depiction of Tags or categories on a web site. CSS is used to set the variable font size of each Tag to illustrate its relative frequency or popularity within the site. As part of the migration of my site to ASP.NET MVC, I am introducing one to replace the Categories navigation that appears on the left hand side of each page, which contains a count of items within each category.

One of the reasons for the change is that I feel I have been mis-using "Categories". Generally, an item should really only be classified within one category, whereas applying more that one Tag to an item is de rigeur within Web 2.0 social networking and bookmarking sites. I've been "filing" articles under more than one category since I started the site, so it's about time I sorted that out.

If you haven't already seen the object model I am working to behind this site (which was featured in a previous article) here's a refresher:

I am using the ADO.NET Entity Framework as my Object Relation Mapping (ORM) tool. From the screenshot of the edmx file above, you should be able to infer that there is a one-to-many relationship between Articles and ArticleTypes (Article, Snippet, Cheat Sheet etc), but a many-to-many relationship between Articles and Categories. This means that each Article can be of one ArticleType, but it can belong to many Categories.

SIDE NOTE:

The actual database schema contains an Articles table, a Categories table and a bridging table to take care of the many-to-many relationship (ArticleCategories). If I was using LINQ to SQL instead of the Entity Framework, the bridging table would be exposed in the diagram, but EF works with a much higher level object model than that. As far as EF is concerned, each Article has a collection of Categories, and conversely, each Category has a collection of related Articles. The logic behind that approach, once you discover it, is undeniable. The real difference here between the designer diagrams are that LINQ to SQL is more representative of the underlying database schema, whereas EF is modelling the entities.

Back to the task at hand - a Tag Cloud, as mentioned earlier - is a visual depiction of the relative frequency that each Tag is used. Accordingly, some calulations are required to establish how many articles are tagged with a specific Category relative to the total of articles. This is done simply by working out the percentage of Articles within each Tag. Clearly, I don't want to have to manually recalculate as articles are added or deleted from the site, so the calculation has to be done dynamically. For that to work, I need the total number of articles, and the number of articles by tag. So I created a new class called MenuCategory, which extends the Category class already generated by EF, and has the following properties:


namespace MikesDotnetting.Models
{
  public class MenuCategory : Category
  {
    public int CountOfCategory { get; set; }
    public int TotalArticles { get; set; }
  }
}

What's needed now is a method that will create the collection of MenuCategory objects that will form the Tag Cloud:


public IEnumerable<MenuCategory> GetMenuCategories()
{
  var totalArticles = de.ArticleSet.Count();
                             
  return (from c in de.CategorySet.Include("Articles")
          orderby c.CategoryName
          select new MenuCategory
          {
            CategoryID = c.CategoryID,
            CategoryName = c.CategoryName,
            CountOfCategory = c.Articles.Count(),
            TotalArticles = totalArticles
          });
}

This appears in ArticleRepository.cs, which is my main data access point for the application. You will notice one thing - the ObjectQuery<T>.Include() method. This forces related entities to be returned as part of the result from the database (Eager Loading). It's a different approach to the one used by LINQ To SQL, where you need to set the options for eager loading at the DataContext level, through the DataLoadOptions. With the Entity Framework, eager loading is defined at the query level.

So, I have my TagCloud object which will contain a collection of MenuCategory items. I have a method in the Repository that will create the collection. Now all I need is a way to get this collection to the MasterPage. Since the Tag Cloud will appear in the Master Page of the site, the data will need to be pulled every time there is a page request. So which controller should be used? The answer is one which all other controllers will inherit from. This means that whenever a Controller that inherits from BaseController is invoked, it will cause the parent Controller to instantiate, along with any properties and methods it contains. In my case, I have unimaginatively called this BaseController:


using System.Web.Mvc;
using MikesDotnetting.Models;

namespace MikesDotnetting.Controllers
{
  public abstract class BaseController : Controller
  {    
    private readonly IArticleRepository repository;

    protected BaseController(): this(new ArticleRepository())
    {
      
    }

    private BaseController(IArticleRepository rep)
    {
      repository = rep;
    }
  }
}

To keep things relatively simple during thie exercise, I will be using untyped ViewData to pass the Tag Cloud data to the MasterPage. So, within the BaseController constructor, I add the following:


ViewData["TagCloud"] = repository.GetCategoriesForTagCloud();

And that's all it takes to add the data to the ViewDataDictionery. All I need now is to vary the font size of each Category within the MasterPage. For that, I need a little helper class and some CSS. the helper class takes two ints representing the number of articles in a category, and the total number of articles. It returns a string based on what the first number represents as a percentage of the second:


public static string GetTagClass(int category, int articles)
{
  var result = (category * 100) / articles;
  if (result <= 1)
    return "tag1";
  if (result <= 4)
    return "tag2";
  if (result <= 8)
    return "tag3";
  if (result <= 12)
    return "tag4";
  if (result <= 18)
    return "tag5";
  if (result <= 30)
    return "tag6";
  return result <= 50 ? "tag7" : "";
}

The strings refer to classes within the CSS file, where the size of the font progressively increases:


/*
------------------------------
  Tag Cloud
------------------------------
*/

.tag1{font-size: 0.8 em}
.tag2{font-size: 0.9em}
.tag3{font-size: 1em}
.tag4{font-size: 1.2em}
.tag5{font-size: 1.4em}
.tag6{font-size: 1.7em}
.tag7{font-size: 2.0em}

Within the MasterPage, each category is assessed using the GetTagClass method, and the css style applied to it dynamically:

    

<h2>Tags</h2>
<div id="tags">
  <%
    foreach (var t in (IEnumerable<MenuCategory>) ViewData["TagCloud"])
    {%>
      <%=Html.RouteLink(
                t.CategoryName,
                "Category",
                new
                {
                  controller = "Category",
                  action = "Listing",
                  id = t.CategoryID
                 },
                 new { @class = Utils.GetTagClass(t.CountOfCategory,t.TotalArticles)}
          )%>
    &nbsp;
    <% }%>
</div>