Forum Topic subscription using attributes

by: Joel Abrahamsson

A very common functionality in a web based forum is the ability for users to track or subscribe to topics (threads). That is, when a new reply is posted in a forum thread all users that are subscribing to the thread, except the author of the reply, receives an e-mail, direct message or some other form of notification. Once a subscriber has received a notification he shouldn’t be notified again until he has viewed the topic again.

EPiServer Community doesn’t provide this functionality out of the box, but there are a number of ways to implement it ourselves. One of the easiest is to use the attribute system which is what I will do in this article.

The solution will consist of two new attributes added to the Topic type and a utility class, TopicSubscriptionUtility, for handling subscriptions. The actual implementation of adding subscribers, notifying subscribers etc can be done in many different ways, but I’ll show a few basic usage examples for the utility class.

Adding the attributesattributes

Let’s begin by adding the attributes. To add an attribute you:

  1. Navigate to the Community tab in EPiServers edit mode.
  2. Click the Create Attribute button.
  3. Fill in the name of the attribute, select the type to create the attribute for (EPiServer.Community.Forum.Topic in our case) and select the type of the attribute (EPiServer.Common.Security.IUser in our case).
  4. Click the Save Information button.

Add two attributes of the type EPiServer.Common.Security.IUser to the type EPiServer.Community.Forum.Topic, one named “Subscribers” and one named “NotifiedSubscribers”.

dialog

The TopicSubscriptionUtility class - Methods

The TopicSubscriptionUtility class will consist of methods for:

  • Adding subscribers, to be used when a user wants to subscribe to the topic, for instance by clicking a button or perhaps checking a checkbox when posting a reply.
  • Removing subscribers.
  • Check if a user is a subscriber, to ensure that we don’t add a user as a subscriber twice and to be able to hide any Subscribe-to-this-topic-buttons.
  • Getting a list of all subscribers. Used by the other methods in the class and for displaying the number of subscribers to a topic.
  • Getting a list of all subscribers that have not been notified since they last viewed the topic, to be used to get a list of all subscribers to notify when a new reply is posted.
  • Setting subscribers as notified, to be used when none-notified subscribers are notified.
  • Clearing a subscriber from the list of notified subscribers, to be used when a subscriber views the topic.

Creating the TopicSubscriptionUtility class

Below I’ll describe how to implement the above mentioned methods. If you just want to implement this functionality and don’t care how it works you can skip this part and just download the source code for the TopicSubscriptionUtility class here.

The first step is to create the class, add necessary using statements and two constants with the names of the recently added attributes:

using System.Collections.Generic;
using System.Linq;
using EPiServer.Common.Security;
using EPiServer.Community.Forum;
 
namespace EPiServer.Templates.RelatePlus
{
    public class TopicSubscriptionUtility
    {
        private const string SUBSCRIBERS_ATTRIBUTE_NAME 
            = "Subscribers";
        private const string NOTIFIED_SUBSCRIBERS_ATTRIBUTE_NAME 
            = "NotifiedSubscribers";
    }
}

The GetSubscribers method

The first method we’ll implement is the method for getting a list of all subscribers, the GetSubscribers method, as it will be used by many of the other methods.

public static IList<IUser> GetSubscribers(Topic topic)
{
    return topic.GetAttributeValues<IUser>(
        SUBSCRIBERS_ATTRIBUTE_NAME);
}

The method simply delegates to the GetAttributeValues method in the topic to retrieve a list of users who are subscribing to the topic, that is, the value of the Subscribers attribute.

The AddSubscriber method

The method for adding a subscriber is named AddSubscriber and is implemented like this:

public static void AddSubscriber(Topic topic, IUser user)
{
    IList<IUser> subscribers = GetSubscribers(topic);
    subscribers.Add(user);
    topic = (Topic) topic.Clone();
    topic.SetAttributeValue<IUser>(
        SUBSCRIBERS_ATTRIBUTE_NAME, subscribers);
    ForumHandler.UpdateTopic(topic);
}

The method retrieves the list of subscribers, adds the new subscriber to it and finally saves the modified list as the new attribute value for the Subscribers-attribute.

The IsSubscriber method

The IsSubscriber method retrieves the list of subscribers for a topic and checks whether the specified user is in that list. In order to implement this method we also create a helper method named IsUserInList. Note that this method doesn’t use the list.Contains()-method but instead checks if there is a user in the list with the same ID as the specified user. We do this as there might be two separate IUser objects that represent the same user, one from the list of subscribers and one from somewhere else and we don’t want to rely on the default comparer when we compare them.

public static bool IsSubscriber(Topic topic, IUser user)
{
    IList<IUser> subscribers = GetSubscribers(topic);
    return IsUserInList(subscribers, user);
}
 
private static bool IsUserInList(IList<IUser> list, IUser user)
{
    IUser userInList = list.FirstOrDefault(
            u => u.ID == user.ID);
 
    return userInList != null;
}

The RemoveSubscriber method

The RemoveSubscriber method removes the specified user from the list of subscribers. It also removes the user from the list of notified subscribers if the user is in that list.

public static void RemoveSubscriber(Topic topic, IUser user)
{
    IList<IUser> subscribers = GetSubscribers(topic);
    subscribers.Remove(user);
    topic = (Topic)topic.Clone();
    topic.SetAttributeValue<IUser>(
        SUBSCRIBERS_ATTRIBUTE_NAME, subscribers);
 
    IList<IUser> notifiedSubscribers = GetNotifiedSubscribers(topic);
    if(IsUserInList(notifiedSubscribers, user))
    {
        notifiedSubscribers.Remove(user);
        topic.SetAttributeValue(
            NOTIFIED_SUBSCRIBERS_ATTRIBUTE_NAME, notifiedSubscribers);
    }
 
    ForumHandler.UpdateTopic(topic);
}

Note that the implementation above calls the GetNotifiedSubscribers method which we haven’t implemented yet. We’ll soon get around to that however.

The GetNotifiedSubscribers, IsNotifiedSubscriber and SetSubscriberAsNotNotified methods

The GetNotifiedSubscribers, IsNotifiedSubscriber and SetSubscriberAsNotNotified methods are very similar to the GetSubscribers, IsSubscriber and RemoveSubscriber methods:

private static IList<IUser> GetNotifiedSubscribers(Topic topic)
{
    return topic.GetAttributeValues<IUser>(
        NOTIFIED_SUBSCRIBERS_ATTRIBUTE_NAME);
}
 
public static bool IsNotifiedSubscriber(Topic topic, IUser user)
{
    IList<IUser> notifiedSubscribers = 
        GetNotifiedSubscribers(topic);
    return IsUserInList(notifiedSubscribers, user);
}
 
public static void SetSubscriberAsNotNotified(Topic topic, IUser user)
{
    IList<IUser> notifiedSubscribers = GetNotifiedSubscribers(topic);
    notifiedSubscribers.Remove(user);
    topic = (Topic)topic.Clone();
    topic.SetAttributeValue<IUser>(
        NOTIFIED_SUBSCRIBERS_ATTRIBUTE_NAME, notifiedSubscribers);
    ForumHandler.UpdateTopic(topic);
}

The SetSubscribersAsNotified method

The SetSubscribersAsNotified method is quite similar to the AddSubscribers method but accepts a list of users instead of a single user as we will most likely notify several users at the same time and we want to minimize the number of database operations by adding setting all of the notified subscribers as notified at the same time.

public static void SetSubscribersAsNotified(Topic topic, IList<IUser> users)
{
    IList<IUser> notifiedSubscribers = GetNotifiedSubscribers(topic);
 
    foreach (IUser userToAdd in users)
        notifiedSubscribers.Add(userToAdd);
 
    topic = (Topic)topic.Clone();
    topic.SetAttributeValue<IUser>(
        NOTIFIED_SUBSCRIBERS_ATTRIBUTE_NAME, notifiedSubscribers);
    ForumHandler.UpdateTopic(topic);
}

The GetSubscribersToNotify method

The final method (phew!) is the GetSubscribersToNotify method which returns a list of all subscribers that have not already been notified:

public static IList<IUser> GetSubscribersToNoify(Topic topic)
{
    IList<IUser> allSubscribers = GetSubscribers(topic);
    IList<IUser> notifiedSubscribers = GetNotifiedSubscribers(topic);
    List<IUser> subscribersToNotify = new List<IUser>();
 
    foreach (IUser subscriber in allSubscribers)
    {
        if(!IsUserInList(notifiedSubscribers, subscriber))
            subscribersToNotify.Add(subscriber);
    }
 
    return subscribersToNotify;
}

Example usage

In the below usage examples I’ll modify the Relate+ templates to implement some of the topic subscription functionality that the TopicSubscriptionUtility class provides.

Automatically add replying users as subscribers

To automatically add replying users as subscribers we modify the ucNewReply_ReplySaved event handler in Topic.ascx.cs in the Relate+ templates. If you don’t use the templates the event handler for the button that the user clicks on to post a new reply would be a good place to insert this functionality into.

protected void ucNewReply_ReplySaved(object sender, EventArgs e)
{
    // Rebind the replies
    BindReplies(1);
 
    //Check if the current user is already a subscriber,
    //otherwise add him as a subscriber. As he has read his
    //own post we set him as notified.
    if (!TopicSubscriptionUtility.IsSubscriber(
        CurrentTopic, CurrentUser))
    {
        TopicSubscriptionUtility.AddSubscriber(
            CurrentTopic, CurrentUser);
        TopicSubscriptionUtility.SetSubscribersAsNotified(
            CurrentTopic, new List<IUser> { CurrentUser });
    }
}

Notifying subscribers

In order to notify subscribers when a new reply is posted we again modify the ucNewReply_ReplySaved event handler in Toic.ascx.cs and add the following code:

//Get a list of all subscribers that should be notified and 
//notify them. Finally set all of the recently notfied users 
//as notified so they wont be notified again until they first
//view the topic again
IList<IUser> usersToNotify = 
    TopicSubscriptionUtility.GetSubscribersToNoify(CurrentTopic);
NotifySubscribers(usersToNotify);
TopicSubscriptionUtility.SetSubscribersAsNotified(
    CurrentTopic, usersToNotify);
}

Note that the NotifySubscribers method will have to be implemented. In it you could send an e-mail to each of the users or notify them in some other way.

Removing users from the list of notified subscribers when viewing the topic

When a user views a topic we need to check whether he is a subscriber to the topic, and if so check if he’s listed as an already notified subscriber. If so, we need to remove him from that list so that he will be notified the next time a reply is posted. In the Relate+ templates there the OnLoad method is a good place to that.

Insert this code

if (Request.IsAuthenticated && 
    TopicSubscriptionUtility.IsNotifiedSubscriber(
        CurrentTopic, CurrentUser))
{
    TopicSubscriptionUtility.SetSubscriberAsNotNotified(
        CurrentTopic, CurrentUser);
}

as marked here

protected override void OnLoad(EventArgs e)
{
    // Call base
    base.OnLoad(e);
 
    if (!IsPostBack)
    {
       ...
        if (CurrentTopic != null)
        {
            ...
            if (CurrentTopic.Room.Forum.ID == 
                Properties.Settings.Default.RootForumId)
            {
                ...
                //INSERT THE ABOVE CODE HERE
            }
            ...
        }
        ...
    }
}

Source code and conclusion

The source code for the TopicSubscriptionUtility class can be downloaded here.

Note that this solution, while easy to implement, is not optimal performance wise. If you are building a forum which will have a huge number of topics and users and where each topic may have thousands of subscribers you should probably build this functionality in another way, perhaps as a custom module. See my article about how to build a custom EPiServer Community module for some ideas about how to do that.

19 April 2009


Comments

  1. Nice!
  2. The link to the source code is broken.
  3. The link to the source code is still broken :(
  4. The code snippets in the article can be copy-pasted and it works fine, so you don't really need the source code :)
  5. Awesome :)
Post a comment    
User verification Image for user verification  
EPiTrace logger