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:
Last Updated:
Posted by:
Total Views to date: 42992

16 Comments

- Kelvin

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

- 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!

- 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.

- Adam

Got it - thanks!

- 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.

- Mike

@Tim

Oops. Sorry ;o)

- jeeva

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

- Mike

@Jeeva

Use a search engine. Or buy a book.

- 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?

- 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...)

- Jayaram Krshnaswamy

Excellent and well written.

- 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)?

- johnny

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

- Ravinder

hi
This Article make my job vary easy.

thanks Warm Regards

- outletGucci

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

- 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.

Recent Comments

Joseph Casey 5/27/2015 3:17 PM
In response to ASP.NET MVC 5 with EF 6 - Working With Files
Probably should have read the title. Couldn't use it with Visual Studios 2013. =[...

jean 5/27/2015 8:41 AM
In response to iTextSharp - Drawing shapes and Graphics
hey Mike, I read your arcticle about drawing on PDF with ITextSharp, it was really interesting !...

Alexandros L 5/26/2015 10:50 PM
In response to Entity Framework 6 Recipe - Hierarchical Data Management
You are a live saver.. thank you so much......

saurabh rao 5/26/2015 1:03 PM
In response to ASP.NET MVC 5 with EF 6 - Working With Files
Hey Mike ! Great article ...the code worked flawlessly for me. Didn't have any issues executing it ....

Vijay 5/25/2015 2:12 PM
In response to How To Send Email In ASP.NET MVC
Thanks...

RR 5/25/2015 8:58 AM
In response to Web Pages - Efficient Paging Without The WebGrid
@MIke.. is it possible to combine pagination and filter (change category ) in the script? Thanks...

Irfan Khan 5/25/2015 6:19 AM
In response to Highlighting Keywords Found In Search Results
This is truely helpful. Thank you for the brilliant tip....

Manoj Kulkarni 5/25/2015 4:46 AM
In response to Custom TagHelpers in ASP.NET MVC 6
Thank you for nice article. Really helpful....

Sean 5/21/2015 4:20 AM
In response to ASP.NET MVC 5 with EF 6 - Working With Files
Thank you for the tutorial. Saving to the database works, but the save FilePath is not working. It...

KrASh 5/19/2015 11:10 PM
In response to Adding Search
Hello Mike, I have a question about the second search. In one line it has a where with the title and...