Select Page

Introduction

 

Amongst the most anticipated new features of SharePoint 2010 were the social activity feed features which bring colleague activity as a feed to an individual. Natively the activity feed displays user profile changes, tagging and notes activity. Microsoft provides an API to extend the activity feed system with your own content. With this extensibility API it is possible to extend this to include enterprise content activity, such as activity around documents. Bringing ECM data into the activity feed gives a full 360 degree picture of activity within the SharePoint system. This article describes key areas of the logic and elements required to achieve this.

What do we get out of the box?

 

Before diving into how to extend the activity feed it’s important to understand what we already have. The activity system collects two main areas of data. Firstly user profile changes such as job title, colleague changes, shared interest and anniversary reminders. Secondly are the social feedback events such as tags, notes, ratings and personal blog posts.

Both types are generated by a scheduled timer job which queries for the changes and generates new activity events for an individual. The process then broadcasts this activity to the individual’s colleagues.

There are two web part displays of an individual’s activity feed. The consolidated feed is everything I track. This type is used on My Newsfeed page. The published feed is my activities. This type is used on My Profile page.
 
The data is also available as ATOM:

Key concepts

 

There are a number of key extensibility concepts to highlight when working with the Activity system. The activity application is the container which contains the activity types. It is the parent container of the extensions being built. The activity type is the ‘what’ definition for example ‘New Image Added’. The activity template defines the formatting on an activity event. The template is a tokenized string used during the rendering process. It also has a flag indicating whether it displays multi-value or single activity events. The activity event is the instance of the activity type displayed using the activity template.

Building the extension

 

There are three elements to the extension, the activity application, the templates and types used in the gatherer and finally the user interface. Throughout this article I will assume you are familiar with basic SharePoint coding and focus on the specific API calls related to the extensions. All the code samples provided will in places utilize the Patterns and Practices logging which again is excluded for brevity.

Enterprise information covers a large amount of data and data types so a sensible approach is to build a gathering service for each required ECM data type then register these all within the same activity application to provide the collective link. This provides a good level of scheduling granularity for the platform admin team.

The activity application

 

The first step is to create a Farm scoped feature to register the custom activity application. Let’s call our ActivityApplication ‘ECMActivity’. Listing 1 contains the code required to register a new ActivityApplication into the platform.

public ActivityApplication EnsureActivityApplication()
{
ActivityApplication app=null;
if (SPServiceContext.Current != null)
{
ActivityManager activityManager = new ActivityManager();
//Check to see that the current user has permissions to access the social data
if (activityManager.PrepareToAllowSchemaChanges())
{
app = activityManager.ActivityApplications[“ECMActivity”];
if (app == null)
{
app = activityManager.ActivityApplications.Create(“ECMActivity”);
app.Commit();
app.Refresh(true);
//log to ULS and event log
logger.LogToOperations(ActivityFeedConstants.ActivityApplication + " created.", TraceAreaCategory);
}
return app;
}
else
{
try
{
//The process user doesn’t have access to profiles service as manage social data role
return activityManager.ActivityApplications[“ECMActivity”];
}
catch (ActivityFeedPermissionDeniedException ex)
{
logger.LogToOperations(ex, ErrorAreaCategory);
}
}
}
return app;
}

Listing 1: Register the custom ActivityApplication

At the time of writing there is a missing API implementation for the removal of ActivityApplication. The API throws a ‘NotImplementedException’.
 

These methods are encapsulated into an ‘ECMActivityFeedApplication’ object and have the feature receiver call the relevant methods during activate and deactivate. Once this feature is active there is now a custom container in the system for all the individual ECM activity feed types to be registered within.

Gathering activities

 

What do we mean by gathering? Well in the case of the Activity system gathering is the process actioned to query for changes and generate new activity events for both published and consolidated feeds. A recommendation would be to use encapsulation to design the gathering mechanism so that ECM data types are self-contained allowing easier management.

There are a couple of mechanisms that could be used for the gatherer. For ECM data, event receivers on the item added and updated events become a possible gathering point. After investigation these proved too expensive from a performance perspective and also required some workarounds for the elevation required for certain API calls.
The other obvious option is a timer job. The out of the box gathering is done through timer jobs which indicates that this is a better approach. There are several advantages, performance wise as it executes away from the WFE servers and the limitations of the web process, timer jobs also allow more granular execution and scheduling. Therefore the ECM extensions are designed around the timer job approach.
 

A web application scoped feature registers the ActivityType into the custom activity application via methods on the Gatherer object and registers the timer job.

Gathering timer job

 
You need to implement a web application scoped timer job and deployment feature to register the job on the application servers. This timer job is responsible for executing the gatherer for the required ECM data type. As the timer job gathering execution logic is relatively intensive it would be advised to only execute on the application servers.
 

Timer jobs are only the scheduling mechanism, so the actual job calls down onto a custom ‘Gatherer’ object which provides an execute method. During investigations it became apparent that certain ECM activity occurs more frequently than others. This helped to determine an approach where each ECM data type is its own timer job and gatherer pair. This allows the admin to change the scheduling frequency as required for distinct types of activity.

The Gatherer

 

The data gathering and event generation are handled by the custom ‘Gatherer’. The out of the box contains two gatherers, ‘ActivityFeedGathererProfile’ which creates the profile based events and ‘ActivityFeedGathererSocial’ which creates the ratings, comments and tag events.

Following this model each ECM data type you wish to collect as activity should be implemented as a custom Gatherer. Let’s examine what the gatherer contains for Image ECM data as an example.
 
As mentioned earlier the gatherer contains the register and unregister logic for the activity type. The registration logic first adds the ActivityType and commits this change to the system. The next step is to register both single and multi-value templates. A single value template is used when the activity event instance contains only a single image for example. For multiple image events the multi-value template is used. The activity feed system determines whether the event is single or multi-value based on whether the ActivityEvent ‘LinksList’ property contains objects. Listing 2 shows the logic to register the ActivityType and the ActivityTemplates for the Image example. Note that you only define one Type while you have single and multi-value templates associated with it.
protected override void CreateActivityTypes()
{
if (SPServiceContext.Current != null)
{
//Instantiate a new activity manager… remember the account will need to be admin on
//User profile service app or you’ll need to elevate
ActivityManager activityManager = new ActivityManager();
ActivityApplication app = new ActivityFeedECMApplication().EnsureActivityApplication();
if (app != null)
{
ActivityType createType = app.ActivityTypes["ActivityFeed_Image_ItemAdded"];
if (createType == null)
{
createType = app.ActivityTypes.Create("ActivityFeed_Image_ItemAdded");
createType.ActivityTypeNameLocStringResourceFile = "ActivityFeedECM";
createType.ActivityTypeNameLocStringName = "ActivityFeed_Image_ItemAdded_Type_Display";
createType.IsPublished = true;
createType.IsConsolidated = true;
createType.Commit();
createType.Refresh(true);
}
//Create the associated Single Value template
ActivityTemplate createTemplate = createType.ActivityTemplates[ActivityTemplatesCollection.CreateKey(false)];
if (createTemplate == null)
{
createTemplate = createType.ActivityTemplates.Create(false);
createTemplate.TitleFormatLocStringResourceFile = "ActivityFeedECM";
createTemplate.TitleFormatLocStringName = "ActivityFeed_Image_ItemAdded_SV_Template";
createTemplate.IsMultivalued = false;
createTemplate.Commit();
createTemplate.Refresh(true);
}
ActivityTemplate createMVTemplate = createType.ActivityTemplates[ActivityTemplatesCollection.CreateKey(true)];
if (createMVTemplate == null)
{
createMVTemplate = createType.ActivityTemplates.Create(true);
createMVTemplate.TitleFormatLocStringResourceFile = "ActivityFeedECM";
createMVTemplate.TitleFormatLocStringName = "ActivityFeed_Image_ItemAdded_MV_Template";
createMVTemplate.IsMultivalued = true;
createMVTemplate.Commit();
createMVTemplate.Refresh(true);
}
}
}
}
Listing 2: Register ActivityType and its templates
 

Removal is almost the reverse of registration, first remove the associated ActivityTemplates then finally remove the ActivityType. Again it is important to note the API has not yet been implemented.
 
The templates live in a resource file deployed to ‘14/Resources’. Each template is matched to a tokenized resource string. Listing 3 shows the template used for a single value ActivityEvent rendering, as you can see the curly braces define the tokens. These tokens are then replaced by the rendering logic of the web parts. Listing 4 shows the multi-value template, the main difference being the inclusion of the {List} token which will define the multiple images during rendering.
{Publisher} has uploaded a new image {Link} into the {Link2} library.
Listing 3: ActivityFeed_Image_ItemAdded_SV_Template template
{Publisher} uploaded {Size} new images into the {Link} library. <br/> {List}
Listing 4: ActivityFeed_Image_ItemAdded_MV_Template template
 
That covers the registration of the ActivityType and its templates. Next let’s look at the generation of the events when the timer job executes.
 
The first step during execution is to retrieve the required data. One technique that can be used is to iterate the site collections and perform a specific query using a SPSiteDataQuery with the relevant CAML query. For small scale SharePoint systems this is likely to meet the performance requirements for the gatherer execution and server load. For enterprise level systems it would be recommended to utilize the change log to retrieve the required data.
 
The security permissions for the User Profile Service Application should be configured to give the timer service account ‘Full control’ so that is can correctly access certain API methods for the ActivityManager. Listing 5 demonstrates how to create an ActivityManager using elevated UserProfileManager and context. You will require the ActivityManager in many places throughout the logic.
try
{
//This is the creation of an elevated activity manager
//If you don’t do this nothing works!!!
SPServiceContext context2 = SPServiceContext.GetContext(aSite);
userProfileManager = new UserProfileManager(context2);
string adminUserName = Environment.UserDomainName + "\\" + Environment.UserName;
if (!userProfileManager.UserExists(adminUserName))
{
Logger.TraceToDeveloper("Profile does not exist for:  " + adminUserName + " so calling create.", TraceAreaCategory);
userProfileManager.CreateUserProfile(adminUserName);
}
UserProfile p = userProfileManager.GetUserProfile(adminUserName);
activityManager = new ActivityManager(p, context2);
}
catch (UserProfileApplicationNotAvailableException ex)
{
//This is an example of logging to the operations version
Logger.LogToOperations(ex, 0, EventSeverity.Error, ErrorAreaCategory);
//You might not need to throw… up to your needs
throw;
}
Listing 5: Creating a ActivityManager with elevated security
 
So at this point we’ve retrieved the data, created an ActivityManager. The next step is to begin processing the data into ActivityEvents. In order to determine whether data should be single or multi-value you need to create an algorithm to process the data.
 
In order to facilitate the process you could use a ProcessingContainer. This holds various information for example the SPListItem, Owner and some calculated values which indicate if it’s a multi-value and has items with unique permissions. Figure 1 shows the ProcessingContainer class diagram.
 
DIWUG_Figure1_WesHackett
Figure 1: Class diagram for the ProcessingContainer
Now you may obviously have your own requirements which determine single versus multi-value, the logic detailed in Listing 6 may provide a proven starting point. The basic logic is that the data is sorted by location, then date created and finally by the created by user. This basically means that events have to be in the same location, by the same user and within a defined timespan (5 minutes in the listing) to qualify to become multi-value events.
 
As you can see the use of the processing container is to collect the information such as owner and to hold the SPListItem(s). You might be wondering why the SPListItems are being collected and not just relying on the data returned by the query. To facilitate the security trimming logic and being able to call the SPListItem method ‘DoesUserHavePermissions’ storing the SPListItem means you only have to retrieve the item once during the processing. Again for performance you will need to assess whether this approach would meet your requirements. Other approaches might involve using search based trimmers. You would also then need to extend the container with required data fields to hold the required attributes for the ActivityEvent information.
Logger.TraceToDeveloper("Sorting data", TraceAreaCategory);
TimeSpan fiveMinuteTimeSpan = new TimeSpan(0, 5, 0);
//Select it
var itemData = from g in dtAllItems.AsEnumerable()
select g;
var sortedItemData = itemData.OrderBy(item => (item["EncodedAbsUrl"] + (item["FileRef"].ToString().Substring(0, item["FileRef"].ToString().LastIndexOf("/") + 1)).Substring(item["FileRef"].ToString().IndexOf("#") + 1))).ThenByDescending(x => x["Author"]).ThenBy(x => x["Created"]);
string previousUrl = String.Empty;
string previousOwner = String.Empty;
DateTime previousDateTime = DateTime.Now;
bool firstCycle = true;
//Instantiate the processing containers collection
List<ProcessingContainer> activityContainers = new List<ProcessingContainer>();
//Create the first container before entering the
ProcessingContainer currentContainer = null;
//Iterate the sorted collection to generate the processing container
foreach (var sortedItem in sortedItemData)
{
string url = sortedItem["EncodedAbsUrl"] + (sortedItem["FileRef"].ToString().Substring(0, sortedItem["FileRef"].ToString().LastIndexOf("/") + 1)).Substring(sortedItem["FileRef"].ToString().IndexOf("#") + 1);
string owner = sortedItem["Author"].ToString().Substring(sortedItem["Author"].ToString().IndexOf("#") + 1);
DateTime dateTime = Convert.ToDateTime(sortedItem["Created"].ToString());
string cleanFileRef = sortedItem["FileRef"].ToString().Substring(sortedItem["FileRef"].ToString().IndexOf("#") + 1);
Logger.TraceToDeveloper("Sorting data for location " + url, TraceAreaCategory);
Logger.TraceToDeveloper("Sorting data for the file " + cleanFileRef, TraceAreaCategory);
TimeSpan timeDifference = dateTime – previousDateTime;
//Only check if we’ve been through before
if (!firstCycle)
{
Logger.TraceToDeveloper("Not First data cycle", TraceAreaCategory);
//Check to see if this item should become a multivalue addition to the current processing container
if ((url == previousUrl) && (owner == previousOwner) && (timeDifference.Ticks < fiveMinuteTimeSpan.Ticks))
{
Logger.TraceToDeveloper("The event matched a previous item", TraceAreaCategory);
//If no container create a new one
if (currentContainer == null)
{
currentContainer = new ProcessingContainer();
currentContainer.Url = url;
currentContainer.Owner = owner;
}
//Get the list item
using (SPSite siteCollection = new SPSite(url))
{
using (SPWeb myWeb = siteCollection.OpenWeb())
{
SPListItem currentItem = myWeb.GetListItem(cleanFileRef);
if (currentItem != null)
{
currentContainer.ListItems.Add(currentItem);
}
}
}
//Set the previous values ready for the next run through
previousOwner = owner;
previousUrl = url;
previousDateTime = dateTime;
}
else
{
Logger.TraceToDeveloper("The event didn’t matched a previous item", TraceAreaCategory);
//We failed to match current so commit the processing container as completed
if (currentContainer != null)
{
activityContainers.Add(currentContainer);
currentContainer = null;
}
//Start a new cycle
if (currentContainer == null)
{
currentContainer = new ProcessingContainer();
currentContainer.Url = url;
currentContainer.Owner = owner;
}
using (SPSite siteCollection = new SPSite(url))
{
using (SPWeb myWeb = siteCollection.OpenWeb())
{
SPListItem currentItem = myWeb.GetListItem(cleanFileRef);
if (currentItem != null)
{
currentContainer.ListItems.Add(currentItem);
}
}
}
previousOwner = owner;
previousUrl = url;
previousDateTime = dateTime;
}
}
else
{
//The item didnt match the previous so will begin a new container
if (currentContainer == null)
{
currentContainer = new ProcessingContainer();
currentContainer.Url = url;
currentContainer.Owner = owner;
}
using (SPSite siteCollection = new SPSite(url))
{
using (SPWeb myWeb = siteCollection.OpenWeb())
{
SPListItem currentItem = myWeb.GetListItem(cleanFileRef);
if (currentItem != null)
{
currentContainer.ListItems.Add(currentItem);
}
}
}
previousOwner = owner;
previousUrl = url;
previousDateTime = dateTime;
firstCycle = false;
}
}
//We failed to match current so commit the processing container as completed
if (currentContainer != null)
{
Logger.TraceToDeveloper("Commit the final item", TraceAreaCategory);
activityContainers.Add(currentContainer);
currentContainer = null;
}
Listing 6: example sorting and processing of returned data
 
Once all the processing is complete the next stage is to generate the ActivityEvent objects. While iterating the ProcessingContainer collection the logic decides whether a single or multi-value event should be generated. Added complexity here is the logic for dealing with multi-value containers which have items containing unique permissions. It would be recommended that if an item has unique permissions that it becomes a single value. This ensures that once multicasting to colleagues occurs trimming logic for colleagues is easier.
 
If the container contains a single SPListItem or comes from a unique permissioned SPListItem then we need to create a single value ActivityEvent. Listing 7 details the logic to create the ActivityEvent. Most of the code is related to setting various attributes of the event, one special line I’d like to highlight is the ‘imageLink.Value = currentItem["EncodedAbsThumbnailUrl"].ToString();’ line. This might not seem that important but this is where we give ourselves the ability to create more exciting Facebook/LinkedIn style rendering. By using the thumbnail url and assigning it to the ‘Value’ attribute of the link. We can then use this to generate a visual thumbnail in the web part logic. The logic also stores the link to the list containing the item.
protected ActivityEvent CreateSingleValueImageActivityEvent(ActivityManager activityManager, Entity owner, Entity publisher, SPListItem currentItem)
{
using (SPMonitoredScope scope = new SPMonitoredScope("CreateSingleValueImageActivityEvent"))
{
ActivityType createType = activityManager.ActivityApplications["ECMActivity"].ActivityTypes["ActivityFeed_Image_ItemAdded"];
if (createType != null)
{
//Generate a new activity event
//Again if you haven’t elevated the activity manager then this WILL throw access denied!!!
ActivityEvent activityEvent = ActivityEvent.CreateActivityEvent(activityManager, createType.ActivityTypeId, owner, publisher);
if (activityEvent != null)
{
activityEvent.Name = createType.ActivityTypeName;
activityEvent.ItemPrivacy = (int)Privacy.Public;
activityEvent.Owner = owner;
activityEvent.Publisher = publisher;
activityEvent.Value = ActivityFeedServiceConstants.ActivityType.Image.ImageContentTypeValue;
if (currentItem[SPBuiltInFieldId.Last_x0020_Modified] != null)
{
DateTime lastModified = Convert.ToDateTime(currentItem[SPBuiltInFieldId.Last_x0020_Modified].ToString());
activityEvent.DateValue = lastModified;
}
#region Image
Link imageLink = new Link();
//Set the title to the display name
if (!String.IsNullOrEmpty(currentItem.Title))
{
imageLink.Name = currentItem.Title;
}
else
{
imageLink.Name = currentItem.DisplayName;
}
//Set the href of the image
imageLink.Href = (currentItem.ParentList.ParentWeb.Site.MakeFullUrl(currentItem.ParentList.DefaultDisplayFormUrl) + "?ID=" + currentItem.ID.ToString());
//Set the thumbnail url
imageLink.Value = currentItem["EncodedAbsThumbnailUrl"].ToString();
//Set the link 2
activityEvent.Link = imageLink;
#endregion
#region Image Library Link
//Create the Link to the image library root link
Link imageLibraryLink = new Link();
//Set the title to the list title
imageLibraryLink.Name = currentItem.ParentList.Title;
//Set the href for the list default view
imageLibraryLink.Href = SPHttpUtility.UrlPathEncode(currentItem.Web.Site.MakeFullUrl(currentItem.ParentList.DefaultViewUrl),true);
//Set the link to this
activityEvent.Link2 = imageLibraryLink;
#endregion
activityEvent.Commit();
return activityEvent;
}
return null;
}
return null;
}
}
Listing 7: Creating a single value Image ActivityEvent
 
If the ProcessingContainer is a multi-value then we pass it to the method dealing with the generation of multi-valued ActivityEvents. Listing 8 shows the important difference with this logic. The individual SPListItems are processed into ‘Link’ objects which are then added to the ‘LinksList’ collection of the ActivityEvent. This is the way you inform the Activity system that this event is multi-value. You will also see the list link goes into a different property. These are subtle differences to note as they become important during the web part rendering and template resource file values.
protected ActivityEvent CreateMultiValueImageActivityEvent(ActivityManager activityManager, Entity owner, Entity publisher, List<SPListItem> items)
{
using (SPMonitoredScope scope = new SPMonitoredScope("CreateMultiValueImageActivityEvent"))
{
ActivityType createType = activityManager.ActivityApplications["ECMActivity"].ActivityTypes["ActivityFeed_Image_ItemAdded"];
if (createType != null)
{
//Generate a new activity event
//Again if you haven’t elevated the activity manager then this WILL throw access denied!!!
ActivityEvent activityEvent = ActivityEvent.CreateActivityEvent(activityManager, createType.ActivityTypeId, owner, publisher);
if (activityEvent != null)
{
activityEvent.Name = createType.ActivityTypeName;
activityEvent.ItemPrivacy = (int)Privacy.Public;
activityEvent.Owner = owner;
activityEvent.Publisher = publisher;
activityEvent.Value = ActivityFeedServiceConstants.ActivityType.Image.ImageContentTypeValue;
if (items[0][SPBuiltInFieldId.Last_x0020_Modified] != null)
{
DateTime lastModified = Convert.ToDateTime(items[0][SPBuiltInFieldId.Last_x0020_Modified].ToString());
activityEvent.DateValue = lastModified;
}
#region Image Library Link
//Create the Link to the image library root link
Link imageLibraryLink = new Link();
//Set the title to the list title
imageLibraryLink.Name = items[0].ParentList.Title;
//Set the href for the list default view
imageLibraryLink.Href = items[0].Web.Site.MakeFullUrl(items[0].ParentList.DefaultViewUrl);
//Set the link to this
activityEvent.Link = imageLibraryLink;
#endregion
#region Images
foreach (SPListItem currentImage in items)
{
Link imageLink = new Link();
//Set the title to the display name
if (!String.IsNullOrEmpty(currentImage.Title))
{
imageLink.Name = currentImage.Title;
}
else
{
imageLink.Name = currentImage.DisplayName;
}
//Set the href of the image
imageLink.Href = (currentImage.ParentList.ParentWeb.Site.MakeFullUrl(currentImage.ParentList.DefaultDisplayFormUrl) + "?ID=" + currentImage.ID.ToString());
//Set the thumbnail url
imageLink.Value = currentImage["EncodedAbsThumbnailUrl"].ToString();
//Set the link 2
activityEvent.LinksList.Add(imageLink);
}
#endregion
activityEvent.Commit();
return activityEvent;
}
return null;
}
return null;
}
}
Listing 8: Creating a multi value Image ActivityEvent
 
Once the ProcessingContainer collection has been converted to a collection of ActivityEvents the next step is to broadcast them to colleagues. The basic process of broadcasting to colleagues of the event owners is shown in Listing 9.
 
As you see the main methods are provided by the ActivityFeedGatherer object. The logic first gets the colleagues of the event owner. Then it calls the ‘MulticastActivityEvents’ method to generate the ActivityEvents which appear in colleagues ‘Newsfeed’ page.
 
In the Activity system the ActivityEvents generated for the owner of the event appear in the Profile page and are known as Published activity events, those which are multicast are known as Consolidated activity events and these appear on the ‘Newsfeed’ page.
 
While Listing 9 shows the basics of multicasting, further logic would need to be injected here which processed security trimming for each colleague. For example if one of the owners colleagues doesn’t have permission to the underlying ECM item then the logic would need to remove their identifier from the MinimalPerson collection used to multicast to. This code has been excluded from Listing 9 to focus on the essential logic needed but would be injected before the call to ‘ActivityFeedGatherer.MulticastActivityEvents’.
public void MulticastPublishedEvents(List<ActivityEvent> activityEvents, ActivityManager activityManager)
{
if (activityEvents.Count == 0)
{
return;
}
List<long> publishers = new List<long>();
foreach (ActivityEvent activityEvent in activityEvents)
{
if (!publishers.Contains(activityEvent.Owner.Id))
{
publishers.Add(activityEvent.Owner.Id);
}
}
Dictionary<long, MinimalPerson> owners;
Dictionary<long, List<MinimalPerson>> colleaguesOfOwners;
ActivityFeedGatherer.GetUsersColleaguesAndRights(activityManager, publishers, out owners, out colleaguesOfOwners);
Dictionary<long, List<ActivityEvent>> eventsPerOwner;
ActivityFeedGatherer.MulticastActivityEvents(activityManager, activityEvents, colleaguesOfOwners, out eventsPerOwner);
List<ActivityEvent> eventsToMulticast;
ActivityFeedGatherer.CollectActivityEventsToConsolidate(eventsPerOwner, out eventsToMulticast);
WriteEvents(eventsToMulticast);
}
Listing 9: Multicasting the events
 
The final step is to batch write the new ActivityEvents to the Activity system. This is shown in Listing 10 which shows the call to batch commit the events using the ‘ActivityFeedGatherer.BatchWriteActivityEvents’ method.
protected virtual void WriteEvents(List<ActivityEvent> events, ActivityManager activityManager)
{
int startIndex = 0;
while (startIndex + activityManager.MaxEventsPerBatch < events.Count)
{
ActivityFeedGatherer.BatchWriteActivityEvents(events, startIndex, activityManager.MaxEventsPerBatch);
startIndex += activityManager.MaxEventsPerBatch;
}
ActivityFeedGatherer.BatchWriteActivityEvents(events, startIndex, events.Count – startIndex);
}
Listing 10: Writing the events out to the Activity system
 

That concludes the logic for the gathering mechanism. At this point you have the data generation functionality available. The out of the box Activity system will now show new custom ECM events in the ‘Newsfeed’ and profile pages. The new ECM events will also come through the ATOM feeds.

Improving the web parts

 

The out of the box web part renders the ActivityEvent collection using a tokenization mechanism. For each event it derives which type and retrieves the mask from the relevant resource file. Passing the event and mask through to the html generator to render the individual ActivityEvent in the UI. There isn’t an easy way to replace the rendering of out the box ActivityTypes without some custom coding.

The aim is to provide a UI experience that includes a more engaging visual. The first focus is the method required to get the right data for the ‘Newsfeed’ page. The consolidated event web part inherits from a common base web part created to handle the majority of the rendering logic. The consolidated web part is only really responsible for call to the data API as shown in Listing 11. The code is part of the ‘OnPreRender’ in the derived class override. You can see that it uses the overload which takes a date and time and number of items to display to retrieve the required data.
if (this.MinEventTime <= ActivityManager.MinEventTime)
{
base.activityEvents = base.ActivityManager.GetActivitiesForMe(ActivityManager.MinEventTime, this.MaxItemsToDisplay);
}
else
{
base.activityEvents = base.ActivityManager.GetActivitiesForMe(this.MinEventTime, this.MaxItemsToDisplay);
}
base.OnPreRender(e);
Listing 11: Get the consolidated activities from the Activity system
 
During the rendering logic the web part html is constructed. In the out of the box web part each ActivityEvent is passed to the ActivityTemplateVariable object which registers token replacement logic. This class contains the logic which would replace for example the ‘{Publisher}’ token as seen in Listing 4. In order to extend and even replace the token replacement you have to implement your own version of the ActivityTemplateVariable class. The suggested approach is to create your own version and call the ActivityTemplateVariable if you don’t require any special rendering.
 
So as of yet we haven’t seen anything in the Listing 3 and 4 tokens not handled by the ActivityTemplateVariable out of the box logic. In order to create something more visually close to the rich Facebook/LinkedIn UI we need to introduce the concept of the SUI version. The SUI (special user interface) allows us to keep our ECM extension functional through the out of the box Activity system, but also use the same data to render improved UI elements.
So the first step is to add the SUI version of the template mask to the resource file as shown in Listing 12. This mask use the same name but with a suffix of ‘_SUI’. The Listing 3 version compatible with the out of box rendering differs as the SUI version in Listing 12 which removes the ‘{Link}’ token and adds a new custom token ‘{ImageThumbnail}’ token after a line break.
{Publisher} has uploaded a new image into the {Link2} library. <br/> {ImageThumbnail}
Listing 12: Image added single value SUI template mask
 
During rendering when the ActivityTemplate resource name is retrieved the logic in Listing 13 is invoked to check for a SUI version before falling back to the normal template. This technique can also be used to replace the template mask for out of the box templates. The listing shows the ‘osrvcore’ substitution to replace the out of box masks.
protected string GetActivityTypeDisplayFormat(ActivityTemplate template, bool useAlternativeFormat)
{
if (useAlternativeFormat)
{
string format = String.Empty;
if (template.TitleFormatLocStringResourceFile.Contains("OsrvCore"))
{
string resourceFileName = template.TitleFormatLocStringResourceFile.Replace("OsrvCore", "OsrvCore_SUI");
format = GetResourceString(resourceFileName, template.TitleFormatLocStringName + "_SUI", (uint)CultureInfo.CurrentUICulture.LCID);
}
else
{
format = GetResourceString(template.TitleFormatLocStringResourceFile, template.TitleFormatLocStringName + "_SUI", (uint)CultureInfo.CurrentUICulture.LCID);
}
if (!String.IsNullOrEmpty(format))
{
return format;
}
}
return GetResourceString(template.TitleFormatLocStringResourceFile, template.TitleFormatLocStringName, (uint)CultureInfo.CurrentUICulture.LCID);
}
Listing 13: Template mask SUI selection logic
 
Once you have this mask the custom token will be recognized by your ExtendedActivityTemplateVariable logic. Listing 14 shows how you might deal with the ‘{ImageThumbnail}’ token.
public static string ImageThumbnailToString(string tag, ActivityTemplateVariable atv, ContentType ct, CultureInfo ci)
{
StringBuilder builder = new StringBuilder(string.Empty);
switch (ct)
{
case ContentType.PlainText:
return atv.Link.LinkToString(ct, ci);
case ContentType.Html:
if (string.IsNullOrEmpty(atv.Link.Href))
{
builder.Append(SPHttpUtility.HtmlEncodeAllowSimpleTextFormatting(atv.Link.Title));
break;
}
builder.Append("<a href=\"");
builder.Append(atv.Link.Href);
builder.Append("\" title=\"" + atv.Link.Name + "\">");
if (!string.IsNullOrEmpty(atv.Link.Value))
{
builder.Append("<img src=’" + atv.Link.Value + "’ alt=’" + atv.Link.Name + "’ style=’max-width:96px;max-height:96px’ />");
}
else if (!string.IsNullOrEmpty(atv.Link.Name))
{
builder.Append(SPHttpUtility.HtmlEncode(atv.Link.Name));
}
builder.Append("</a>");
break;
default:
return null;
}
return builder.ToString();
}
Listing 14: Logic to create the image thumbnail
Using this technique you can see how a single value image looks in the out of the box in Figure 2 and how it looks in the custom SUI web part in Figure 3.
 
DIWUG_Figure2_WesHackett
Figure 2: Single value image in the out of box web part
 
DIWUG_Figure3_WesHackett

Figure 3: Single value image in the custom SUI web part

Conclusion

 
In this article we have looked at how to extend the SharePoint 2010 Activity system with enterprise content activity. Hopefully this has given you an understanding of the elements and an example approach to implement this extension. Other elements to consider are the security trimming approaches, subscription mechanism so that you don’t have to be colleagues to see activity and embellishing the UI even further with jQuery/Ajax style implementations.