Post View Counter

Tuesday, 07 April 2009 07:42 AM
by Coose

My wife wanted to see the number of views she had on each post, so I began the hunt to plug in a view counter for BlogEngine.NET.  I liked the simplicity of this one I found at Moses of Egypt.net.  But after installing, I decided that I wanted to track some information, and I wanted it to go into my SQL database instead of an XML file.

My first pass included a LINQ to Entities model, but due to the artifacts required, that has to be in an assembly (well, it doesn't have to be…but it's easiest that way).  When I started to post it, I though it would be much easier as a simple extension that could be dropped in the BlogEngine.NET extensions directory and go.

So an easier solution to that is a LINQ to SQL context.  It's not going away that soon, so I'm posting the LINQ to SQL solution here.

Database

The first step is the database.  I just want to track the post identifier, date of the view, source of the view (IP address), and the referrer.  So my table looks like this:

Post View Schema

The script to create the table:

USE [BlogEngine] 
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[fm_PostViews](
[Id] [uniqueidentifier] NOT NULL CONSTRAINT [DF_fm_PostViews_Id] DEFAULT (newid()),
[PostId] [nvarchar](255) NOT NULL,
[ViewDate] [datetime] NOT NULL,
[ViewSource] [nvarchar](50) NULL,
[ViewReferral] [nvarchar](255) NULL,
CONSTRAINT [PK_fm_PostViews] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,
ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

and create in index on PostId

USE [BlogEngine] 
GO
CREATE NONCLUSTERED INDEX [IX_fm_PostViews_PostId] ON [dbo].[fm_PostViews]
(
[PostId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF,
IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

 

Data Context

Open Visual Studio, and open your BlogEngine.NET web site.  Right click the Extensions folder, select Add New Item…, then select LINQ to SQL Classes I named mine PostViews.dbml.

Here's a trick, so pay attention:
I put my post view table in the same database as BlogEngine.NET.  Most people will.  Now, when you add the table to the LINQ to SQL design surface, the Connection property will be set to use Application Settings, which is what we want.  BUT, it will create a new application setting for you, which is NOT what we want.  So, remove the newly added connection string from the connectionStrings section of the web.config file.  Save the LINQ to SQL designer (dbml file).  Then right-click on it in the Solution Explorer, select Open With | XML Editor.  Now, change the ./Database/Connection[@SettingsPropertyName] value to "BlogEngine" (or whatever you have defined in your web.config file for BlogEngine.NET).

<Connection Mode="WebSettings" ConnectionString="" SettingsObjectName="System.Configuration.ConfigurationManager.ConnectionStrings" SettingsPropertyName="BlogEngineConnectionString" Provider="System.Data.SqlClient" />

Now, the LINQ to SQL context is configured to use the same database as BlogEngine.NET.

Close the LINQ to SQL XML and open it again with the normal designer. Drag the table created above to the LINQ to SQL design surface.  You can change the name of the object in the data context so something prettier.  I changed mine to PostView.

 

Extension Code

Now, create a new class in the Extensions folder:

    1 using System;

    2 using System.Globalization;

    3 using System.Linq;

    4 using System.Text.RegularExpressions;

    5 using System.Web;

    6 using BlogEngine.Core;

    7 using BlogEngine.Core.Web.Controls;

    8 

    9 /// <summary>

   10 /// Summary description for PostViews

   11 /// </summary>

   12 [Extension("Counts and displays number of views for a post", "1.0", "<a href=\"http://www.funkymule.com\">Funky Mule</a>")]

   13 public class PostViews

   14 {

   15     private static ExtensionSettings _settings;

   16 

   17     public PostViews()

   18     {

   19         Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);

   20         InitSettings();

   21     }

   22 

   23     void Post_Serving(object sender, ServingEventArgs e)

   24     {

   25         IPublishable ipub = ((IPublishable)sender);

   26 

   27         int viewCount = -1;

   28 

   29         using (Extensions.PostViewsDataContext ctx = new Extensions.PostViewsDataContext())

   30         {

   31             //Check For Single Post View, When viewing Specific Post, basically through post.aspx

   32             if (e.Location == ServingLocation.SinglePost)

   33             {

   34                 string pattern = _settings.GetSingleValue("ExecludedIPs");

   35                 string ip = HttpContext.Current.Request.UserHostAddress;

   36                 bool matchedIp = false;

   37                 if (!string.IsNullOrEmpty(pattern) )

   38                 {

   39                     matchedIp = Regex.IsMatch(ip, pattern);

   40                 }

   41 

   42                 //Do not count view of authenticated users and users who have IPs match execluded IPs pattern

   43                 if (!matchedIp && !HttpContext.Current.Request.IsAuthenticated)

   44                 {

   45                     Extensions.PostView pv = new Extensions.PostView();

   46                     pv.Id = Guid.NewGuid();

   47                     pv.PostId = ipub.Id.ToString();

   48                     pv.ViewDate = DateTime.UtcNow;

   49                     pv.ViewReferral = HttpContext.Current.Request.UrlReferrer == null

   50                         ? string.Empty : HttpContext.Current.Request.UrlReferrer.ToString();

   51                     pv.ViewSource = HttpContext.Current.Request.UserHostAddress;

   52 

   53                     ctx.PostViews.InsertOnSubmit(pv);

   54                     ctx.SubmitChanges();

   55                 }

   56             }

   57 

   58             if (e.Location == ServingLocation.PostList || e.Location == ServingLocation.SinglePost)

   59             {

   60                 string postId = ipub.Id.ToString();

   61                 viewCount = ctx.PostViews.Where(pv => pv.PostId == postId).Count();

   62             }

   63         }

   64 

   65         if (bool.Parse(_settings.GetSingleValue("AuthenticatedOnly")) && !HttpContext.Current.Request.IsAuthenticated)

   66             return;

   67 

   68         if (viewCount > 0)

   69         {

   70             e.Body += string.Format(CultureInfo.InvariantCulture, "<br /> {0} Views", viewCount);

   71         }

   72     }

   73 

   74     private void InitSettings()

   75     {

   76         ExtensionSettings settings = new ExtensionSettings(GetType().Name);

   77         settings.IsScalar = true;

   78         settings.AddParameter("AuthenticatedOnly", "Display to authenticated only", 5, true);

   79         settings.AddParameter("ExcludedIPs",

   80             "<a href=\"https://www.google.com/support/googleanalytics/bin/answer.py?answer=55572\" target=\"_blank\">Execlude IPs</a>",

   81             255, false);

   82         settings.AddValues(new string[] { "True", string.Empty });

   83         settings.Help = "<p>Set <strong>Authenticated Only</strong> field to <strong>True</strong> if you wish " +

   84           "<strong>only authenticated users</strong> to view total number of views of each post.<br/>" +

   85            "Set <strong>Execlude IPs(regex)</strong> field if you wish to execlude range of IP addresses " +

   86            "from accumulating post view count. This is Regular Expression field.</p>" +

   87            "You can use <a href=\"https://www.google.com/support/googleanalytics/bin/answer.py?answer=55572\" target=\"_blank\">this tool</a> " +

   88            "to generate your range of IPs ";

   89         ExtensionManager.ImportSettings(settings);

   90         _settings = ExtensionManager.GetSettings(this.GetType().Name);

   91     }

   92 }



Now you  have data that you can mine.

Thanks to Moses of Egypt for a great and simple start to where I went.  If you don't want to use SQL, download Moses' version which writes to an XML file.

Comment on this
Development
|