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>

Date Posted: Monday, June 1, 2009 7:26 AM
Last Updated: Friday, October 10, 2014 9:09 PM
Posted by: Mikesdotnetting
Total Views to date: 40849

16 Comments

Tuesday, June 9, 2009 11:42 AM - Kelvin

Your articles are always clearly written and easy to follow. Keep up the good work!

Tuesday, June 9, 2009 9:09 PM - Adam

Great article!

"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:"

Could you provide a link to this article?

Also, I would like to follow your articles - is there an RSS feed available?

Thanks!

Tuesday, June 9, 2009 10:38 PM - Mike

@Adam,

I've linked to the previous article now. If you check the third paragraph, you should find it. Alternatively, click the ASP.NET MVC link in the Categories menu and you will find an article on PartialViews. But the image is the same one used in that article.

There's also an RSS feed link (the orange square) in the left hand column under the MVP logo.

Tuesday, June 9, 2009 11:27 PM - Adam

Got it - thanks!

Thursday, June 11, 2009 7:49 PM - Tim

Exelent article and I'm glad to see it using EF it's something I've been wanting to start using.

But the down side is that I've been working on the exact same idea for a submission to the ASP.NET Daily Articles. Guess I better come up with another idea.

Thursday, June 11, 2009 8:06 PM - Mike

@Tim

Oops. Sorry ;o)

Friday, June 12, 2009 7:07 AM - jeeva

What is MVC? How to use MVC.Please explain clearly thank ypu.

Friday, June 12, 2009 7:54 AM - Mike

@Jeeva

Use a search engine. Or buy a book.

Friday, June 12, 2009 1:23 PM - Bob

I have been enjoying reading these articles. Just wondering about caching the results? With this being your base controller each time the site page is refreshed it will hit the database for the article counts? The counts would only change as articles are submitted or removed. Would this be a candidate for caching the result somewhere? Is this something you plan to cover in a later article?

Friday, June 12, 2009 8:24 PM - Mike

@Bob,

Yes, it would be a candidate for caching by all that's right. The data changes just 3 or 4 times a month maximum. I haven't bothered implementing caching for 2 reasons - first I don't want to confuse the main thrust of the article with additional concepts outside of the main ones to get the tag cloud to work, and secondly, I haven't implemented caching at all in my site. No need - it's one of only 5 sites living on a massive Quad Core box :o) The odd visit that this site gets is just about all that SQL Server gets to remind itself that it exists....

But I may well look at caching in another article (once I've eventually found the time to finish the MVC migration...)

Friday, July 10, 2009 4:17 PM - Jayaram Krshnaswamy

Excellent and well written.

Friday, July 17, 2009 3:28 PM - totalNewbie

Thank you very much for a clear article.

your navigational property should be called ArticleType(S) missing the S...

there is an error in numbering (6..7) in GetTagClass, and I would use switch instead of ifs...

question, isn't there an easier way to count how many times particular category name is present in the Category table? (without the need for the eager load another table)

storing values somewhere (collumn)?

Monday, July 27, 2009 9:42 PM - johnny

Very nice article. I am wondering is there any link for the sample code?

Wednesday, August 26, 2009 12:39 PM - Ravinder

hi
This Article make my job vary easy.

thanks Warm Regards

Saturday, June 11, 2011 9:32 AM - outletGucci

I lilke your article,it is very clear for me and help me solve many problems,thank you very much.

Thursday, February 21, 2013 11:31 AM - Atul Sharma

Hi, Thanks for giving understanding on tag cloud, can you also please help in sharing link of downloadable example sample application?

Thanks in Advance.
Add your comment

If you have any comments to make about this article, please use this form to do so. Make sure that your comment relates specifically to the article above. More general comments can be posted through the form on the Contact page.

Please note, all comments are moderated, and some may not be published. The kind of things that will ensure your comment is deleted without ever seeing the light of day are as follows:

  • Not relevant to the article
  • Gratuitous links to your own site or product
  • Anything abusive or libellous
  • Spam
  • Anything in a language I don't understand including gibberish.

I do not pass email addresses on to spammers, so a valid one will assist me in responding to you personally if required.

Recent Comments

Allen Michaels 12/17/2014 4:37 PM
In response to Cascading DropDownLists with jQuery and ASP.NET
Fantastic thank you so much!...

Emily 12/17/2014 12:36 PM
In response to Parameterized IN clauses with ADO.NET and LINQ
Thanks, very helpful!!!! Can i use this for multiple in's ????? IN (.....) and IN(...) and IN...

sss 12/16/2014 3:06 PM
In response to Solving the Operation Must Use An Updateable Query error
good...

Gjuro 12/15/2014 10:30 PM
In response to Examining the Edit Methods and Edit View
You have one fromr (and it should be from, I suppose). :-)...

Gjuro 12/15/2014 10:27 PM
In response to Adding Search
Hi, thnx for all this C#->VB translations. Yet, the following code block is (slightly) in error it a...

Scot 12/14/2014 1:39 PM
In response to Entity Framework 6 Recipe - Alphabetical Paging In ASP.NET MVC
Thanks,Mike I found solution....

Gjuro 12/13/2014 10:52 PM
In response to Accessing Your Model's Data from a Controller
The article mentions "Creating an Entity Framework Data Model for an ASP.NET MVC Application" (at is...

Samuel 12/13/2014 8:40 AM
In response to Displaying The First n Characters Of Text
I have failed to use the extension because it throws an error that it doesn't recognise the chop be...

Ignas 12/12/2014 5:11 PM
In response to Cleaner Conditional HTML Attributes In Razor Web Pages
Any suggestions for Html Helper elements with HtmlAttributes, when you need to conditionally set it...

Gautam 12/11/2014 8:50 PM
In response to Validation In Razor Web Pages 2
Hi Mike Is this required for V3, non html helper input...