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.
- Consolidated feed:
- Published feed:
- http://<mysitehost>/_layouts/activityfeed.aspx?publisher=<accountname>
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
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.
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
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.
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);
}
}
}
}
{Publisher} has uploaded a new image {Link} into the {Link2} library.
{Publisher} uploaded {Size} new images into the {Link} library. <br/> {List}
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;
}
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;
}
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 ImageLink 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;#endregionactivityEvent.Commit();return activityEvent;
}
return null;
}
return null;
}
}
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 Imagesforeach (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);
}#endregionactivityEvent.Commit();return activityEvent;
}
return null;
}
return null;
}
}
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);
}
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);
}
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.
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);
{Publisher} has uploaded a new image into the {Link2} library. <br/> {ImageThumbnail}
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);
}
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();
}
Figure 3: Single value image in the custom SUI web part
Wes, Thanks for this post. I just finished completing a link and photo to news feed solution which is no small task. Especially when getting around the User Profile Service ownership issue for regular users.
Then I stumbled across this and see the potential here for posting when management blogs have been posted to etc…
The problem is I have never used a lot of these things before and fear messing my dev server up quite a bit. (Timers for example…) I hate when people ask for code straight up so perhaps you can post screen shots of your solution explorer or something else with a little more overview? Thank you.
Hi Anthony,
If you are worried about corrupting you development environment I would suggest reading Tristan Watkin’s articles about creating a virtual environment which provides you mechanisms to revert to a stable state. The information can be found here
My article covers all of the complex elements of the implementation specific with the Social API and applying these to ECM data. If you require assistance with the plumbing such as the timerjob I would suggest the following excellent links:
Andrew Connell’s article on TimerJobs here
Social data SDK samples for SP2010 here
These resources should provide the help around these elements.
Cheers,
Wes
Hi Wes,
It’s nice article. Can you please explain the following block section
//Select it
var itemData = from g in dtAllItems.AsEnumerable()
select g;
Hi Ranjeet,
That line is LINQ syntax to copy the datatable contents into an enumerable collection that the following Lambda expressions work against.
Check out MSDN for information on LINQ and Lambda.
Cheers,
Wes
Hi Wes,
Thanks for your reply. I was trying this at my development environment but i would like to know from where we have to create dtAllItems? what is dtAllItems containing? Thanks in advance.
That’s the results datatable from the SiteDataQuery.
Thanks Wes for your reply. Is it possible to write activity feed for particular user rather than colleagues?
I assume you mean to broadcast a generated event to specific user? As generating an event for a specific user is what you are already doing when you create the ActivityEvent in the example logic. To broadcast to different user’s than the originating event user’s colleagues is actually quite simple. If you check out the logic within the ‘MulticastPublishedEvents’ method you will see a collection of MinimalPerson objects which are generated by this:
Dictionary<long, List> colleaguesOfOwners;
ActivityFeedGatherer.GetUsersColleaguesAndRights(activityManager, publishers, out owners, out colleaguesOfOwners);
If you simply hydrate that collection yourself with the users you wish to publish events too then that should be what you are after.
Once again thanks a lot for your reply. I am able to create event for single user. As of now i am using foreach loop for broadcasting event for userprofile collection (i know it is not better approach).from your replay i understood that we can broadcast event to multiuser by using hydrate that collection. Can you please share some links about how to hydrate collection (i am new for .net development). Thanks in advance.
You need to instantiate a MinimalPerson object for each user you wish to broadcase the source event too. Then add these to the collection using the .Add() method.
The purpose of the source event and boradcast is so that you can represent a user doing ‘something’ and telling other users about it (the broadcast bit).
Thanks for the reply. I will look into the links you provided.
Thanks again.
How to get The ActivityEventId set after the creation of a new ActivityEvent?
After i created an ActivityEvent (activityEvent.Commit()) the ActivityEventId =-1
I tried to create a SyndicationItem:
SyndicationItem syndi = activityEvent.CreateSyndicationItem(actMgr.ActivityTypes, ContentType.Html);
But syndi.Id = -1 activityEvent.ActivityEventId =-1 🙁
Executing ActivityManager.GetActivitiesByMe();
I find the event with the ActivityEventId set
Is there a way to get the real ActivityEventId after I’m creating the event?
Pelase help me
The ID is normally hydrated once you’ve called the CreateActivityEvent method.
ActivityEvent activityEvent = ActivityEvent.CreateActivityEvent(activityManager, createType.ActivityTypeId, owner, publisher);
I’m not sure how you would get an item by the ID, interesting there are no API’s that support getting an ActivityEvent by specific ID
Wes,
Anthony again… I have come along way and created event listeners for special lists which then create the newsfeed events asynchronusly. Now I would like to render images in the newsfeed, like you have done in Figure 3: Single value image in the custom SUI web part. I have the additonal resource item set up along with the solution deployed correctly. I am just having trouble overriding the web part as the orinigial PublishedNewsFeedWebPart seems to be sealed. Any advice/links? Thank you once again. This article has helped in countless ways.
If you want to enhance the display further then you have to replicate the OOB web part logic so that you can add the additional logic to create the display your require. I did this using reflector, it should be noted you can improve the MS OOB code though around the de-serialisation of the returned objects. I found that creating a serialiser and reusing it throughout the collection drastically improved the performance. Once you have a reasonable copy of the OOB you can decide how to identify the new token and create its rendering. The area to look is the OOB ActivityTemplateVariable class as this is reposnible for the serialisation object and the token replacement logic for rendering.
So take a nose through the OOB code and it should highlight how they got it working by combining the data and the token masks from the resx files. Then you can go ahead and replicate the logic as you need.
Cheers,
Wes
Awesome. Thanks for the tips… A lot of work ahead!
Great post thanks, just one question do you know if the api has been updated to allow the removal of custom activity events ?
To the best of my knowledge these missing API’s are still not implemented by Microsoft.
Thanks for the update, i’m really concerned about implmenting extentions to the activity feed, for this reason. Can you offer any advise on how to handle this ?
I’ve personally put extensions into more than six large scale projects and have had little problem with the missing delete API. On the rare occasion you need to remove the entries you can only really do this via SQL. Obviously this goes against the golden rule of SharePoint, but if needs must and you’re sensible, backup before actions and the removing required rows is possible *at your own risk*.
Hi Wes,
You stated:
“At the time of writing there is a missing API implementation for the removal of ActivityApplication. The API throws a ‘NotImplementedException’.”
do you know if this missing module has been addressed/added now? If not, do you have any insights if/when it’ll be added?
Or do you know how to remove/hide an activity application from edit profile settings page?
Thanks.
Amir
Hi Wes,
For the Gatherer, were does this code go? In Feature activation so it runs once?
And what is base class? I see this override:
protected override void CreateActivityTypes()
The gatherer is executed during each timerjob execution. As for the override i just had some helpers in a base class and some abstracts to make sure I hooked up the correct logic sequences.
This ‘gatherer’ approach is taken from the pattern the native timerjobs use, if you break out your favourite decompiler and trace through the timerjobs you’ll see the gatherers.
Hi Wes,
Another question. What is the benefit of applying this (with custom Gatherer) instead of only creating a custom activity?
For example this post:
http://blogs.microsoft.co.il/blogs/johnnyt/archive/2011/03/26/working-with-sharepoint-2010-user-activity-newsfeed.aspx
Gatherer and the associated timerjob are there for scale. Doing anything directly from an UI invoked action only scales to a point.
Also if i followed the link article code correctly it never broadcast the generated activity to that user’s colleagues. It would only show on that persons profile feed (Published Feed) and not on their colleagues (Consolidated Feed) feed on the newsfeed page.
Have you done anything in this area with extending the functionality to include likes and comments on the social feed? Of course a lot of (expensive) 3rd party tools are out there, but I am considering writing my own for an internal project at my company. I dont want to go down the road of having to make a sepaarate data model or db tables for the comments, but not sure if the OOTB social feeds can accomodate commenting otherwise. Thanks for an excellent article
Hi Bill,
Yeah I’ve done a lot of work on this particular implementation and you can create something very complex without those third party products. Chris O’Brien recently wrote an article about these features from our project http://www.sharepointnutsandbolts.com/2012/03/extending-sharepoint-2010-social.html
You will struggle to create them using only the native API’s as they don’t really support the complexity required to provide the comment and republishing of the comments to colleagues.
We architected our likes, comments and subscriptions into a custom Service Application and associated database to provide the data store.
That’s the same thought I had and is the same approach at least 2 of the popular 3rd party tools are using. Thanks again.
I was wondering if activity feeds can be built around entities other than “people” or “users”. For instance, if I have a customer or industry entity with data in various systems relating to instances of those entities, could I create an activity feed around them?
Hi Wek,
I want to check/uncheck the activities under Newsfeedsettings > Activities I am following ….using custom solution..like Sharing Interest,…for this i am trying with custom code using SharePoint empty project > I wrote the code in FeatureActivated event…
Thanks in advance,
Raj