Simple Tag Cloud Implementation for ASP.NET MVC

 

I read this post by Mikesdotnetting a few days ago when I was trying to implement a Tag Cloud for our open source forum project SubForum. Although it was a good post and got me started on how to do it, I found it a bit complex for what I needed. One of the complications that I saw was obviously the Entity Framework and the data layer that Mike talked about to build the Tag Cloud. The data I needed was quite simple and didn’t need to be that complex.

I decided to implement a simple ASP.NET MVC Tag Cloud using plain SQL and a simple table structure. Also, I did not want categories. I just wanted posts and post tags, so that my Tag Cloud could more accurately represent the Tags in the posts.

RenderAction Approach

I decided to use the RenderAction approach introduced in the MVC Futures library. This helped me strongly type the Tag Cloud partial view to a Model and a specific action in the Controller that populates the model. This is helpful because the Model for the main page is different from the Model for the TagCloud and I wanted to avoid adding the Tag Cloud model to the ViewData collection as a dictionary value. You can read Jimmy Bogard’s post for more about the benefits of RenderAction.

The Steps

Back to the Tag Cloud. Let me start with the Database model:

image

 

The Post and Tag object are used to represent a Post and a List of Tags:

 image 


The way it works is that every time a post is added, the comma separated tags associated with it are taken and inserted as individual tags in the PostTags table.



We need to pull all the information from the database to perform our Tag Cloud calculations. We need the number of times each tag occurs in the system, the Tag itself and the total number of posts in the system. We can pull all this information in one simple query:




Select Count(1) As Count, PostTags.Tag As Tag, (Select Count(*)
From Posts) As TotalPosts
From PostTags
Group By PostTags.Tag
Order By 'Count' Desc





The result of executing this query against our test database returns:



image



As you can see, this shows the tag, the number of times it occurs in the system and the total number of posts in the system. We can then return this data through the TagCount object (which has just three properties: Count, Tag and TotalPosts) from our Data Layer function GetTagCloud:










public IList<SubForum.DataAccess.Objects.TagCount> GetTagCloud()

        {

            System.Data.IDbCommand command = null;

            IDataReader dataReader = null;

            List<SubForum.DataAccess.Objects.TagCount> returnList = new List<SubForum.DataAccess.Objects.TagCount>();

            IDbConnection connection = new System.Data.SqlClient.SqlConnection(this.ConnectionString);

            try

            {

                connection.Open();

                command = connection.CreateCommand();

                command.CommandText = "Select Count(1) As Count, PostTags.Tag As Tag, (select count(*) from Posts) as To" +

                    "talPosts\nFrom PostTags\nGroup By PostTags.Tag\nOrder By \'Count\' Desc";

                System.Console.WriteLine("Executing Query: {0}", command.CommandText);

                dataReader = command.ExecuteReader();

                for (

                ; dataReader.Read();

                )

                {



                    SubForum.DataAccess.Objects.TagCount modelObj = new SubForum.DataAccess.Objects.TagCount();

                    modelObj.Tag = ((String)(dataReader["Tag"]));

                    if ((dataReader["Count"].Equals(DBNull.Value) == false))

                    {

                        modelObj.Count = ((int)(dataReader["Count"]));

                    }

                    else

                    {

                        modelObj.Count = null;

                    }

                    if ((dataReader["TotalPosts"].Equals(DBNull.Value) == false))

                    {

                        modelObj.TotalPosts = ((int)(dataReader["TotalPosts"]));

                    }

                    else

                    {

                        modelObj.TotalPosts = null;

                    }

                    returnList.Add(modelObj);

                }

            }

            finally

            {

                if ((dataReader != null))

                {

                    ((System.IDisposable)(dataReader)).Dispose();

                }

                if ((command != null))

                {

                    ((System.IDisposable)(command)).Dispose();

                }

                if ((connection != null))

                {

                    connection.Close();

                    ((System.IDisposable)(connection)).Dispose();

                }

            }

            return returnList;

        \






  The ForumController that handles all the Forum MVC requests has a GetTagCloud() function that returns the model for the View:








public ActionResult TagCloud()

        {



            IList<TagCount> tagCountList = DataAccessService.GetTagCloud();



            return View(tagCountList);

        \






Then comes the user control or the MVC Partial View that displays the tags as links with the right Tag class to use. TagCloud.ascx:








<%@ Control Language="C#"  Inherits="System.Web.Mvc.ViewUserControl<IList<SubForum.DataAccess.Objects.TagCount>>" %>

<h5>Tags</h5>

<
ul>

<%

int totalNumberOfTags = base.Model.Count;



foreach (SubForum.DataAccess.Objects.TagCount tagCount in base.Model)

{

    if (!string.IsNullOrEmpty(tagCount.Tag))

    {

        string tagClass = SubForum.Web.Controllers.ForumController.GetTagClass(tagCount.Count.Value, tagCount.TotalPosts.Value);

%>  <li><%=Html.RouteLink(

                tagCount.Tag,

                "Tag",

                new

                {

                    controller = "Forum",

                    action = "Tag",

                    id = tagCount.Tag

                },

                      new { id = tagClass }

          )%></li>

  <%}

}
%></ul>







The UI calls the GetTagClass() function to determine which TagClass from the CSS to use:








  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 "tag5";

            return result <= 50 ? "tag6" : "";

        }







These Tag classes match our CSS that has the following definition:










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







Finally, in our Master page we will call the controller to Render the Partial View TagCloud.ascx and shows us all the tags in the system appropriately weighted:








<div id="tags">

    <% Html.RenderAction<SubForum.Web.Controllers.ForumController>(c => c.TagCloud());%>

</div>






This yields a plain and simple TagCloud looking as such, with the most popular showing first :



image



Extension



The Tag Cloud can be extended by limiting the Tags to a predefined set of tags instead of making it free for all. That can simply be done by adding a Tags table which holds all the system’s predefined Tags and only allowing users to pick from those tags. The overall working of this code and the SQL would not be affected at all.



You can also return the top 10 or however many tags that you want to be displayed instead of displaying all the tags by simply adding a Top clause to your query:



Select Top 10 Count(1) As Count, PostTags.Tag As Tag, (Select Count(*)
From Posts) As TotalPosts
From PostTags
Group By PostTags.Tag
Order By 'Count' Desc


Conclusion



With this approach all the code from the UI layer including the CSS down to the Data Layer is shown. The approach is universal to any database engine or provider you may want to use and there is nothing hidden except the workings of the MVC Framework. You can see a live demo (if it is up :) ) at www.PortaltoolBox.net or checkout the actual source on www.subforum.net.