Home ยป BlogEngine.NET

BlogEngine.NET HeatMaps: Part I

1 Mar 2009 6:51 PM 8 Comments Bookmark and Share kick it on DotNetKicks.com

Have you ever thought about what users do on your site?  Beyond the usual statistics, logs, etc, denoting where your users are clicking can be very useful.  A heatmap might just be the thing.  There are many services / products out there but nothing in .NET that I wanted.  What I wanted was an easy, quick and zero-performance-impact solution to implement in my site.  So here we go.

HeatMap

 

Collector

I divided this “idea” into 2 sections.  Today, I’ll discuss the HeatMap Collector as it was the easiest to develop.  Essentially it grabs the mouse coordinates on every mouse click and sends the info to an AJAX-enabled WCF Service.  This service will then save the current date, page and mouse coordinates to an XML file in my App_Data folder.  The name of this xml file is configurable through the web.config as such:

   1: <add key="HeatMap_Settings" value="HeatMap.xml"/>

 

At first I used ASP.NET call-backs but the performance hot was not pretty.  I was looking at 7+ seconds for any mouse click to be sent to the WCF Service.  With a doubt, that wasn’t a viable solution.  In the end I used JQuery + Ajax.  Take a look at the performance, the numbers say it all.

HeatMapPerformance

With that, I present you with this simple user control.

 

HeatMapCollector.ascx
   1: <%@ Control Language="C#" AutoEventWireup="true" CodeFile="HeatMapCollector.ascx.cs" Inherits="HeatMapCollector" %>
   2:  
   3: <script type="text/javascript" language="javascript">
   4:  
   5:  
   6:     ///////////////////////////////////////////////////
   7:     // Internal Variables
   8:     ///////////////////////////////////////////////////
   9:     var IE = document.all ? true : false
  10:     if (!IE) document.captureEvents(Event.MOUSEUP)
  11:  
  12:     document.onmouseup = GetMouseXY;
  13:  
  14:     var sTime = Date();
  15:     
  16:     ///////////////////////////////////////////////////
  17:     // Capture MouseUP Events
  18:     ///////////////////////////////////////////////////
  19:     function GetMouseXY(e) 
  20:     {
  21:         var posx = 0;
  22:         var posy = 0;
  23:         
  24:         if (!e) var e = window.event;
  25:         if (e.pageX || e.pageY)     
  26:         {
  27:             posx = e.pageX;
  28:             posy = e.pageY;
  29:         }
  30:         else if (e.clientX || e.clientY)     
  31:         {
  32:             posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
  33:             posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
  34:         }
  35:      
  36:      
  37:         // Get the current page name, or user supplied page name
  38:         var sPage;
  39:         sPage = HMC_DefaultPageName;
  40:         
  41:         if(HMC_PageName == "")  // No user supplied pagename, attempt to parse page name from URL
  42:         {
  43:             var sPath = window.location.pathname;
  44:             sPage = sPath.substring(sPath.lastIndexOf('/') + 1);
  45:  
  46:             // There's a possibility that sPage will be NULL due to the default page of a site or virtual site
  47:             // If that's the case, alert user
  48:             // i.e http:///www.somewhere.com/
  49:             if ((sPage == '') || (sPage == null))
  50:             {
  51:                 if ((HMC_DefaultPageName == '') || (HMC_DefaultPageName == null))
  52:                 {
  53:                     alert("No Default PageName parameter or URL 'page name' available to save mouse click.");
  54:                 }
  55:                 else
  56:                     sPage = HMC_DefaultPageName;
  57:             }
  58:             else if (sPage.toLowerCase() == HMC_DefaultPageName.toLowerCase())
  59:             {
  60:                 sPage = HMC_DefaultPageName;
  61:             }
  62:         }
  63:         else
  64:             sPage = HMC_PageName;   // Use user supplied name
  65:  
  66:  
  67:         $j.ajax({
  68:             type: "POST",
  69:             url: getRootURL() + "/themes/13sides/HeatMapService.svc/SaveClick",
  70:             data: '{ "coordX":"' + posx + '", "coordY":"' + posy + '", "page":"' + sPage + '" }',
  71:             contentType: "application/json; charset=utf-8",
  72:             dataType: "json",
  73:             success: function(msg) { },
  74:             error: function(xhr, msg, e) { }
  75:         });
  76:  
  77:  
  78:         // Let the mouse event continue normally
  79:         return true;
  80:     }
  81:     function getRootURL()
  82:     {
  83:         var baseURL = location.href;
  84:         var rootURL = baseURL.substring(0, baseURL.indexOf('/', 14));
  85:  
  86:         // if the root url is localhost, don't add the directory as cassani doesn't use it
  87:         if (baseURL.indexOf('localhost') != -1)
  88:         {
  89:             return rootURL + "/BlogEngine.Web/";
  90:         } 
  91:         else
  92:         {
  93:             return rootURL + "/";
  94:         }
  95:     }
  96:     ///////////////////////////////////////////////////
  97: </script>

 

HeatMapCollector.ascx.cs

 

   1: using System;
   2: using System.ComponentModel;
   3: using System.Web.UI;
   4:  
   5: [ToolboxData("<{0}:HeatMapCollector runat=server></{0}:HeatMapCollector>")]
   6: public partial class HeatMapCollector : System.Web.UI.UserControl
   7: {
   8:     #region Properties
   9:     [Category("HeatMap Optional")]
  10:     [Description("String - Page name to save mouse coords against.  If blank, page name will be parsed from URL.")]
  11:     public string PageName { get; set; }
  12:     [Category("HeatMap Optional")]
  13:     [Description("String - Page name to save mouse coords against if PageName property is not set and page name can not be parsed from URL.")]
  14:     public string DefaultPageName { get; set; }
  15:     #endregion
  16:  
  17:     protected void Page_Load(object sender, EventArgs e)
  18:     {
  19:         // HeatMap Collector Config Options Script
  20:         string ss = @"var HMC_PageName = '" + PageName + "';" + System.Environment.NewLine;
  21:         ss += @"var HMC_DefaultPageName = '" + DefaultPageName + "';" + System.Environment.NewLine;
  22:         Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "HeatMap_Collector_Config", ss, true);
  23:     }
  24: }

 

A couple of explanations for the above code would probably help now.  I exposed a couple of properties in the code to get around issues with MasterPages.  There is probably a much more elegant way to do this but… anyway, its done.  To integrate this control to my theme, I dropped it on my MasterPage as such:

   1: <%@ Register src="~/User controls/ThirteenSides/HeatMapCollector.ascx" TagName="HeatMapCollector" TagPrefix="ucHMC" %>
   2:  
   3: <body>
   4:     <form id="Form1" runat="Server" class="body">
   5:  
   6:     <asp:ScriptManager ID="ScriptManager1" runat="server" />
   7:     
   8:     <script src="/js.axd?path=jquery-1.3.2.min.js" type="text/javascript"></script>
   9:     <script type="text/javascript">
  10:         $j = jQuery.noConflict();
  11:     </script>              
  12:  
  13:     <ucHMC:HeatMapCollector ID="ucHeatMapCollector" runat="server" 
  14:             PageName="" 
  15:             DefaultPageName="default.aspx" />
  16: ...
  17: ...
  18: ...

 

BlogEngine.NET already defines the $ for its own use so unless we use the useful jquery.noConflict() method, I wouldn’t be able to use JQuery anywhere in my code.

You can see the call to my WCF service via this JQuery Ajax call:

 

$j.ajax({
    type: "POST",
    url: getRootURL() + "/themes/13sides/HeatMapService.svc/SaveClick",
    data: '{ "coordX":"' + posx + '", "coordY":"' + posy + '", "page":"' + sPage + '" }',
    contentType: "application/json; charset=utf-8",
    dataType: "json",
    success: function(msg) { },
    error: function(xhr, msg, e) { }
});

 

I created the Ajax-enabled WCF Service, named HeatMapService in the root of my theme.  If you’ve read any of my previous posts you will see how I’ve done this (notably, the Factory parameter).

   1: <%@ ServiceHost Language="C#" Debug="true" 
   2:                 Service="HeatMapService" 
   3:                 CodeBehind="~/App_Code/HeatMapService.cs" 
   4:                 Factory="CustomServiceFactory" %>

 

HeatMapService.cs

   1: using System;
   2: using System.Configuration;
   3: using System.ServiceModel;
   4: using System.ServiceModel.Activation;
   5: using System.Web;
   6: using System.Xml.Linq;
   7:  
   8: [ServiceContract(Namespace = "")]
   9: [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
  10: public class HeatMapService
  11: {
  12:     /// <summary>
  13:     /// Saves the Mouse Coordinate and Page Name to the XML data source
  14:     /// Will create the XML file, if it's the first time
  15:     /// </summary>
  16:     /// <param name="coordX"></param>
  17:     /// <param name="coordY"></param>
  18:     /// <param name="page"></param>
  19:     [OperationContract]
  20:     public void SaveClick(int coordX, int coordY, string page)
  21:     {
  22:         string settings = ConfigurationManager.AppSettings["HeatMap_Settings"];
  23:         string path = string.Concat(HttpContext.Current.Request.PhysicalApplicationPath,
  24:                                     "App_Data/" + settings);
  25:  
  26:         XElement element = new XElement("HeatMapClick",
  27:                                         new XElement("Date", DateTime.Now.ToString()),
  28:                                         new XElement("Page", page),
  29:                                         new XElement("CoordX", coordX),
  30:                                         new XElement("CoordY", coordY));
  31:  
  32:         XElement heatmapClicks = null;
  33:         try
  34:         {
  35:             heatmapClicks = XElement.Load(path);
  36:         }
  37:         catch { }
  38:  
  39:         if (heatmapClicks != null)
  40:         {
  41:             heatmapClicks.Add(element);
  42:             heatmapClicks.Save(path);
  43:         }
  44:         else
  45:         {
  46:             XElement root = new XElement("HeatMapClicks");
  47:             root.Add(element);
  48:             root.Save(path);
  49:         }
  50:     }
  51: }

 

Using Linq-to-Xml, its a piece of cake to create and write to Xml files.

That’s it to the HeatMapCollector control.  A few items to note on this control:

  1. I left JQuery on the root of my site.
  2. The folder App_Data should (and probably already is) write-enabled, to save the HeatMap.xml file.
  3. This xml file will continue to grow – beware.  I’ve not added any logic to create new files every day,  etc.

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Comments

Comments are closed