Thursday 15 November 2012

SharePoint Hit Counter

If you've ever enabled and viewed the Usage Analysis reports for SharePoint, you'll already know that they're...well, lacking. I was recently requested to provide hit counts for annoucements on a coporate Intranet and rose to the challenge, of which their were many. :)
Now while there are many ways to skin this cat, my requirements were to provide an ad hoc solution that can could be enabled at the list level by anyone with the Manage List permission level.
I have described a different approach [SharePoint Page View Counter] for Publishing Pages that uses a dedicated list, gathers more information, and comprises a custom webcontrol that is then referenced in the site's custom master page.
The following describes a MOSS 2007 solution comprising of two web parts - a Hit Counter and a Hit Viewer - as well as a site column definition, and is deployed as a site level feature. This could be easily modified for 2010.
The end result will look something like this.
Fig.1 - Hit Viewer web part
Fig.2 - Hit Counter web part
I use WSPBuilder for convenience when creating MOSS solutions, so some of these steps will vary.
Start by adding a web part feature to your project called CounterWP. Then add a second web part called HitViewerWP.
Open up your elements.xml file and add the following field definition for the site column.
There are several advantages to deploying the site column. Firstly, it removes any guess work or room for error on behalf of the content author who is adding it to the target list. Secondly, we're able to hide the field from any of the list form views; something you can't really do via the UI.

HitCounter Web Part

Now let's create a class file for the Counter web part. I'm inheriting from System.Web...WebPart class just to reduce the using footprint. First thing I want to do is create my global booleans for an ID key/pair value in the current URL and the presence of the custom Counter field.
The rest of the code is commented pretty heavily but I'll break it up anyway. As we only want the code to run on DispForm pages with an valid ID present, I'm going to check for the ID first in the OnInit event. No point continuing otherwise.
If the ID is there we can then grab the current context in the OnLoad event. Because we're elevating privileges, we want to get our contexts before trying any update methods.
We don't want the counter field to increment when the page is in Edit mode, or the field doesn't exist, so we'll check for both.
Now we can create our elevated privileges block and open a new SPSite instance to re-grab our needed objects. I do an additional (redundant) check for the Counter field because that's how I roll. You could/should just as easily use a try/cach block.
Then check for a null value on Counter and set it to zero. This is important, because if we add the field to a list with potentially hundreds of existing items, we don't want to have to set a value for all of them. New items get the default value of zero, as defined in the field definition.
Notice that I check the field value's object for null. Also important. Checking for counterStr as a null or empty string does not yield the correct result. Don't ask me why.
We then increment the current value by 1 to count the current hit.
Now we're ready to perform our updates. As we're wanting to update the database from a GET request, we need to set allowunsafeupdates to true on the SPWeb.
We're also going to do an additional check for Content Approval on the list object and approve the item at the same time, otherwise we'll leave the updated item in a Pending state.
Here's the rest of our OnLoad code.
Now let's render the results to screen to provide feedback for the user. This is optional but I provide it here for completeness. I used the RenderContents method for future proofing.
And that's it for our CounterWP. Build your project and let's move on to our HitViewerWP.

HitViewer Web Part

Again, I inherit from System.Web.UI and define a couple of globals for error checking and to get/set the user-selected query for the report viewer.
I chose to check and set a few web part properties during initialising. Again, this is optional.
Now add the CreateChildControls method which will call our render code. As I'm rendering multiple controls this method is preferable to the RenderContents method in this scenario.
Our LoadLists function firstly gets the list collection for the current SPWeb and adds the user controls for the view selector dropdown list.
I then chose to add a script block with a little jQuery goodness to collapse the lists on page load, and an onclick event to toggle the items display. If you don't already have a jQuery reference in your masterpage, you will need to add a reference to the library here as well.
").AppendLine(); ]]>
Then begin rendering the container and grab only those lists whose field collection contains our Counter field. No point querying lists that don't and causing performance issues.
Now let's try to grab the list titles and define the queries for the view dropdown list. I provided two options. The default query includes a filter that grabs only those items modified in the last 30 days. The second doesn't discriminate. It will grab all items.
In both cases I've restricted the queries to the first ten items matching the criteria. I also specify only the ViewFields required to render my items. This is again for performance.
I use SPQuery over other methods (such as SPSiteDataQuery and CrossListQueryInfo) because I already have my filtered list collection, and because it offers me more control.
Now we can define the render method for our list items, catch any exceptions, and add our literal control to the page.
Finish off by adding our exception handling function and we're done!

In Closing...

Because we're updating the list item, any list views that order by modified date will be affected. There are pros and cons to this. The pro, is that your list will now be effectively sorting by popularity (most recently viewed items.
If this isn't what you want, or you have the need to keep some items 'sticky', then you have a number of options. Sort the view by Created date, create a new Yes/No column to keep important items at the top...the choice is yours. In my case I did both.
As a final touch you could use a little jQuery to check for a 'yes' value in your 'sticky' field and append a ! to the item's title to make it clear why some items are at the top.
Enjoy! And as usual, I look forward to any feedback.

No comments: