Add Simple Tracking To dasBlog Entries

I was looking for a built in method to display the number of times a particular blog entry had been viewed in dasBlog. After spending some time looking at the API on-line and combing through the source code I did not find one. This article will help you build your own. The following image shows how the information will be presented:

image image To add this information to each entry, you update the item template for your dasBlog theme to render the new data. The dasBlog engine utilizes macros that are defined by ‘<% %>’ brackets. The template processor then replaces the macros with dynamically rendered html. The following is my complete item template:

<div class="post">
    <h1 class="title"><%itemTitle%></h1>
    <div class="postWidgets">
      <%editButton%>
    </div>
    <p class="byline"><small>Posted in <%categoryLinks%> - Viewed <%NumViewsControl()|cravens%> times</small></p>
    <div class="entry">
        <%itemBody%>
    </div>
    <div class="meta">
        <div class="info">
            <%when%> by <%authorName%>
        </div>
        <div>
            <span class="views">Views [<%NumViewsControl()|cravens%>]</span> |
            <span class="comment"><%commentLink%></span> |
            <span class="permalink"><%permalink%></span> |
            <span class="saveAndShare">
                <a class="a2a_dd" href="http://www.addtoany.com/share_save?linkname=pageName.html&linkurl=http%3A%2F%2Fblog.bobcravens.com">
                    Save and Share
                </a>
                <script type="text/javascript">
                    a2a_linkurl="<%permalinkUrl%>";
                </script>
                <script type="text/javascript" src="https://static.addtoany.com/menu/page.js">
                </script>
            </span>
        </div>
    </div>
</div>

With in this bit of code you will find a macro called <%NumViewsControl()|cravens%>. Before using the macro, you must register it. You do this by inserting the following lines into the web.config:

<newtelligence.DasBlog.Macros>
    <add macro="cravens" type="CravensPlugin.CravensMacros, CravensPlugin"/>
</newtelligence.DasBlog.Macros>

This registers a macro named ‘cravens’ with the dasBlog engine. The definition of the macro is set by the type parameter. In this case, the macro is implemented by the class ‘CravensMacros’ in the namespace ‘CravensPlugin’. This name is then used on the right-side of the pipe (|) when calling the macro. The left side of the pipe is the method name in the method in the class. The following is the “cravens” macro implementation:

namespace CravensPlugin
{
    public class CravensMacros
    {
        protected SharedBasePage sharedBasePage;
        protected Entry currentEntry;

        public CravensMacros(SharedBasePage page, Entry entry)
        {
            sharedBasePage = page;
            currentEntry = entry;
        }

        public virtual Control NumViewsControl()
        {
            long numViews = CravensPlugin.Utils.NumViews(currentEntry.EntryId);
            Label lbl = new Label();
            lbl.Text = numViews.ToString();
            return lbl;
        }
    }
}

Notice the constructor accepts an instance of the base page (dasBlog implements a number of useful / common features in the page base class) and an instance of an Entry. The NumViewsControl method simply returns a label control that has its text property set to the number of views for the current entry. The CravensPlugin.Utils class is encapsulating all the logic to track and persist the data. The following class diagrams show these details:

image

The data is serialized out to an XML file. Instances of the class EntryStats contain the number of views for each entry. The EntryId is a unique identifier. The EntryStatsCollection is named type that contains the collection.

The Utils class is a static class. The LoadEntryStats and SaveStats methods serialize the data collection to disk. The NumViews method returns the number of views (or zero) for a given entry ID. The UpdateEntryStats method increments the number of views for the entry. The following is the code for these classes:

[Serializable]
public class EntryStats
{
    public string EntryId { get; set; }
    public long NumViews { get; set; }
}

[Serializable]
public class EntryStatsCollection : List<EntryStats>
{

}

public static class Utils
{
    private static Dictionary<string, int> _statsLut = new Dictionary<string,int>();
    private static EntryStatsCollection _stats = new EntryStatsCollection();

    static Utils()
    {
        // Load the entry stats
        //
        LoadEntryStats();
    }

    public static void UpdateEntryStats(string entryId)
    {
        if(_statsLut.ContainsKey(entryId))
        {
            int index = _statsLut[entryId];
            _stats[index].NumViews += 1;
        }
        else
        {
            EntryStats es = new EntryStats();
            es.EntryId = entryId;
            es.NumViews = 1;
            _stats.Add(es);
            _statsLut[entryId] = _stats.Count - 1;
        }

        SaveStats();
    }

    public static long NumViews(string entryId)
    {
        if (_statsLut.ContainsKey(entryId))
        {
            int index = _statsLut[entryId];
            return _stats[index].NumViews;
        }
        else
        {
            return 0;
        }
    }

    private static string AppData
    {
        get
        {
            return HttpContext.Current.Server.MapPath("content");
        }
    }

    private static void LoadEntryStats()
    {
        // Create the root folder if necessary and get the path to the
        //    xml file that holds the book info.
        //
        string rootFolder = Path.Combine(AppData, "EntryStats");
        if (!Directory.Exists(rootFolder))
        {
            Directory.CreateDirectory(rootFolder);
        }
        string statsXmlFile = Path.Combine(rootFolder, "stats.xml");

        // Validate the existence of the data file.
        //
        if (!File.Exists(statsXmlFile))
        {
            _stats = new EntryStatsCollection();
        }
        else
        {
            // Open a stream for reading.
            //
            Stream fileStream = new FileStream(statsXmlFile, FileMode.Open, FileAccess.Read);

            // Deserialize the data.
            //
            try
            {
                XmlSerializer xmlSerializer = new XmlSerializer(typeof(EntryStatsCollection));
                _stats = (EntryStatsCollection)xmlSerializer.Deserialize(fileStream);
            }
            catch
            {
                _stats = new EntryStatsCollection();
            }
            finally
            {
                // Close the stream.
                //
                fileStream.Close();
            }
        }

        // Sync the dictionary for fast lookups
        //
        _statsLut = new Dictionary<string, int>();
        for (int i = 0; i < _stats.Count; i++)
        {
            EntryStats es = _stats[i];
            _statsLut[es.EntryId] = i;
        }
    }

    private static void SaveStats()
    {
        // Create the root folder if necessary and get the path to the
        //    xml file that holds the book info.
        //
        string rootFolder = Path.Combine(AppData, "EntryStats");
        if (!Directory.Exists(rootFolder))
        {
            Directory.CreateDirectory(rootFolder);
        }
        string statsXmlFile = Path.Combine(rootFolder, "stats.xml");

        // Open up a file stream for saving.
        //
        Stream fileStream = new FileStream(statsXmlFile, FileMode.Create, FileAccess.Write);

        // Serialize out the data.
        //
        XmlSerializer xmlSerializer = new XmlSerializer(typeof(EntryStatsCollection));
        try
        {
            xmlSerializer.Serialize(fileStream, _stats);
        }
        catch (Exception ex)
        {
            Rlc.Utilities.Logging.Logger.LogCritical("SaveStats", ex);
        }
        finally
        {
            fileStream.Close();
        }
    }

}

Notice the EntryStats class are marked with the serializable attribute. The load and save methods use the XmlSerializer class to handle serialization and de-serialization of the data collection. In addition to the list of entry stats, the class also has an instance of a Dictionary. This Dictionary instance is used as a fast look-up-table to get the index into the collection given an entry ID.

So where do you hook up the call to UpdateEntryStats? This will require modifying the dasBlog engine source code. I decided to put this call in the macro <%bodytext%>. This macro gets called to generate the list of entries shown on the page. Here is where I chose to place the call:

public virtual Control Bodytext
{
    get
    {
        IPageFormatInfo formatInfo = requestPage as IPageFormatInfo;
        if (formatInfo != null)
        {
            return formatInfo.Bodytext;
        }
        else
        {
            PlaceHolder bodyPlaceHolder = new PlaceHolder();

            EntryCollection entries = requestPage.WeblogEntries;
            if (entries != null)
            {
                ArrayList al = new ArrayList();
                foreach(Entry entry in entries)
                {
                    // RLC 5/11/2009
                    // Maintain statistics for this entry
                    //
                    CravensPlugin.Utils.UpdateEntryStats(entry.EntryId);

                /* ADDITIONAL CODE NOT SHOWN... */
                }
            }
        }
    }
}

This content is cached by the by dasBlog. I have done some studies to determine when the UpdateEntryStats is called.

  • The count is not updated if you simple refresh you page.
  • The count is updated if you hit view an entry from another session.

This placement seems to provide the appropriate behavior. I will monitor it and update if I determine otherwise.

Tags:,
Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

*