Thoughts about migrating content into Yammer from forums

YammerMig

This article hopes to cover the considerations and possible approach for planning and migrating existing forum content into Yammer. Yammer doesn’t provide any form of ‘migration’ or ‘import’ for other system content. They take the stance that the working paradigms created by Yammer are unique to the platform and that managing the transition from old system to Yammer is where the investments should be made. The Yammer article here describes their high level thoughts on the subject, key for me was the statement:

2. Start fresh with content

Our customers’ experience has shown that technical migrations of content are not generally recommended, as information on Yammer tends to be more ephemeral in nature – not necessarily needed as material for future reference. There is better potential for users to be invested in and engaged with the new network if they are empowered to shape its structure and content themselves.

The realities of Client engagements tends to be slightly less black and white, where most want to consider some form of carry across information into Yammer where they have existing knowledge management forums established. Many of our clients have established forums woven into the day to day behaviours, the information within them can’t be wilted on the vine so to speak.

One key takeaway before diving into the more detailed thoughts….

Consider why you are migrating, unless its aligned to a business benefit realisation or business goal then the effort of the migration is mis-aligned to its value to the organisation. Also consider what your new operating model is, don’t migrate an old operating behaviour along with the content, you MUST migrate the content into the new behaviours and approaches you aim to have using Yammer integrated within your organisation.

Understanding the source forum information

Whatever the technology platform involved whether it is SharePoint forums or another forum technology the key element to a successful migration is auditing and understanding the content within the current forums. Key elements to understand are as follows:

  • Users
  • Forums metadata such as Title, description and logo image
  • Security model
  • Thread content
    • Styles of thread, such as single origin, multi-thread
    • Thread parenting, which forum the earliest multi-origin thread comes from
    • Mentioning users
    • Mentioning metadata
    • Attachments
    • Links
  • Value add activities like adding additional tagging or additional metadata content

From there you need to plan the content destinations in Yammer. Not always will there be a one forum to group mapping. In fact, now may be a sensible time to ‘refactor’ the approach to segregation of the conversations to something that is more relevant or easier to understand.

Users

The source system is unlikely to hold user accounts in the same fashion as Yammer. Therefore a mapping exercise is needed. Yammer users are keyed off the email address (the primary key as such).

A full listing of the users who have interacted with the forum needs to be generated. Alongside this a full extract of the Yammer users information should also be requested. In order to have all users within the Yammer Network you must establish the first date your network existed and pull the data from that date. The Yammer data export will only pull data where a user was interactive for that time period. This means you need to go a long way back for the first extract to get all the required user records. Subsequent users can be pulled in more recent extracts if you need to refresh and append data.

From this point you have the source and destination user lists. The next step is to produce a mapping between them. In general a good approach is to manipulate the records in the source user list to append the email address if possible. If you can get the source users to have a unique email address then you can map them directly against the Yammer user list.

From the mapping exercise you’ll be left with users in both lists who are not matched. Ignore the non-matches in Yammer side as they’re not really going to be a problem for migration as they have never interacted with the source forum. The non matched users in the source user list is where the focus should be placed. You have the following options:

  • Map all non-matched forum users to a generic account in Yammer. Something like Company Alumni, this has the advantage that it’s simple to map, and you can tie everything to one central user. During your Yammer rollout you can then inform the user base about the Alumni user and promote this as historic facts. The disadvantage of this approach is if there is value in knowing who made every thread post. Consider a situation where the discipline for the forum is more detailed and technical, users may still require to know who made which comment in order to trace that individual even outside of the organisation.
  • Map all non-matched forum users to newly created Yammer users. The advantage of this is that the user to thread relationship is maintained. Disadvantages is that you will have to consider how users are managed. If DIRSync is being performed this may be more complex. Also these users need to be active during migration and then suspended once the threads are added.

For the purposes of forum migration it is unlikely you need to consider the values within the user profiles and migrating those, but again this is worth discussing for your scenario.

Groups

Consider how you will be mapping in the existing forums. Do the existing forums contain threads which span wide topics? Could it be beneficial to split the existing forum into smaller segments, in which case the thread mapping to Yammer Groups needs to establish this. Also consider that the groups may not yet exist, so you might have to work out the creation and admin assignment as well. Be careful not to orphan any threads which are topics that no longer fit into the Yammer Group structure.

Also the big difference between Yammer Groups and many forum based technologies is that the Yammer Groups lack any form of hierarchy, so where a source forum may be grouping several sub sections the Yammer Groups don’t behave in this fashion. My thoughts here is that you should structure the Yammer Network to support the new working patterns you are creating. It is likely that the Yammer rollout is in support of other initiatives. So consider if the new Groups are topic/subject centric now, and how you might have to map threads into these topics. Many of the implementations we have been rolling out use a SharePoint site to aggregate the groups into a form of hierarchy or associations. This is a loose coupling though and a sensible approach, the association to hierarchy is only appropriate if the user scenario warrants it, such as a knowledge network.

Group metadata such as name, logo and description are also key for discovery by users within the Yammer Network. Consider also telling users which the primary language is for the group. Yammer has a pretty good translation mechanism using Bing Translate, but some groups naturally centre around a specific language.

Consider User membership for new Yammer Groups, do you want to add the users from the existing forums to the new Yammer Group? Sometimes this is a straight mapping one to one, sometimes if you re-jig the structure it becomes more complex. A simple algorithm is to capture all the users involved in the group threads and make sure they get added to the Group members. Again this in principal goes against what I’d normally be proposing, Users really shouldn’t be forcibly added to group it’s bad practice for good engagement and really annoys people.

Threads and Content

Threads and content form the cornerstone of the forum experience, in Yammer these map roughly to ‘Conversations’ and individual ‘Yams’. The Yammer paradigm is quite straight forward structure wise.

  • Network – The overall container for all the content
  • Groups – The segmentation of the content within the network into groupings of people who generate content
  • Conversation (aka a thread) – The conversations which are started and replied too
  • Yam (aka a message) – An individual post within the conversation thread.

Basics

Each thread message needs to be analysed for the following:

  • It’s parent message, so that it can be correctly associated into a new Yammer conversation
  • Any forum members mentioned in the content, so that the correct @mentions can be inserted into the new Yam
  • Any tagging used, so that the correct @mentions can be inserted into the new Yam (I know it sounds weird to say @ mentions again, but the user and tags are effectively the same with some meta data differences)
  • Any attachments being referenced by the message, so that the correct handling approach can be used (more about attachments in a minute)
  • Any Hrefs within the content, so that you can decide how to deal with the link (more about that in a few more lines)

Links

Handling Hrefs can take a couple of formats:

  • Keep them as is and just ensure they map into the content specification of Yammer
  • Modify the url against a mapping rule set, creating a permanent redirection to other migrated content
  • Modify the url against a mapping service, creating a permanent redirection but to something that can manage the redirection as content moves around in supporting platforms

If you go for an external link redirection service consider how long this will be in place for. Consider the overall benefits of redirection and watch the analytics tracing to ensure that users are still getting benefits from the service.

I did consider using SharePoint query rules as a way to manage redirection. Imagine that you create key threads in Yammer, you can create the links within them to point to a SharePoint site, each site can have its own query rules for content promotion. So where you’re knowledge management community also has a SharePoint site collection you can implement a local collection of search schema search query rules. If you take this approach you can effectively ‘map’ a Href into a search based url with specific terms. Then it fires across to SharePoint and the relevant promoted results would be presented.

Attachments

Dealing with attachments can take several forms:

  • Uploading the content into Yammer directly and effectively storing that content in Yammer
  • Making reference to external sources such as a web page where the content lives

Depending on your requirements may mean choosing the option most relevant almost on a message by message basis. My recommendation would be to store information outside of Yammer where it better aligns with collaboration systems such as Office365 sites, and upload directly where it makes more sense as social content. The obvious examples are a rule set which treats all Office document formats as upload to SharePoint and things such as images go into Yammer. You may also decide to treat some messages as ‘OpenGraph’ and have Yammer treat them as more like a notification than a message.

Thread sharing

Yammer allows a thread to be shared once created to another group. When sharing the thread sharer can choose to add additional text to the conversation share. Once shared the thread is effectively a new thread with its own content. So with this in mind you need to analyse the forum sharing paradigm in the current forums and decide how to map them into Yammer.

In situations where a thread appears across multiple forums. For example:

  • Original post goes into forum A, then is shared to forum B and C
  • When a user view either forum A,B or C the entire thread appears, thus capturing all the conversation once, but displaying it in multiple places

You need to consider how this maps into the Yammer sharing paradigm. I think the most sensible approach is to decide which Yammer Group the thread is most suited for, this is probably a manual decision in most cases. Once targeted for a specific group post the whole thread to that group, then share the complete thread into other groups. It might also be work retrospectively adding ‘tags’ to the thread.

Topics

Yammer has a tagging concept called ‘Topics’. Each thread can be tagged with topics to help discoverability. A user can elect to follow a specific Topic. So when migrating content analyse the content for potential topics and ensure they are tagged in the Yam with Topics. This is a value add activity and would require quite a lot of intelligence to identify topics in existing threads.

Dates

As you add new Yams to the Yammer threads you can’t influence the date stamp. So if you require date stamps to be known the only real solution is to append the information into the Yam text.

Security

Always a thorny issue, the conflicting paradigms of information security and engagement and openness working like a network.

There is definitely no right answer about how to handle security, so these thoughts are where I’d propose you started.

Yammer’s paradigm really needs Yammer Groups to remain publically visible and readable to reap the benefits from working like a network. So you need to plan how to ensure information which might have been secure remains so. Often the existing forums are restricted security wise by ‘membership’ as a way to keep a tight control of who can edit, often driven by reporting and funding concerns about membership numbers rather than true data sensitivity. For this scenario in most cases adopting the public group paradigm is fine.

Where you need a private group, say for example the Board, then using a Private group is perfectly acceptable.

You will need to audit the information in every thread for its compliance if you want to be 100% sure that you’ve not shown restricted information from the existing forums in the new Yammer Groups.

User engagement and roll out

Although not strictly migration in the technical sense don’t under estimate how you need to deal with users moving them from the old world into the new. Thinking about how to help users find threads that have moved.

One possible solution to this is to add a new reply in the existing forums which contains information about the thread location within Yammer. This helps anyone who has the old thread bookmarked come away with a smile as they can navigate to the Yammer conversation and continue the discussions. It also allows for a time of dual running where some but not all threads have made it into Yammer.

Another aspect to consider is publishing the whole process for the end user to read. Sounds little bonkers at first, why should users care or be told about the internals of an IT project. Well if you think about what Yammer has as a central pillar… it’s openness. So help users get into the correct mind set.

Yammer Technical

App Registration

The processing app must be registered within Yammer. Instructions for the App Registration process can be found in the Yammer Developer Center: https://developer.yammer.com/introduction/#gs-registerapp

Once the migration processing app is registered the Client Id and Client secret can be used by the app to make calls into the Yammer Network.

Authentication

The processing app must authenticate to Yammer in order to make calls the API. Information about the authentication process and options can be found in the Yammer Developer Center: https://developer.yammer.com/authentication/

OAuth Impersonation

To interact with the Yammer API on behalf of another user you will need to use a Verified Admin account for the processing app and have it call the relevant REST API endpoint to collect the user token for the user in question. Information about impersonation can be found in the Yammer Developer Center: https://developer.yammer.com/authentication/#a-impersonation

Users

The Yammer API allows you to retrieve, create, update and suspend (soft delete) users. Information about the exact API call format can be found here: https://developer.yammer.com/restapi/#rest-users

Rate limits

The migration processing app must have logic to throttle the calls made to Yammer as to not exceed these thresholds. It should also monitor the responses and act accordingly to retry and ensure data integrity in the event of API calls being blocked. Information about the Yammer REST API rate limits can be found on the Yammer Developer Center: https://developer.yammer.com/restapi/#rest-ratelimits

The content at the time of writing is as follows:

API calls are subject to rate limiting. Exceeding any rate limits will result in all endpoints returning a status code of 429 (Too Many Requests). Rate limits are per user per app. There are four rate limits:

Autocomplete: 10 requests in 10 seconds.

Messages: 10 requests in 30 seconds.

Notifications: 10 requests in 30 seconds.

All Other Resources: 10 requests in 10 seconds.

These limits are independent e.g. in the same 30 seconds period, you could make 10 message calls and 10 notification calls

Pre-reqs

Also useful during the process is the availability of an Enterprise Yammer network, mainly so you can get the admin functions like data export and impersonation working. The question over having a discreet ‘dev’ network came up, my personal thoughts are as follows. If you’re not ‘live’ with Yammer while doing the migration work then why not simplify the moving parts by keeping just the main Network. At the end of the day you can always try things in a private group first to write your processing code, then open it up once the code is tested. Worst case you add messages you need to delete later. This keeps costs down. If however you’re already using Yammer, then best to go for multiple Networks. Again it’s your choice, but Yammer is a service that you can’t actually customise, so your options are safer in my opinion.

NB: This information is my pre-execution thinking and hopefully after the work is complete I’ll come back and make any amendments to the information.

SUGUK 1st March 2012

LondonSUGUK

So last night saw the return of the London SUGUK.

Matt Taylor has stepped down as co-coordinator for London and I want to thank Matt for all his efforts and establishing such a great user group.

So Steve Smith started proceedings with some ITPro and End User snippets. Starting with a small 3 slide long PowerPoint he dived straight into some demos.

The demos began with some SMTP love, must admit most of this talk of AD etc went a little over my head so frantic note taking was the order of the day. There were some neat tips about delegating administration via a customised MMC console and how to get around some production system challenges regarding SharePoint self-managing AD stuff.

Next demo came down to SQL Server databases and the fact that SP doesn’t always do the best defaults. I learnt some nice things about pre-creating a SQL database then using Central Admin to ensure that defaults were better suited to a production scale. Also the points about database growth were very interesting and I could see how these could be quickly leveraged to improve performance.

Finally was a quick run through of some Office functionality connecting search into the Office application and how to configure ‘save as’ links from the MySite UPA.

Then a short break in proceedings gave me just enough time to connect up my laptop and hope it played nice with the projector…. something about this W520 hates projectors….

Breathing a sigh of relief I launched into a Dev centric topic of Extending the Activity Feed within SP2010. The demo source code can be downloaded from code download and the slides can be seen below.

Thanks again for Steve for inviting me to present as it’s always a pleasure dusting off Visual Studio and sharing something cool with the SUGUK. Open-mouthed smile

SharePoint Saturday 2011

SPSUK

Saturday 12th November saw the 2011 SharePoint Saturday at Nottingham’s Conference Centre. Another great event organised with so many quality speakers and companies in attendance.

I presented a session on Social Intranet and the slides can be seen below. I hope everyone found the session useful Smile I certainly enjoyed presenting to such an interactive audience.

Just goes to thank everyone involved in the organisation and planning for the event, it was awesome again SmileSmile

Ratings and SharePoint Search better together

SearchResultsWithRatings.png

SharePoint 2010 introduces a new feature to allow a user to rate content within the sites. Depicted by five stars the user can rate content and these ratings are collated to provide the average rating for the item. Displaying content based on ratings can assist users determining the quality of content easily. Ratings will also help content authors understand which content is considered to be higher quality by the readers.

The native rating user interface is a collection of five stars. After a user selects the desired rating it is submitted and averaged with the other ratings for the content. The ratings are processed by a timer job process so there is some small delay.

By default ratings can be configured on lists and libraries and also added to page layouts. They also get fed into the Activity Feed system as rating activities. These add some great user focused features, but in my opinion there is a missing piece.

Search….

So I got thinking having seen some articles about additional search managed properties. Seeing as the average rating and rating count are both columns added to the list/library I decided to investigate whether it was possible to index and therefore present the rating within the search results.

So here the journey of rating discovery begins…

Adding ratings to lists or libraries

 

Before we can investigate getting ratings into the search experience we need to get rating up and running on some content. So lets go ahead and create a document library as our demo content. Upload numerous documents into the library (at least five) so that we have some content to rate.

So that’s the content ready for rating so what next?

  • Browse to the library.
  • From the ‘Library’ ribbon click the ‘Library settings’ button.
  • Under the general heading click the ‘Rating settings’ link.
  • Under the ‘Allow items in this list to be rated’ click yes and ‘Ok’.

This adds two columns to the library. The ‘Rating (0-5)’ and ‘Number of Ratings’. By default the Rating column is added to the view. You can also add the number column to the view if you want (more of that idea later). It should also be noted that these columns will be added to any bound content types as well.

That’s prepared the content for rating so now go ahead and click some ratings. For my demo I logged in as several users and rated the documents with ratings to demonstrate several of the number of stars.

DocLibRating

 

Timer job

 

As mentioned earlier the ratings are calculated via a timer job called {user profile SA name} – Social Rating Synchronization Job. This job aggregates the ratings. To speed up the development you can manually execute the timer job to cause the rating aggregation.

RatingJob

 

Search Managed Properties

 

So with the native rating functionality function configured and ready for indexing it’s time to swing over to the farm Search Service Application to perform the steps needed to index our ratings.

As with any element within SharePoint content for it to get indexed it needs to be a managed property. By default most common content fields are already configured. Ratings however are not setup in this way. So the first step is to create these managed properties for the ‘Rating (0-5)’ and ‘Number of Ratings’ columns.

 

AverageRating Managed Property

 

To create the average rating property map the ‘ows_AverageRating(Decimal)’ as shown in the screenshot below.

AverageRatingProp

 

RatingCountProp

Once the properties are configured a index of the content source is required. Browse to the ‘content sources’ and start a crawl. While this is whizzing along in the background the modifications to the search results web part can be made.

 

Search Results web part

 

The standard Search Results web part provides the view of the content found matching the query term.

DefaultSearchResults

As you can see there are no ratings information displayed. So we’re going to modify the results web part to include the rating and rating count below the content description.

 

Adding the columns to fetched data

 

To be able to show the AverageRating and RatingCount results they need to be added to the ‘Fetched Properties"’ xml within the web part settings (the circled element in the screen shot below)

AddingColumns

Modify the xml to add the new columns to it. The example below lists the new columns last, you can copy this or append your existing list.

<Columns>
<Column Name="WorkId"/>
<Column Name="Rank"/>
<Column Name="Title"/>
<Column Name="Author"/>
<Column Name="Size"/>
<Column Name="Path"/>
<Column Name="Description"/>
<Column Name="Write"/>
<Column Name="SiteName"/>
<Column Name="CollapsingStatus"/>
<Column Name="HitHighlightedSummary"/>
<Column Name="HitHighlightedProperties"/>
<Column Name="ContentClass"/>
<Column Name="IsDocument"/>
<Column Name="PictureThumbnailURL"/>
<Column Name="PopularSocialTags"/>
<Column Name="PictureWidth"/>
<Column Name="PictureHeight"/>
<Column Name="DatePictureTaken"/>
<Column Name="ServerRedirectedURL"/>
<Column Name="AverageRating"/>
<Column Name="RatingCount"/>
</Columns>

Apply the changes to the web part. Now the data is coming back within the search result set.

 

Modifying the XSLT

 

Next step is to get these properties displaying.

XSLEdit

I’m not that skilled in front end coding so this will demo the concept and I’m sure those more creative design peeps will add their own flare to the visuals Winking smile

So I’m choosing to inject the rating and number of raters just after the title and description. Therefore locate the ‘<div class="srch-Metadata2">’ div to inject the new code. Below is a snippet from the section and includes the calls to the new templates.

<div class="srch-Metadata2">

<xsl:call-template name="stars">
<xsl:with-param name="starCount" select="averagerating"/>
</xsl:call-template>
<xsl:call-template name="ratingcount">
<xsl:with-param name="ratingCount" select="ratingcount"/>
</xsl:call-template>
<br/>

<xsl:call-template name="DisplayAuthors">
<xsl:with-param name="author" select="author" />
</xsl:call-template>…..

As you can see I’ve introduced two new templates, one for each property.

Before we dive into the templates there is something important to share. To improve performance most visual images displayed in css sprite format. This means that css class positions the images to display the required section from a large map of images. The rating control is no different and uses the sprite image found ‘/_layouts/Images/Ratings.png’

 

Ratings

 

So the template to render the stars needs to make use of the same native css classes.

The following is the star rating template. It contains the logic to read the rating value and generate the relevant css positioned rating image.

<!– The Stars displaying –>
<xsl:template name="stars">
<xsl:param name="starCount"/>

<span class="ms-currentRating">
<!– Set the correct css sprite for the number of stars –>
<xsl:choose>
<xsl:when test="$starCount &gt;= 4.5" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 5 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_5" alt="Current average rating is 5 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 4.5 and $starCount &lt; 5" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 4.5 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_4_5" alt="Current average rating is 4.5 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 4 and $starCount &lt; 4.5" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 4 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_4" alt="Current average rating is 4 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 3.5 and $starCount &lt; 4" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 3.5 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_3_5" alt="Current average rating is 3.5 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 3 and $starCount &lt; 3.5" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 3 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_3" alt="Current average rating is 3 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 2.5 and $starCount &lt; 3" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 2.5 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_2_5" alt="Current average rating is 2.5 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 2 and $starCount &lt; 2.5" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 2 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_2" alt="Current average rating is 2 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 1.5 and $starCount &lt; 2" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 1.5 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_1_5" alt="Current average rating is 1.5 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 1 and $starCount &lt; 1.5" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 1 star.</xsl:text>
</xsl:attribute>
<img class="ms-rating_1" alt="Current average rating is 1 star." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:when test="$starCount &gt;= 0.5 and $starCount &lt; 1" >
<xsl:attribute name="title">
<xsl:text>Current average rating is 0.5 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_0_5" alt="Current average rating is 0.5 stars." src="/_layouts/Images/Ratings.png" />
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="title">
<xsl:text>Current average rating is 0 stars.</xsl:text>
</xsl:attribute>
<img class="ms-rating_0" alt="Current average rating is 0 stars." src="/_layouts/Images/Ratings.png" />
</xsl:otherwise>
</xsl:choose>
</span>

</xsl:template>

 

As well as the rating we’re adding the number or people who have rated. This gives the consuming user a decent idea of the level of interest the item has had. The following is the rating count template.

<!– The Rating Count displaying –>
<xsl:template name="ratingcount">
<xsl:param name="ratingCount"/>

<xsl:choose>
<xsl:when test="$ratingCount = 1" >
<xsl:text>Rated by 1 person.</xsl:text>
</xsl:when>
<xsl:when test="$ratingCount &gt;= 1" >
<xsl:text>Rated by </xsl:text>
<xsl:value-of select="$ratingCount" />
<xsl:text> people.</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>Not rated.</xsl:text>
</xsl:otherwise>
</xsl:choose>

</xsl:template>

 

With all these changes applied to the web part settings, save and publish the page and you should find the results now look something like the following screenshot.

SearchResultsWithRatings

Ok so that is pretty cool, users can now see content and peoples ratings of them to assist in finding the right things. It’s not quite the whole story on all the cool things to display, refiners are the other.

 

Refining by Ratings

 

With the ratings being displayed in the results it got me thinking that having a refiner for the rating value and number of people who had rated.

So the native refinement web part allows the customisation of the refiners via its settings. One important point to note is that the web part settings will have no effect unless the following check box is unchecked.

RefinerCheckBox

The Rating refiner is below.

<Category Title="Rating"
          Description="The average rating for the item." 
          Type="Microsoft.Office.Server.Search.WebControls.ManagedPropertyFilterGenerator"
          MetadataThreshold="5"
          NumberOfFiltersToDisplay="4"
          MaxNumberOfFilters="0"
          SortBy="Frequency"
          SortDirection="Ascending"
          SortByForMoreFilters="Name"
          SortDirectionForMoreFilters="Ascending"
          ShowMoreLink="True"
          MappedProperty="AverageRating"
          MoreLinkText="show more"
          LessLinkText="show fewer"/>

The Number of Ratings refiner is below.

<Category Title="Number of ratings"
          Description="The number of ratings from people for the item."
          Type="Microsoft.Office.Server.Search.WebControls.ManagedPropertyFilterGenerator"
          MetadataThreshold="5"
          NumberOfFiltersToDisplay="4"
          MaxNumberOfFilters="0"
          SortBy="Frequency"
          SortDirection="Ascending"
          SortByForMoreFilters="Name"
          SortDirectionForMoreFilters="Ascending"
          ShowMoreLink="True"
          MappedProperty="RatingCount"
          MoreLinkText="show more"
          LessLinkText="show fewer"/>

Add these to the XML property of the web part.

RefinerXml

Save the page and you end up with the refiners for ratings and number of ratings available.

RefinersAndSearch

Going the extra step with the refiners

So the initial refiners display any found values. A nice proof of concept would be to add some filter groups to group rating values together into Bronze, Silver, Gold instead of 0-5. That is possible with the refiner property grouping using ranges.

Wrap-up

Hopefully this no-code solution adds some extra sweetness to the use of ratings and search in equal measure.

A status by any other name would still smell as sweet

Status
While investigating status updates from within the Activity Feed it was necessary to understand the native social status update logic. The aim was to allow commenting a-la Facebook against any of the activity feed events in the feed. This article presents the information learnt about the native status update logic.
 
An example Facebook comment:
 
Status1
 
 
Fundamentally this could be achieved by adding a new ‘Social Comment’ against that item, I suspect there will be some challenges with this approach like the fact it would generate ‘Note Board’ activity events by default. Before diving into that I thought it would be a sound idea to understand more about where the data comes from for each activity type. This article covers the Profile Status.
 
In my previous post about the Activity Templates you can find the template specific for the ‘Status update’
Keyname:
"ActivityFeed_Status_Message_SV_Template"
Template:
{Publisher} says "{Value}".
 
This is used to display the Status update to your colleagues.
 
So first up I wanted to understand what happens when you type a new status.
 
Status2
 
 
The control which is used is the ‘Microsoft.SharePoint.Portal.WebControls.StatusNotesControl’. This control renders the required input box and references for the client side script to make this pretty seamless for the end user. Under the bonnet it sets a property called ‘SPS-StatusNotes’ against the current user’s profile.
 
Status3
 
 
Looking at the profile property several take-away information nuggets are worth noting.
  • The default max length for a status update is 512 characters
  • The privacy setting is ‘Everyone’
  • The ‘show in the profile properties section of the users profile page’ is unchecked. (If checked the value would have appeared below ‘Development’ in the screenshot above)
  • The property is not configured to be indexed by Search. This could be useful to enable if you wanted to show this value within the results page or build a component to return these results using search.
Once committed the ‘People’ search incremental crawl captures this change marker and the next time the Activity Feed timer job executes it will generate the status update event.

Activity Feed Item Templates

ActivityFeed

The SharePoint Activity Feed rendering is controlled by templates. These templates are used within the ActivityTemplate class http://msdn.microsoft.com/en-us/library/microsoft.office.server.activityfeed.activitytemplate.aspx which is responsible for controlling the rendering.

The OOB templates are held within a resources file located:

C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\Resources\osrvcore.resx

This contains the following templates:

Keyname: “ActivityFeed_ProfilePropertyChange_SV_Template”
Template: {Publisher} updated profile.&lt;br/&gt;{Name}: {Value}

Keyname: “ActivityFeed_ProfilePropertyChange_MV_Template”
Template: {Publisher} updated profile.&lt;br/&gt;{List}

Keyname: “ActivityFeed_Birthday_Reminder_SV_Template”
Template: {Publisher} is celebrating a birthday on {Name}.

Keyname: “ActivityFeed_Birthday_Today_SV_Template”
Template: {Publisher} is celebrating a birthday today.&lt;br/&gt;Wish {Publisher} a happy birthday!

Keyname: “ActivityFeed_WorkplaceAnniversary_Reminder_SV_Template”
Template: {Publisher} is celebrating a {Value} year workplace anniversary on {Name}.

Keyname: “ActivityFeed_WorkplaceAnniversary_Today_SV_Template”
Template: {Publisher} is celebrating a {Value} year workplace anniversary today.&lt;br/&gt;Wish {Publisher} a happy anniversary!

Keyname: “ActivityFeed_ColleagueAddition_SV_Template”
Template: {Publisher} added a new colleague.&lt;br/&gt;{Link}

Keyname: “ActivityFeed_ColleagueAddition_MV_Template”
Template: {Publisher} added {Size} new colleagues.&lt;br/&gt;{List}

Keyname: “ActivityFeed_TitleChange_SV_Template”
Template: {Publisher} has a new job title.&lt;br/&gt;{Value}

Keyname: “ActivityFeed_ManagerChange_SV_Template”
Template: {Publisher} has a new manager. &lt;br/&gt; {Link}

Keyname: “ActivityFeed_BlogUpdate_SV_Template”
Template: {Publisher} published a new blog post.&lt;br/&gt;{Link}

Keyname: “ActivityFeed_DLMembershipChange_SV_Template”
Template: {Publisher} has a new membership. &lt;br/&gt; {Link}

Keyname: “ActivityFeed_DLMembershipChange_MV_Template”
Template: {Publisher} has {Size} new memberships. &lt;br/&gt; {List}

Keyname: “ActivityFeed_SocialTaggingByColleague_SV_Template”
Template: {Publisher} tagged {Link} with {Link2}.

Keyname: “ActivityFeed_SocialTaggingByColleague_MV_Template”
Template: {Publisher} tagged {Link}.&lt;br/&gt;{List}

Keyname: “ActivityFeed_NoteboardPosts_SV_Template”
Template: {Publisher} posted a note on {Link}.&lt;br/&gt;{Value}

Keyname: “ActivityFeed_SocialTaggingByAnyone_SV_Template”
Template: {Publisher} tagged {Link} with your interest.&lt;br/&gt;{Link2}

Keyname: “ActivityFeed_SocialRatings_SV_Template”
Template: {Publisher} rated {Link} as {Value} of {Name}.

Keyname: “ActivityFeed_SharingInterest_SV_Template”
Template: {Publisher} shares an interest with you. &lt;br/&gt; {Value}

Keyname: “ActivityFeed_SharingInterest_MV_Template”
Template: {Publisher} shares {Size} interests with you. &lt;br/&gt; {List}

This contains the following template names:

Keyname: “ActivityFeed_ChangeMarker_SV_Template”
Template: Previous Gatherer Run

Keyname: “ActivityFeed_Status_Message_SV_Template”
Template: {Publisher} says “{Value}”.

Keyname: “ActivityFeed_ProfilePropertyChange_Type_Display”
Template: Profile update

Keyname: “ActivityFeed_Birthday_Reminder_Type_Display”
Template: Upcoming birthday

Keyname: “ActivityFeed_Birthday_Today_Type_Display”
Template: Birthday

Keyname: “ActivityFeed_WorkplaceAnniversary_Reminder_Type_Display”
Template: Upcoming workplace anniversary

Keyname: “ActivityFeed_WorkplaceAnniversary_Today_Type_Display”
Template: Workplace anniversary

Keyname: “ActivityFeed_ColleagueAddition_Type_Display”
Template: New colleague

Keyname: “ActivityFeed_TitleChange_Type_Display”
Template: Job title change

Keyname: “ActivityFeed_ManagerChange_Type_Display”
Template: Manager change

Keyname: “ActivityFeed_BlogUpdate_Type_Display”
Template: New blog post

Keyname: “ActivityFeed_DLMembershipChange_Type_Display”
Template: New membership

Keyname: “ActivityFeed_SocialTaggingByColleague_Type_Display”
Template: Tagging by my colleague

Keyname: “ActivityFeed_NoteboardPosts_Type_Display”
Template: Note Board post

Keyname: “ActivityFeed_SocialTaggingByAnyone_Type_Display”
Template: Tagging with my interests

Keyname: “ActivityFeed_SharingInterest_Type_Display”
Template: Sharing Interests

Keyname: “ActivityFeed_ChangeMarker_Type_Display”
Template: Gatherer Change Marker

Keyname: “ActivityFeed_SocialRatings_Type_Display”
Template: Rating

Keyname: “ActivityFeed_Status_Message_Type_Display”
Template: Status Message

The curly brackets are then replaced by the UI web part rendering code.

Hopefully this sheds a little bit more light on the internals of the activity feed internals.

Extending the activity feed with enterprise content

ActivityFeed

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.