In our previous RavenDB and SignalR articles, we built a small blogging application in ASP.NET MVC. While building those Apps, I realized a ‘Tag Cloud’ is a nice feature that we could add.
So to expand on that idea, in this article
- We will build a Tag cloud functionality using HTML5 Canvas. Though HTML5 or Canvas is not a pre-requisite to build a Tag Cloud, we will take it as a use case to learn
How to Draw text on canvas
Measure height of text on canvas
Measure width of text on canvas
Set Font family and size of text canvas
Make canvas items clickable. By default, canvas doesn’t provide any way of tracking click in individual items on it. A canvas is what it’s name implies - a blank slate. It will raise a click event if you click on it, but to detect what you clicked on, you need to wire up some code.
- The Server side code will have examples of
Let’s get started. You can get the RavenDB code from here. The final code base is available over here
Step 1: Build and Run the code.
Step 2: Since we start off with an empty database, it will be good to have some test data for us. You can use the attached script (in the final code base) to generate some test blogs.
Build and run the Application once to create the Database (if it doesn’t exist already). Start SQL (Express) Management Studio and execute the GenerateBlogData.sql to generate some test data.
You could create some blogs manually too. (Don’t forget to mark the newly created blogs as Published else you will have to log in to view them).
Step 3: The Tag cloud should be present in the blog’s Index page. So let’s update the Index.cshtml. We add an external <div> to encapsulate the entire blog list and Canvas element, and set its width to 900px.
There are two inner <div> elements, one containing the blog items and the other containing the canvas that will have the tag cloud.
Step 4: To get data for the tags, we will make an asynchronous call using jQuery to an action method in our Blog controller. Drop in the following script to wire up the controller
This above code calls the Tags action method on the Blog controller. It will return a string representing a JSON array.
Step 5: The populateTagCloud parameter is a method delegate that will be called once the Action method returns. The implementation is as follows
a. Get the canvas object (tagCloudCanvas)
b. Retrieve the 2D context
c. Convert the string returned from the controller into a JSON object
d. lastXPos and lastYPos handle manage the top left (x, y) co-ordinates as we loop through each tag returned
e. TagSize is a weighted value calculated in the service layer that tells us how big, font of each tag should be. We can use logic like number of hits, most recently accessed or simply the number of blog items with the particular tag to determine the size. The frontend is not concerned with the logic but simply the value of TagSize. As we see below, TagSize is multiplied by ten (10) to get to the final font size.
f. Font size and font family is then set to canvasContext.font property. You can change this value before every attempt to draw text
g. Once the font size and font is set, we retrieve the width of the tag’s text using the canvas context’s measureText API.
h. The ‘if’ condition checks if the amount of space remaining after the last write is enough to fit in the new tag. If it is, the new tag is written to the right of the old tag.
i. Else it’s written below the previous tag. As you can see positioning is absolute with respect to canvas position.
j. The call to filltext method actually paints the text.
k. tagClickHandler is the method delegate that will actually be called when user clicks on a the tag text in the canvas.
Step 6: The createClickMap function
a. The createClickMap function is passed the following parameters
The canvas object on which the operations are happening
Top left (x, y) coordinates of the area containing the tag text
Width (w) and Height (h) of the text
Function delegate that should be called if conditions are fulfilled.
And the Json object containing the Tag Name, Id and other details
b. The first line attaches a click event handler.
c. The inline function generates the relative positions mX and mY for the click location.
d. Checks if the function m returns true by passing it the text position and the click position. By an interesting use of inline function, we have avoided explicit declaration of a map to store the text’s location. Instead we let the JS interpreter manage the function tables, where each function will have it’s own set of values for x, y, w, h, mx and my.
e. Reference: David Pirek’s blog here.
f. The tagClickHandler method generates the Url to the Index page with the tag name as parameter and redirects the page to this Url. This covers all the UI changes we had to do. At this point, if you are a non-ASP.NET developer, you would have realized that if you pass a JSON stream to the Tag cloud, the cloud will render itself and make itself clickable.
Step 7: Retrieving unique Tags for the tag cloud
a. If you remember, we did not have a ‘Tag’ domain object because at the domain level all we needed earlier was a string representing all the tags. But now we need a separate Tag object so that we can query it, filter it and get unique values out of it.
b. We add a new Tag class to the Domain model. The code looks as follows:
c. As we can see it’s not a POCO rather a class that implements IEquatable<Tag> interface. As we will see shortly we need only unique Tag names and to control whether an object is should be treated as Equal or not in LINQ we need to implement this interface.
d. In the above code, we implement Equal functionality to return true if the Id and the Name of the tag are the same. As we have learnt, since the wee days of dot net, if you have a custom Equals method you should override GetHashCode and make sure you provide unique hash codes yourself. Hence we have overridden the GetHashCode method as well.
Step 8: Updating the Repository implementations
a. With the tag cloud in place, we will update the repository to return us a list of tags. We will also add an overloaded GetPosts method that will accept isAuthenticated and tagName as parameters and return blogs with the given tagName only.
b. Add the following methods in the abstract class BlogPostRepository
c. Update the RavenBlogPostRepository.cs with empty methods
d. Update the SqlBlogPostRepository.cs with the following GetTags method implementation
Let’s look at the LINQ query inside the if(userAuthenticated) in a little bit of details. The inList var stores a query to get all the Tags in the system. Now we need the count of how many times a tag has appeared on the blog. This will be a factor that we use to indicate the font size of the tag. The postTags query that selects all the BlogPosts and then flattens them out using the SelectMany to give us a list of the Blog-to-Post relationships.
Next we do a join with the PostTags to determine the tags in use.
The select new statement actually creates a projection of the return values. In this case, we are able to project it into a Domain Tag object. So we end up populating outList with required Domain Tag objects.
The LINQ query in the else statement does an additional join to filter out Tags associated with blogs that are not in Published status. Essentially, we don’t consider Tags that are associated with blogs in Draft status.
e. Similar to the GetTags method we implement the GetPosts method as follows
This first query returns a list of domain BlogPost objects filtered by the tagName. The second query filters it for blogs in published status only.
Note: Since we are calling post.ToDomainBlogPost() in the query, we need to load the BlogPosts eagerly (no deferred execution). So in this case, inList actually has the data. If we don’t do that, LINQ will defer execution to the point of posts.ToList<Domain.Model.BlogPost>(). Now when it executes the query, it tries to project the list into Domain.Model.BlogPost instead of Data.Model.BlogPost. This results in a method not found exception.
Step 9: Updating the ProductService
Now that the Repository is setup to return Tags and Posts filtered by tags, let’s update our service layer. One thing we want to do is ‘normalize’ the PostCount value so that if one tag has 1000 posts associated whereas another has only 1 post associated, we don’t end up with Tags that are too big or too small to see. To do that we use the following algorithm:
a. We decide we will show say 10 different sizes of fonts (MAX_FONT_SIZES_COUNT).
b. This count does not co-relate to number of distinct tags or number of blogs each tag is associated with. It is simply the number of different font sizes we will use to represent the tags
c. Determining density: It is the factor that tells how many different tags we will have to lump together in each font size. In our case we are using how many distinct buckets of post tags we have divided by the maximumAllowedCount, which is either MAX_FONT_SIZES_COUNT or the actual number of count.
d. We choose number of posts a tag is associated with as the factor determining the size of the tag font. So we get a list of these numbers. (it is stored in distinctCounts).
e. Then we loop through them and based on the density assign the count a font size. This mapping helps keep the calculate n+m where n = distinct post counts and m = number of tags.
f. Finally we loop through the tags and based on the PostCount assign the font size.
The final code looks as follows:
Step 10: Updating the Controller.
Getting the Tags
a. The tag cloud calls an Action method called Tags that returns a JSON object. So we will update our BlogController with the following method.
b. This method instantiates the BlogPostService and calls the GetTags method. It also passed the User.Identity object so that we pull up articles based on whether the current user is logged in or not.
c. The service method returns an IEnumerable<Tag> that is converted using the Json helper method.
d. Note the JsonRequestBehavior.AllowGet parameter. Without this method ASP.MVC will block Json requests and you will get null values instead of the Json stream at the front-end.
Filtering Index based on tag name.
a. We update the existing Index method to take a string parameter that is the tag name selected.
b. If the name is null or empty we call the old GetPosts method. Else we call the new GetPosts method and pass it the tag name.
c. There is one slight problem here, the Routing engine by default assumes the parameters being passed in an action method map to an Id. It’s easy to pass an Id from the UI but the Url would like like
http://myblog.com/Blog/Index/1 where 1 stands for the tag’s Id. Instead we want it to say http://myblog.com/Blog/MVC to indicate the current page is showing MVC tags only. To do this, we need to update the Routing table. The Routing table is configured in the Global.asax. The changes are highlighted below. The named parameter name corresponds to the input parameter name in the Action method.
a. There are some more cosmetic CSS change for the bread-crumb, the header and footer for each blog post.
b. All the while I was testing without RavenDB server running so I kept getting exceptions. Put in a little exception handling for that.
We started with the simple aim to create a tag cloud and in process, learn a bit of HTML5 Canvas. However on the way we also looked at LINQ and ASP.NET MVC routing.
The next step would be to add hover support on the canvas so that when you hover on the Tags, they change cursor and color to look more realistic.
The entire source code of this article can be downloaded over here
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!