One thing I liked most in former Joomla times was the possibilty to create global tokens that could be used anywhere in a Post / Article. Think of an "add to cart" token that you can place in a blog post that expands to a button that adds a predefined product to your cart!
What's the goal ?
What I actually wanted to do was integrating the download counts of my open source projects on my website http://www.bitboxx.net. And it should be possible in every HTML-Module or blog or anywhere else.
<ul>
<li>BBNews:<strong> \[ bitboxx:downloads|http://bbnews.codeplex.com/stats?ddrange=-1,https://api.github.com/repos/weggetor/BBNews/releases] </strong>downloads</li>
<li>BBImageHandler: <strong>\[ bitboxx:downloads|http://bbimagehandler.codeplex.com/stats?ddrange=-1,https://api.github.com/repos/weggetor/BBImagehandler/releases,https://api.github.com/repos/weggetor/BBImagehandler-8/releases] </strong>downloads</li>
<li>BBQuery: <strong>\[ bitboxx:downloads|http://bbquery.codeplex.com/stats?ddrange=-1,https://api.github.com/repos/weggetor/BBQuery/releases]</strong> downloads</li>
<li>BBContact: <strong>\[ bitboxx:downloads|http://bbcontact.codeplex.com/stats?ddrange=-1,https://api.github.com/repos/weggetor/BBContact/releases]</strong> downloads</li>
<li>BBBooking: <strong>\[ bitboxx:downloads|http://bbbooking.codeplex.com/stats?ddrange=-1,https://api.github.com/repos/weggetor/BBBooking/releases]</strong> downloads</li>
<li>BBImageStory:<strong> \[ bitboxx:downloads|https://api.github.com/repos/weggetor/BBImageStory/releases] </strong>downloads</li>
</ul>
and that's how should look like in non-edit-mode:
- BBNews: 3066 downloads
- BBImageHandler: 1764 downloads
- BBQuery: 138 downloads
- BBContact: 259 downloads
- BBBooking: 108 downloads
- BBImageStory:67 downloads
HttpHandler vs HttpModule
Looking at the mechanics how to achieve this goal I found out that there are two different approaches that ASP.NET delivers:
HttpHandler - This one is called by a specific url and delivers anything that I create in the code. Could be an image, a javascript or even html as result. The dnnImagehandler works this way - calling http://www.domain.tld/dnnImagehandler.ashx and providing in the url parameters which image we want to generate and how this image should be altered (resize, crop etc.). Not working here.
HttpModule - Integrates in the calling chain of every hit to the website and has a lot of hooks to change some of the input (Request) or output (Response). This should be the way to go! I could hook in when the whole page is generated and ready to be delivered to the browser. The only thing that is to be done then is looking at the generated html and replace my tokens with whatever I want to include in the page.
Creating the project
In my case I wanted to create something that is independent to any module, so I created a new project. But if you are a module developer and you want to deliver tokens for your specific module (like the "Add to cart"-Button in a store module), its easy to add a class to your module and code the token replace and you are done !
The first thing to do is adding a class to the project that inherits from IHttpModule
like this:
public class BBTokenReplaceHttpModule : IHttpModule
To fulfill the interface definition, you have to implement two methods:
public void Dispose()
{
//clean-up code here.
}
public void Init(HttpApplication context)
{
context.PostRequestHandlerExecute += new EventHandler(OnPostRequestHandlerExecute);
}
The PostRequestHandlerExecute hook method is the one we need to accomplish our task. It chimes in right before the content is send back to the browser. In this method we do the following:
public void OnPostRequestHandlerExecute(Object source, EventArgs e)
{
HttpApplication app = (HttpApplication) source;
HttpResponse response = app.Response;
if (response.ContentType == "text/html" && !Globals.IsEditMode())
{
ResponseFilterStream filter = new ResponseFilterStream(response.Filter);
filter.TransformStream += filter_TransformStream;
response.Filter = filter;
}
}
First we check if the Response is of mimetype "text/html" because we do not want to replace our tokens in images or something else. And by adding a reference to the dotnetnuke.dll we are able to check if DNN is in editmode or not. This is very important because in editmode we want to see the token code and not the output of the token - otherwise it would be very complicated to edit the token later!
Cahnging the output in an HttpModule should be done by adding a filter to the resonse object. Rick Strahl from Westwind WebConnections wrote a blog post about this topic and provides a class that we can use to get help with this. See Capturing and Transforming ASPNET Output with ResponseFilter.
As a result I have to add this class to the project:
ResponseFilterStream.cs
This class has an TransformStream event where we could add our token replace code:
MemoryStream filter_TransformStream(MemoryStream ms)
{
Encoding encoding = HttpContext.Current.Response.ContentEncoding;
string output = encoding.GetString(ms.ToArray());
while (output.IndexOf(""));
string prop, format;
if (token.IndexOf('|') > -1)
{
prop = token.Split('|')[0];
format = token.Split('|')[1];
output = output.Replace("", ReplaceToken(prop, format));
}
else
{
prop = token;
format = "";
output = output.Replace("", ReplaceToken(prop, format));
}
}
ms = new MemoryStream(output.Length);
byte[] buffer = encoding.GetBytes(output);
ms.Write(buffer, 0, buffer.Length);
return ms;
}
This is simply converting the stream to a string, replacing the tokens that start with [ bitboxx:..]. I'm using the standard DNN token notation that uses [object:Property|format]. I tried to use the token replace engine of DNN but this results in destroying a lot of JSON code in the page and so I decided to look specifically for the "[ bitboxx:..]" thing and replace only these tokens by doing it the non regex way (I booked a course for regex on Udemy but I think I'll never get the head around this - maybe you do better ^^).
Implementing the replacement code
Now we have all architectural things in place an we can begin to implement the specific replacement code:
public string ReplaceToken(string strPropertyName, string strFormat)
{
string cacheKey = "bbtoken_" + strPropertyName + "_" + strFormat;
string result = (string) DataCache.GetCache(cacheKey);
if (result != null)
return result;
result = "";
switch (strPropertyName.ToLower())
{
case "downloads":
int cnt = 0;
foreach (string url in strFormat.Split(','))
{
cnt += Download.DownloadCount(url);
}
result = cnt.ToString();
break;
}
DataCache.SetCache(cacheKey,result);
return result;
}
Here we get our property name and the format string into our method. First thing to do is looking at the cache if our token is inside. If yes we return this so we do not need to generate it with every call which could result in slowing your website down!
In my specific case, the format parameter contains a comma seperated list of urls which point to the different sites with my open source project releases. I'm using a list because some projects are on codeplex AND github, and so I had to sum the counts from the different pages.
To retrieve the counts from codeplex I make use of a nuget library that provides the ability to quest html-pages with Linq (see: HtmlAgilityPack). On Github this task is easier because the have an API for this that responses with JSON.
public static class Download
{
public static int DownloadCount(string url)
{
int downloads = 0;
if (url.Contains("codeplex"))
{
var webGet = new HtmlWeb();
var document = webGet.Load(url);
string dl = "-1";
var nodes = document.DocumentNode.SelectNodes("//table[@id='DownloadsSummary']");
var cell = (from tag in nodes.Descendants()
where tag.Name == "strong" && tag.InnerText.IndexOf("downloads") > -1
select tag).FirstOrDefault();
dl = cell.InnerText.Replace("downloads", "").Replace("\r", "").Replace("\n", "").Trim();
downloads = Convert.ToInt32(dl);
}
else
{
using (WebClient wc = new WebClient())
{
wc.Headers.Add("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)");
var jsstring = wc.DownloadString(url);
JArray releases = (JArray)JsonConvert.DeserializeObject(jsstring);
foreach (JObject release in releases)
{
downloads += Convert.ToInt32(release["assets"][0]["download_count"].ToString());
}
}
}
return downloads;
}
}
Adding handler to the DNN installation
In order to make our TokenReplaceHttpmodule run in DNN , we have to do two more things:
- copy the compiled DLL to the bin folder of your DNN installation
- add the following line to web.config in the system.Webserver/modules path:
<add name="BBTokenReplace" type="Bitboxx.HttpModules.BBTokenReplaceHttpModule" preCondition="managedHandler"/>
Thanks for listening ! Comments welcome!