Here’s an example of why asking the right questions can lead to a rockslide. This question was received by email:
Do you know if there is a way to monitor how many users are currently logged onto a MOSS server? I’ve searched high and low and can’t find this simple bit of information.
The closest I’ve gotten was the Performance tool under Administrator Tools but I’ve been able to see InfoPath forms in memory and “number of connections since the machine was first installed” but not “Currently connected users” anywhere.
Your question is as old as the web itself, and while the question may seem simple, the answer is not. I’ll elaborate.
The problem with ‘logged in’
The difficulty of answering this lies in the fact that HTTP is completely stateless. The technical term ‘logged in’ exists only for as long as a single request is processed, usually a second or less, so concurrent users are very rare. The problem is that the browser doesn’t have any kind of connection to the server after the page is delivered. So, if I close my browser after opening the home page or if I sit and spend 10 minutes reading the announcements, the server wouldn’t know the difference.
However, the normal meaning is to somehow detect if someone is still working on the web after having done something to ‘log on’ and haven’t switched over to YouTube. And the answer, from a technical point of view is: You can’t. From a marketing point of view the answer is: Sort of. The trick lies in predicting how interesting your stuff is and how long users will spend reading each page. Then, set an timeout for how long you think it would take the user to click the next link. If they click that link you consider them still logged in and if not you consider them logged out.
The logic of determining logged in
You might think that the front page takes less time to read, but is read more often, than, say, a long report. So, you set your timeout to 15 minutes so that all requests made within 15 minutes are considered the same ‘session’ based on the front page. However, the user, once they find what they are looking for, may spend 45 minutes reading a certain document without any server interaction, even though they would still be ‘logged in’ to the site using the same session. So, you set your timeout to 45 minutes. But of course, in 45 minutes a user may have ‘logged in’, seen what they want on the front page, closed the browser, had lunch, seen an episode of the Simpsons, worked on two other projects, before returning to see if something new happened after 37 minutes. Hardly being ‘logged in’ but still the same session according to the 45 minute timeout.
So, you have a bit of logic work ahead of you to make this accurate or reliable. You might get a ball-park figure using a given timeout, and that might be all you need. So, I’ll suggest a solution and present it to you. Here’s what we’ll do:
Using DelegateControls you can insert code in your pages. If you haven’t seen how yet, grab the DelegateControls excerpt from the “Building the SharePoint User Experience” SharePoint Developer book. You need to sign up for the mailing list, though, but doing so is free. Using a DelegateControl we will insert a piece of code into each page that will log the time the user was last active in their SharePoint user information.
Then, when you need to see how many users are logged in, we just iterate through that list and count how many users have logged in for the last X minutes. We will do this on a custom application page. This is the ball-park solution which I’ll explain in this article.
However, since this is sort of a useful thing for many people, I’ve decided to take this a bit further. In the next couple of weeks I will write an eBook that will cover a more advanced solution. More on that after we’ve gone through the basic solution.
Let’s get started.
What you will need
Of course, you’ll need SharePoint. And you will need Visual Studio. I’m still using VS2005, mainly to make sure everyone can use the code even if they haven’t upgraded to VS2008 yet.
I’ll use WSPBuilder, including the Visual Studio extensions, for this project, so grab a copy of that. I also prefer to have SharePoint Manager 2007 installed. Besides that, your regular setup should work.
Our two main goals are as such:
- Create a DelegateControl to log user access
- Create a custom application page to report and set session timeout
Our rules of engagement are:
- We will use only supportable methods and not harm a single Microsoft-provided file.
If we modify any file that ships with SharePoint your solution will be non-supportable by Microsoft. But that’s another show.
- We will use the least amount of effort possible
This article is supposed to show you how to do something, not necessarily give you a ‘best practice’ approach to anything. Expect to add error handling yourself, for example.
With that our of the way, let’s start with creating our DelegateControl.
Creating the solution
Our solution will have three features. We will get back to more details around these a bit later, but for now, let’s quickly set up the Visual Studio solution.
In Visual Studio, create a new WSPBuilder project, and call it something nice. I’ve called mine CurrentUsers, you can call yours Al if you like. A tip is to use an easy recognizable name as a prefix to your features as well. Your features will be deployed in a folder, so if they have the same prefix, such as CurrentUser or Al if that’s you preference, they will end up close together for easy location.
Next, right click your project and add a new item. The type should be Feature with receiver, and you should name it something with control or DelegateControl. I’m calling mine CurrentUserDelegateControl. Add a nice description if you like and make sure you set the scope to Web.
Now, you may wonder why I chose to go with a Feature with receiver instead of just a blank feature. The reason is that we will need to get our 4-part strong name and we need to sign our assembly to get that. When we use a Feature with receiver item, WSPBuilder will both create a ke
y for us, sign the assembly, and extract the strong name for us, so we save a lot of time.
Next, do the same thing for two more items, one called CurrentUserAdministration and one called CurrentUserSetup. The Administration feature will be responsible for managing our current users solution while the Setup feature will add the necessary columns to our user list, activate the two other features, and add the properties where we store our configuration. We’ll get back to these two features a bit later.
A DelegateControl is a SharePoint ASP.net control that acts as a placeholder for content or other controls. Think regular ASP.net content placeholders, but with a SharePoint feature deployment method, meaning you can activate content on a page as you would activate any other feature in SharePoint.
Lucky for us, the default SharePoint pages include several DelegateControls that we can utilize to our advantage. The functionality we want to create here is to log the time when a user hits any page. For our purpose, we don’t need to output anything, so the DelegateControl called AdditionalPageHead is perfect. The AdditionalPageHead is located inside the Head section of the default.master master page, and it even allows multiple controls to be included, so we know we won’t have any conflict with other features.
We have two options for our DelegateControl. We can create an ASCX control, or we can skip it. We would use an ASCX control if we wanted to have a visual interface for the user. In our case, however, we just want to log the request, not display anything, so we will go for the other option and just create the class file. We’ll do this in the elements.xml file of the DelegateControls feature, so open that file now. It should contain an empty elements XML element.
At this point I do hope you have set up intellisense so you get some help writing the code. Of course, you could just paste the code I’ll provide, but it is more fun to write yourself. If you haven’t done so you can check out my article on getting intellisense for onet.xml and most other CAML files.
To your elements.xml, add the following code:
ControlClass="[NAMESPACE AND CLASSNAME]"
You need to replace the [STRONG NAME] and [NAMESPACE AND CLASSNAME] with your own value. Here’s is where that Feature with receiver makes sense. Open your feature.xml file and you should find two attributes to the Feature element called ReceiverAssembly and ReceiverClass. You can simply copy the values from these attributes, ReceiverAssembly into ControlAssembly and ReceiverClass into ControlClass. Simple, eh?
Oh, delete the ReceiverAssembly and ReceiverClass attributes from the feature element while you are in there; they won’t work since we’ll be using the class for our control instead.
The remaining two attributes in the Control element are Id and Sequence. The Id maps our control to the correct DelegateControl. To find the correct Id value, locate the DelegateControl in the ASP.net page, in our case the default.master, and look for the ControlId property.
The Sequence attribute is also important. The Sequence basically tells SharePoint the order in which to process multiple Controls. To understand how this works, consider having two features that both adds content to the same DelegateControl. Now, DelegateControls in a SharePoint page can set the AllowMultipleControls to True, in which case both the features’ content gets added, with the feature with the lowest Sequence added first. The AdditionalPageHead is an example of a control that allows multiple content.
If the DelegateControl does not set the AllowMultipleControls to true the only the feature with the lowest Sequence gets added at all. The other features will not have their content added.
Your feature.xml file should now look like this:
Description="Add DelegateControl to all pages"
Note that the Feature Id will be different in your solution.
Right, with out element and feature set up correctly we now turn to our code file. Open the CurrentUsersDelegateControl.cs file. It will be set up as a feature receiver, so just trim everything down so only the class declaration remains. Make sure your class is public and inherits from WebControl, which means you should also be using System.Web.UI.WebControls;. Before we add any code, your class file should look like this:
public class CurrentUsersDelegateControls : WebControl
We need to do something when our control loads, so let’s override the OnLoad method. Inside we want to log that the user has accessed a page and save that information in the user’s information. So, add the following method to your class:
protected override void OnLoad(EventArgs e)
SPWeb web = SPContext.Current.Web;
SPUser spUser = web.CurrentUser;
SPList userList = web.SiteUserInfoList;
SPListItem user = userList.Items.GetItemById(spUser.ID);
user["LastPageHitTime"] = DateTime.Now.ToString();
web.AllowUnsafeUpdates = true;
web.AllowUnsafeUpdates = false;
What we’re doing here, line by line, is:
- Get a reference to the current web.
- Get a reference to the current user
- Find the User information list (web.SiteUserInfoList)
- Get the SPListItem that represents the current user in the User Information List
- Set the “LastPageHitTime” property of the user item.
- Update the user item to save the new information.
Really simple, yes? Some things to notice, however.
First, to be allowed to make updates in a page retrieved using the GET HTTP method we need to allow unsafe updates. This is a security feature, so don’t set this willy-nilly. In our case we are quite safe, however; we are not retrieving any parameters or doing other unsafe stuff.
Second, we are retrieving the user information list using the web.SiteUserInfoList. In my article on SharePoint Event Receivers I retrieved the list using its English name (SPList list = web.lists[“User Information List”]). It is much better to retrieve the web.SiteUerInfo, since the name of the list may be different in different language packs. My bad, I’ll blame being plain stupid back when I wrote the other article.
Oh, just in case you didn
’t know: User information in SharePoint is stored in a regular list, just like most other information. You can work with these items like you work with any other list item in SharePoint.
You may also be surprised to see that we are updating the column “LastPageHitTime”. That column doesn’t exist, so we’ll need to add that later. We’ll get to that when we create our Setup feature.
We’re done with the DelegateControl, so let’s move on to our administration feature.
Next we turn to our administration. What we are going to do here is add a custom administration page to our solution. We also want to add a link to our administration page from the site settings page. Let’s do the last first, so open your elements.xml file in the CurrentUsersAdministration feature. As with the last feature, it should only have and empty Elements element. Add the following code:
What we’re doing here is adding a custom action, using, you guessed it, the CustomAction element. This element has two crucial attributes that determines where it will be placed; the GroupId and the Location. Combined, these two attributes determines in which menu or list our action is added. The process of finding these values for all possible positions, however, is a bit out of the scope of this article, but one tip is to investigate the SiteSettings feature and its SiteSettings.xml file. You can also look at John Holliday’s CustomAction identifier list, but please not that his list is not complete and that it includes elements from MOSS.
The Rights attribute determines what permissions users would need in order to see the link. This does not affect the actual page, though, so we need to add some sort of security to our actual page in a moment. Finally, the child UrlAction element defines where our custom action link will go. We point this to a page inside the _layouts directory so we need to add that file. Let’s do that now.
One nice thing about WSPBuilder is that it adds a root 12 folder to our solution. This 12 folder maps to all the folders in the 12-hive () of your SharePoint installation. We know, or at least I know, and you will know in about 5 seconds, that the _layouts virtual folder is mapped to the TEMPLATE\LAYOUTS physical folder inside . So, to make sure we have our page deployed into the right folder, inside your Visual Studio solution explorer, create a new folder under the 12\TEMPLATE folder called LAYOUTS. Inside that folder, add a new text file and call it CurrentUsersAdmin.aspx.
By the way, adding a text file, but naming it .aspx will cause Visual Studio to give us an empty file but treat it it like an ASP.net page, even though out project is not a web project.
To keep with our rules of engagement, we keep our page as simple as possible. Open your new aspx file, and add the following code:
<%@ Assembly Name="[STRONG NAME]" %>
<%@ Page Language="C#"
Inherits="[NAMESPACE AND CLASS NAME]" %>
Again you need to add the strong name of your assembly and the namespace and class to your file. Good news is that you can use the exact same method as last time. Just open your feature.xml file in the Administration feature and grab the ReceiverAssembly, which replaces the Assembly Name property in your code, and the ReceiverClass, which replaces the Inherits property above. Oh, yeah, and like last time, get rid of the ReceiverAssembly and ReceiverClass attributes in the feature.xml file as we’ll use the class file as the code-behind for our page.
The remained of the code is just a simple interface that gives the user a simple interface to set the timeout for a session and then the number of users.
We don’t really need much else for now, but we do need to hook up the functionality, though, so let’s turn to the class file. Open that file, strip down everything except the using statements and the class declaration. The class should be made public and then inherit from LayoutsPageBase, which means you should be using Microsoft.SharePoint.WebControls; At this point your class file should look something like this:
public class CurrentUsersAdministration : LayoutsPageBase
Next for the filling. Start by adding using statements as such:
We’ll need the Microsoft.SharePoint.Utilities to use a normal DateTime object in our CAML query later. I’ve explained this earlier in my article Convert DateTime to ISO8601 for use in SharePoint CAML queries, and we’ll see this in action in a moment.
Second we need protected members to map to our controls. Add the following inside your class:
protected TextBox TextBoxMinutesPerSession;
protected Label LabelMinutesPerSession;
protected LinkButton LinkButtonUpdate;
protected Label LabelResults;
This class is more complicated than the DelegateControl class, as we need to do a lot more. We start out overriding the OnInit method. I’ve added comments inline:
protected override void OnInit(EventArgs e)
// Make sure we have our child controls
// Hook up the update LinkButton
LinkButtonUpdate.Click += new EventHandler(LinkButtonUpdate_Click);
// Get a reference to the current web
SPWeb web = SPContext.Current.Web;
// Next, try reading the current session duration…
TextBoxMinutesPerSession.Text = web.Properties["CurrentUserSessionDuration"];
// …and if it is not set, use the default.
TextBoxMinutesPerSession.Text = "15";
Note the use of the web.Properties to read the current session duration. We can store custom properties in any number of objects, including SPWeb and SPListItem. However, if you want to store custom properties you need to use the list’s SPFolder object instead. I’ve also recently written an article on storing custom properties in
an SPFolder object.
A natural progression here is to go to the LinkButton method created:
void LinkButtonUpdate_Click(object sender, EventArgs e)
SPWeb web = SPContext.Current.Web;
web.Properties["CurrentUserSessionDuration"] = TextBoxMinutesPerSession.Text;
Still a fairly simple method, all we want to do is store the new session duration in the web properties. Note that we need to call Properties.Update() whenever we update any properties on any SP object.
One important thing about custom properties. You need to add them to the collection before you can update their values. We haven’t done this yet, but we will once we get to the Setup feature.
Finally for our Administration class, we need to add the real meat; the OnLoad method, again with inline comments:
protected override void OnLoad(EventArgs e)
// Set up some variables
SPWeb web = SPContext.Current.Web;
SPList userList = web.SiteUserInfoList;
SPQuery query = new SPQuery();
// Try reading the session duration, with a bit of error handling.
sessionLength = double.Parse(TextBoxMinutesPerSession.Text);
throw new SPException("You need to enter a number, in minutes, for session length");
// Convert the date
string sessionTime =
// Build our query. Note the use of IncludeTimeValue="True" to check against time
query.Query = @"
// Get the users that have logged in since now minus session length
SPListItemCollection users = userList.GetItems(query);
int userCount = users.Count;
// And output the result
LabelResults.Text = userCount + " current users";
Couple of things to notice:
As mentioned earlier we must use the SPUtility.CreateISO8601DateTimeFromSystemDateTime to get our DateTime value into a format that SharePoint can use.
Also, since we want to compare the time as well as the date, in our CAML query we need to add the IncludeTimeValue attribute on the Value element.
Right, that actually concludes our Administration page. Onwards to wrap all this together in the Setup feature.
I always include a Setup feature in my projects. The reason is that I want more control over how a solution gets provisioned. There may be certain requirements that are simply cannot be met without writing code, and if nothing else, it allows me to activate features in the order I want.
In our Setup feature, you can simply delete the elements.xml file, since we wont need it. Also, remove the reference to the elements.xml file from the feature.xml file. All our work will be done in the feature activation handler, so turn to the CurrentUsersSetup.cs file and open that file.
Oh, and this time, do not delete the ReceiverAssembly or ReceiverClass attributes from the feature.xml, or all hell will break loose.
In the CurrentUsersSetup.cs file, delete all the nasty throw new Exception("The method or operation is not implemented."); from all the methods.
Next, add the following code to your FeatureActivated method, again with inline comments:
public override void FeatureActivated(SPFeatureReceiverProperties properties)
SPWeb web = (SPWeb)properties.Feature.Parent;
SPList userList = web.SiteUserInfoList;
// Add required field to user list
if (! userList.Fields.ContainsField("Last Page Hit Time"))
userList.Fields.Add("LastPageHitTime", SPFieldType.DateTime, false);
SPFieldDateTime lastPageHit = (SPFieldDateTime)userList.Fields["LastPageHitTime"];
lastPageHit.DisplayFormat = SPDateTimeFieldFormatType.DateTime;
lastPageHit.Title = "Last Page Hit Time";
// Add properties to web
if (web.Properties["CurrentUserSessionDuration"] == null)
// Activate features
if (web.Features[new Guid("[ID_OF_DELEGATECONTROL_FEATURE]")] == null) web.Features.Add(new Guid("[ID_OF_DELEGATECONTROL_FEATURE]"));
if (web.Features[new Guid("[ID_OF_ADMINISTRATION_FEATURE]")] == null) web.Features.Add(new Guid("[ID_OF_ADMINISTRATION_FEATURE]"));
I’ll explain a bit more about what we are doing here.
First we get references to the current web and its user list. We do this by retrieving the properties.Feature.Parent and casting that to SPWeb. We know the Parent object is an SPWeb since our feature is web scoped.
Second we add the new column to the User Information List. As I mentioned earlier we can work with the User Information List just like any other list, including adding columns. Note that I rename the column after I have added it. This is to ensure that we get a short and manageable name. If we add the column with a name containing special characters such as space, SharePoint will replace those with an Unicode code such as _0x0200_ which sort of makes a mess of things.
Third we add the CurrentUserSesstionDuration custom property to the web to make sure we have a place to store our current session length. Again, remember to call Properties.Update() to make sure your property is stored.
Finally we activate the two other features. We do this by adding the Guid Id of the two features. You need to replace the placeholder strings here with the actual Feature Id, which you can get from the respective feature.xml files of the DelegateControl and Administration features.
That’s it! We’ve done all our development work. Just a few more steps before we can harvest the fruits of our labor.
Deploying your SharePoint solution
Before we can deploy we need to build our WSP file. Since we are using WSPBuilder, this is as easy as right-clicking the project node in solution explorer and click ‘Build WSP’.
Second we need to deploy, and golly-gosh this is just as easy when we have WSPBuilder to hold our hand. Again, right-click on the project node and click ‘Deploy’.
When you are ready to put this into production, which of course you would never do since this is illustrative code only, you would right-click the node and click ‘Create deployment folder’.
God, I love WSPBuilder.
However, we still need to do one manual thing. We need to allow our DelegateControl to run on the pages. This calls for an update to the web.config file. If you are building for production you can include this information in the manifest.xml file in the WSP automatically, but using our approach with lazy right-click deploy in WSPBuilder, we need to add this manually to our web.config file.
Don’t worry, this is real easy. Find the web.config file, usually located in C:\Inetpub\wwwroot\wss\VirtualDirectories\[postnumber]. Open the file and locate the SafeControls section in the SharePoint node. Add the following line:
One final time you need to find your strong name, but after two former attempts you should know well where to find this information. Replace the CurrentUsers namespace as well if you called your solution something else. Such as Al.
Hey, time to test our stuff.
Activate and test
The final and most fun part is to te
st our solution. First, go to the site settings page of your solution and activate the Current Users Setup feature only. The other two features should activate automatically.
Next, go back to the site settings page. Under the Users and Permissions, you should now see the link to your custom application page:
Clicking that link should take you to the administration page, which will, if you’ve done everything correctly, something like this:
Oh, and in case something doesn’t work, I’ve added the entire solution, ready for you to download and investigate. You can download that file here.
What about that eBook?
Ok, so this is a rather simple solution, but it has plenty of potential. There are still some problems we haven’t addressed.
First, we have only one session duration. How about that report that takes 45 minutes to read? We don’t have a good solution for this yet.
Second, when we have a nice method of logging when users are accessing any page, so it should be fairly easy to expand our solution to include which pages users are currently accessing.
Finally it would be really nice to se who is actually ‘logged in’ in addition to just counting the number.
To make this into a more advanced solution is a bit beyond the scope of this single article. So, I’ve decided to write an eBook describing a more advanced solution. Not just that, I’ve decided to start a new SharePoint Journal, called Understanding SharePoint Journal, which will focus on teach SharePoint from a task-oriented perspective.
The first issue will be released Monday February 23 2009 and will be tightly coupled with the next surprise. I’ve created and uploaded a new project on CodePlex, called SPCurrentUsers, which contains a more advanced solution than what has been shows here.
USP Journal Volume 1 Issue 1 will explain SPCurrentUsers and all its features in great detail. I’ll post more on this shortly.
.b has left the building!
I hope you’ve enjoyed yourself and learned something you can use in your own projects. If you like this stuff, do me a favor and tell me. Heck, even if you hate it, I’d love to hear from you. Send me an email at furuknap<[at]>gmail.com or comment here. If you don’t like writing, at least vote in the poll below.
Thanks for listening, and hope to see you again soon.
Found this article valuable? Want to show your appreciation? Here are some options:
a) Click on the banners anywhere on the site to visit my blog's sponsors. They are all hand-picked and are selected based on providing great products and services to the SharePoint community.
b) Donate Bitcoins! I love Bitcoins, and you can donate if you'd like by clicking the button below.
c) Spread the word! Below, you should find links to sharing this article on your favorite social media sites. I'm an attention junkie, so sharing is caring in my book!