📜 ⬆️ ⬇️

Cloud tags on ASP.Net with caching.

Odin gloomy Sunday morning I had nothing to do and I decided to try to write my own version of the bike - a tag cloud on ASP.Net. The result was quite interesting, so I decided to issue it as an article and put it on Habré.
At once I will make a reservation - this is the result of only one and a half hour coding, accordingly, the request not to perceive it as fully ready control, but only as a concept that can still be developed and developed.


So. What do we know about the tag cloud? A tag cloud is a set of objects, which are described by a pair of values ​​<Number of Entries, Tag>. In turn, the tag is a pair of <NameTag, Link>. So let's create a class that will encapsulate the tag information in it.
public class Tag
{
public string Text { get ; set ; }
public string Href { get ; set ; }
}

* This source code was highlighted with Source Code Highlighter .

The number of entries is rather an external entity in relation to the tag, so we will separate the flies from the patties and will operate on the Pair <int, Tag> object, which is completely similar to the standard KeyValuePair <int, Tag>, except that the Key and Value properties are not marked as readonly. It will be useful to us in the future. Accordingly, in the Key property we will have the number of occurrences of the tag by which we will rank them, and in the Value the actual tag itself.
Okay, let's imagine a few hundred people visit the site every minute. Obviously, it is necessary to make it so that the generation of the tag cloud does not take much time. Tags are a pretty static thing, it hardly makes sense to generate a cloud more than once every N minutes, because they shouldn't change dramatically. So you need to organize caching. Given these considerations, we write our control.
[ToolboxData( "<{0}:TagCloudControl runat=server></{0}:TagCloudControl>" )]
public class TagCloudControl : WebControl
{
public const int MULTIPLIER = 2;
public const int MIN_SIZE = 5;

public event TagListDelegate TagsCollected;

protected override void Render(HtmlTextWriter writer)
{
writer.AddAttribute(HtmlTextWriterAttribute.Align, "Center" );
writer.AddAttribute(HtmlTextWriterAttribute.Width, Width.ToString());
writer.AddAttribute(HtmlTextWriterAttribute.Height, Height.ToString());
writer.AddAttribute(HtmlTextWriterAttribute.Class, CssClass);
writer.RenderBeginTag(HtmlTextWriterTag.Div);

if (TagsCollected != null )
{
foreach ( var tag in TagCloudCache.GetTags(TagsCollected))
{
writer.WriteEncodedText( " " );
writer.AddStyleAttribute(HtmlTextWriterStyle.FontSize, string .Format( "{0}px" , (tag.Key+MIN_SIZE)*MULTIPLIER));
writer.RenderBeginTag(HtmlTextWriterTag.Span);
writer.AddAttribute(HtmlTextWriterAttribute.Href, tag.Value.Href);
writer.RenderBeginTag(HtmlTextWriterTag.A);
writer.WriteEncodedText(tag.Value.Text);
writer.RenderEndTag();
}
}

writer.RenderEndTag();
}
}


* This source code was highlighted with Source Code Highlighter .

Everything is very simple - it is inherited from the WebControl abstract class and overloads the Render method. The control is passed to the delegate parameter.
public delegate IEnumerable <Pair< int , Tag>> TagListDelegate();

* This source code was highlighted with Source Code Highlighter .

which should return a list of pairs with information about tags and their entries. The cache should be able to look at the delegate and determine if tags need to be regenerated. So, let's write a class that will encapsulate information about the generated tag cloud in itself:
public class TagCalculationInfo
{
public TimeSpan TimeOut { get ; set ; }
public DateTime LastFiring { get ; set ; }
public IEnumerable <Pair< int , Tag>> CalculatedTags { get ; set ; }

public bool IsExpired
{
get
{
return DateTime .Now - LastFiring > TimeOut;
}
}
}

* This source code was highlighted with Source Code Highlighter .

The class contains a timeout - the lifetime of the generated tag cloud during which it is considered relevant and does not need to be regenerated. The LastFiring property is the time of the last tag regeneration. CalculatedTags - tags proper. The IsExpired property returns true when the necessary time has passed since the last regeneration of the trades. Now we have all the bricks from which we can build a cache:
public static class TagCloudCache
{
private const int TAG_GROUPS = 10;
private static readonly TimeSpan DEFAULT_TIMEOUT = new TimeSpan (0, 1, 0);
private static readonly Dictionary< string , TagCalculationInfo> m_Cache = new Dictionary< string , TagCalculationInfo>();

public static IEnumerable <Pair< int , Tag>> GetTags(TagListDelegate target)
{
lock (m_Cache)
{
string key = target.GetKey();
if (!m_Cache.ContainsKey(key))
{
m_Cache.Add(key, new TagCalculationInfo { TimeOut = DEFAULT_TIMEOUT });
}
var tagInfo = m_Cache[key];
if (tagInfo.IsExpired)
{
tagInfo.CalculatedTags = RecalculateTags(target);
tagInfo.LastFiring = DateTime .Now;
}
return tagInfo.CalculatedTags;
}
}
}

* This source code was highlighted with Source Code Highlighter .

So, the cache is not complicated at all - in the m_Cache dictionary there is information about tag clouds already generated. When the control calls the GetTags method, we check if the necessary cloud is in the cache - if not, add empty information. Further we look, whether the cloud has become outdated. If it is outdated, update it and set the last regeneration time to the current one. Here it must be remembered that you cannot use a Dictionary <TagListDelegate, TagCalculationInfo> because, unfortunately, the delegates' Equals method always returns false. I decided to use as a key the string that is formed by the extension method:
public static string GetKey( this TagListDelegate del)
{
return string .Format( "{0}{1}{2}" , ((MulticastDelegate)del.Target).Method.DeclaringType, ((MulticastDelegate)del.Target).Method.Name, ((MulticastDelegate)del.Target).Method.MethodHandle.Value);
}

* This source code was highlighted with Source Code Highlighter .

During testing, the line turned out like this: "TagCloud.TagCloud.TagCloudCacheGetTestTags84221360". Unfortunately, I did not find information on the Internet on how to correctly cache the results of the methods in .Net, so if anyone wants to use this method in a real project, you need to think carefully and google how to do it.
Moving on. It remains only to figure out how the tag cloud will be calculated from the input data. Two more methods:
private static IEnumerable <Pair< int , Tag>> RecalculateTags(TagListDelegate target)
{
var tags = new List <Pair< int , Tag>>(target());
var max = tags.Max().Key;
var min = tags.Min().Key;
var clusters = new int [TAG_GROUPS];
var step = (max - min)/(TAG_GROUPS - 1);
for ( int i = 0; i < TAG_GROUPS; i++)
{
clusters[i] = min + i*step;
}
foreach ( var tag in tags)
{
tag.Key = FindClosestPosition(clusters, tag.Key);
}
tags.Sort();
for ( int i = 0; i < tags.Count; i += 2)
{
yield return tags[i];
}
for ( int i = tags.Count % 2 == 0 ? tags.Count - 1 : tags.Count - 2; i >= 0; i -= 2)
{
yield return tags[i];
}
}

* This source code was highlighted with Source Code Highlighter .

The RecalculateTags method splits tags into groups - there will be 10. We find the minimum and maximum number of entries for the tag. We fill the array of clusters with values ​​from minimum to maximum evenly. Now for each tag we find the group to which it is closest - the array of clusters is sorted, so you can apply a binary search:
public static int FindClosestPosition( int [] arr, int key)
{
int h = arr.Length - 1, l = 0;
while (h - l > 1)
{
int m = (h + l)/2;
if (arr[m] > key)
{
h = m;
}
else
{
l = m;
}
}
if ( Math .Abs(arr[h] - key) < Math .Abs(arr[l] - key))
{
return h;
}
return l;
}


* This source code was highlighted with Source Code Highlighter .

Now you can implement a feature - I want the tag size to grow from the edges to the middle of the cloud. Sort tags by groups. We first return all tags that are on pair positions from the smallest to the largest, then - those that stand on unpaired in the reverse order.
It remains a bit - to test what happened. Add a property to the file with the page code that will be returned by the delegate with test tags:
public TagListDelegate TestTags
{
get
{
return TagCloudCache.GetTestTags;
}
}

* This source code was highlighted with Source Code Highlighter .

Register the prefix for tags:
<%@ Register Assembly="TagCloud" Namespace="TagCloud.TagCloud" TagPrefix="cc" %>

* This source code was highlighted with Source Code Highlighter .

And add our cloud to the page:
< cc:TagCloudControl runat ="server" Name ="TagCloudControl" OnTagsCollected ="TestTags" />

* This source code was highlighted with Source Code Highlighter .

Run and get the result:

Of course, the control still needs careful refinement by the file, but the basic principles are unlikely to change.
Apologizing in advance for possible mistakes - Russian is not native. Well, in general, the first article on Habré :)

UPD: Control generates the following HTML code:
< span style ="font-size:10px;" >< a href ="#" > PHP </ a > < span style ="font-size:10px;" >< a href ="#" > Delphi </ a > < span style ="font-size:10px;" >< a href ="#" > Internet </ a > < span style ="font-size:10px;" >< a href ="#" > Nemerle </ a > < span style ="font-size:10px;" >< a href ="#" > Outsourcing </ a > < span style ="font-size:10px;" >< a href ="#" > VB.Net </ a > < span style ="font-size:10px;" >< a href ="#" > JavaScript </ a > < span style ="font-size:12px;" >< a href ="#" > C++ </ a > < span style ="font-size:12px;" >< a href ="#" > Apple </ a > < span style ="font-size:12px;" >< a href ="#" > Intel </ a > < span style ="font-size:14px;" >< a href ="#" > CLR </ a > < span style ="font-size:14px;" >< a href ="#" > Java </ a > < span style ="font-size:16px;" >< a href ="#" > WinForms </ a > < span style ="font-size:16px;" >< a href ="#" > Web </ a > < span style ="font-size:18px;" >< a href ="#" > WPF </ a > < span style ="font-size:22px;" >< a href ="#" > AJAX </ a > < span style ="font-size:28px;" >< a href ="#" > .Net </ a > < span style ="font-size:24px;" >< a href ="#" > ASP.Net </ a > < span style ="font-size:20px;" >< a href ="#" > C# </ a > < span style ="font-size:18px;" >< a href ="#" > Google </ a > < span style ="font-size:16px;" >< a href ="#" > MVC </ a > < span style ="font-size:14px;" >< a href ="#" > Microsoft </ a > < span style ="font-size:14px;" >< a href ="#" > SQL </ a > < span style ="font-size:12px;" >< a href ="#" > SEO </ a > < span style ="font-size:12px;" >< a href ="#" > jQuery </ a > < span style ="font-size:12px;" >< a href ="#" > Habrahabr </ a > < span style ="font-size:12px;" >< a href ="#" > Flash </ a > < span style ="font-size:10px;" >< a href ="#" > Sun </ a > < span style ="font-size:10px;" >< a href ="#" > LISP </ a > < span style ="font-size:10px;" >< a href ="#" > Facebook </ a > < span style ="font-size:10px;" >< a href ="#" > Perl </ a > < span style ="font-size:10px;" >< a href ="#" > RSDN </ a > < span style ="font-size:10px;" >< a href ="#" > Yandex </ a ></ span >


* This source code was highlighted with Source Code Highlighter .

')

Source: https://habr.com/ru/post/47448/


All Articles